diff --git a/TODO.md b/TODO.md index 3757832..bc58527 100644 --- a/TODO.md +++ b/TODO.md @@ -29,10 +29,10 @@ - [x] Login Customer - [x] Login Customer Gmail - [x] Customer Edit Profile -- [ ] Register Refferal -- [ ] Customer Deposit Manual -- [ ] Customer Deposit Payment Gateway +- [x] Customer Deposit Manual +- [x] Customer Deposit Payment Gateway - [ ] Customer Purchase Voucher +- [ ] Register Refferal - [ ] Customer Share Buyed Voucher, via WA dll - [ ] Customer View Coin History - [ ] Verified Akun diff --git a/app/Http/Controllers/Customer/DepositController.php b/app/Http/Controllers/Customer/DepositController.php new file mode 100644 index 0000000..d59969f --- /dev/null +++ b/app/Http/Controllers/Customer/DepositController.php @@ -0,0 +1,155 @@ +id()) + ->orderBy('updated_at', 'desc') + ->orderBy('is_valid', 'desc'); + + return inertia('Home/Deposit/Index', [ + 'histories' => $histories->paginate(20) + ]); + } + + public function create() + { + return inertia('Home/Deposit/Topup', [ + 'payments' => GeneralService::getEnablePayment(), + ]); + } + + public function store(Request $request) + { + $request->validate([ + 'amount' => 'required|numeric|min:10000', + 'payment' => [ + 'required', + Rule::in([Setting::PAYMENT_MANUAL, Setting::PAYMENT_MIDTRANS]) + ] + ]); + + DB::beginTransaction(); + $deposit = DepositHistory::make([ + 'customer_id' => auth()->id(), + 'debit' => $request->amount, + 'description' => 'Top Up #' . Str::random(5), + 'payment_channel' => $request->payment, + ]); + + if ($request->payment == Setting::PAYMENT_MANUAL) { + $deposit->is_valid = DepositHistory::STATUS_WAIT_UPLOAD; + $deposit->save(); + } + + if ($request->payment == Setting::PAYMENT_MIDTRANS) { + $deposit->is_valid = DepositHistory::STATUS_WAIT_PAYMENT; + $deposit->save(); + + $token = (new MidtransService($deposit, Setting::getByKey('MIDTRANS_SERVER_KEY')))->getSnapToken(); + + $deposit->update(['payment_token' => $token]); + } + + DB::commit(); + + return redirect()->route('customer.deposit.show', ['deposit' => $deposit->id, 'direct' => 'true']); + } + + public function show(Request $request, DepositHistory $deposit) + { + return inertia('Home/Deposit/Detail', [ + 'deposit' => $deposit, + 'accounts' => Account::get(), + 'midtrans_client_key' => Setting::getByKey('MIDTRANS_CLIENT_KEY'), + 'is_production' => app()->isProduction(), + 'direct' => $request->direct + ]); + } + + public function update(Request $request, DepositHistory $deposit) + { + $request->validate([ + 'account_id' => 'required|exists:accounts,id', + 'image' => 'required|image', + ]); + + $file = $request->file('image'); + $file->store('uploads', 'public'); + $deposit->update([ + 'image_prove' => $file->hashName('uploads'), + 'account_id' => $request->account_id, + 'is_valid' => DepositHistory::STATUS_WAIT_APPROVE + ]); + + session()->flash('message', ['type' => 'success', 'message' => 'Upload berhasil, silahkan tunggu untuk approve']);; + } + + public function midtrans_payment(Request $request, DepositHistory $deposit) + { + DB::beginTransaction(); + $transaction_status = $request->result['transaction_status']; + if ($transaction_status == 'settlement' || $transaction_status == 'capture') { + $is_valid = DepositHistory::STATUS_VALID; + $deposit->update_customer_balance(); + } elseif ($transaction_status == 'pending') { + $is_valid = DepositHistory::STATUS_WAIT_PAYMENT; + } else { + $is_valid = DepositHistory::STATUS_INVALID; + } + $deposit->update([ + 'is_valid' => $is_valid, + 'payment_response' => json_encode($request->result), + 'payment_type' => $request->result['payment_type'], + ]); + + DB::commit(); + + return redirect()->route('customer.deposit.show', ['deposit' => $deposit->id]); + } + + public function mindtrans_notification(Request $request) + { + DB::beginTransaction(); + $deposit = DepositHistory::where('id', $request->order_id)->first(); + + if ($deposit != null && $deposit->is_valid != DepositHistory::STATUS_VALID) { + $deposit->fill([ + 'payment_response' => json_encode($request->all()), + 'payment_type' => $request->result['payment_type'], + ]); + + if ($request->transaction_status == 'settlement' || $request->transaction_status == 'capture') { + $deposit->fill(['payment_status' => DepositHistory::STATUS_VALID]); + $deposit->update_customer_balance(); + } elseif ($request->transaction_status == 'pending') { + $deposit->fill(['payment_status' => DepositHistory::STATUS_WAIT_PAYMENT]); + } else { + $deposit->fill(['payment_status' => DepositHistory::STATUS_INVALID]); + } + + $deposit->save(); + } + + DB::commit(); + + return response()->json([ + 'status' => 'ok', + 'order' => $deposit, + ]); + } +} diff --git a/app/Models/Customer.php b/app/Models/Customer.php index e7927bd..f60b84e 100644 --- a/app/Models/Customer.php +++ b/app/Models/Customer.php @@ -27,6 +27,7 @@ class Customer extends Authenticatable 'google_id', 'deposit_balance', 'coin_balance', + 'paylater_balance', 'identity_verified', 'identity_image', 'customer_level_id', diff --git a/app/Models/DepositHistory.php b/app/Models/DepositHistory.php index f40e192..43bbdb7 100644 --- a/app/Models/DepositHistory.php +++ b/app/Models/DepositHistory.php @@ -2,13 +2,22 @@ namespace App\Models; +use Illuminate\Database\Eloquent\Casts\Attribute; +use Illuminate\Support\Carbon; + class DepositHistory extends Model { - const VALID = 0; + const STATUS_VALID = 0; + + const STATUS_WAIT_UPLOAD = 1; + + const STATUS_WAIT_APPROVE = 2; + + const STATUS_WAIT_PAYMENT = 3; - const WAIT = 1; + const STATUS_INVALID = 4; - const INVALID = 2; + const STATUS_REJECT = 5; protected $fillable = [ 'debit', @@ -19,10 +28,75 @@ class DepositHistory extends Model 'related_id', 'is_valid', 'image_prove', + 'account_id', 'payment_token', 'payment_status', 'payment_response', 'payment_channel', 'payment_type', ]; + + protected $appends = [ + 'status', + 'format_human_created_at', + 'format_created_at', + 'amount', + 'image_prove_url' + ]; + + public function status(): Attribute + { + return Attribute::make(get: function () { + return [ + self::STATUS_VALID => ['text' => 'Success', 'color' => 'bg-green-600'], + self::STATUS_WAIT_UPLOAD => ['text' => 'Upload bukti transfer', 'color' => 'bg-red-600'], + self::STATUS_WAIT_APPROVE => ['text' => 'Menunggu Approve', 'color' => 'bg-green-600'], + self::STATUS_WAIT_PAYMENT => ['text' => 'Menunggu Pembayaran', 'color' => 'bg-green-600'], + self::STATUS_INVALID => ['text' => 'Error', 'color' => 'bg-red-600'], + self::STATUS_REJECT => ['text' => 'Reject', 'color' => 'bg-red-600'], + ][$this->is_valid]; + }); + } + + public function formatHumanCreatedAt(): Attribute + { + return Attribute::make(get: function () { + return Carbon::parse($this->created_at)->locale('id')->format('d F Y'); + }); + } + + public function formatCreatedAt(): Attribute + { + return Attribute::make(get: function () { + return Carbon::parse($this->created_at)->locale('id')->format('d M Y H:i:s'); + }); + } + + public function amount(): Attribute + { + return Attribute::make(get: function () { + if ($this->credit == 0) { + return $this->debit; + } + return $this->credit; + }); + } + + public function imageProveUrl(): Attribute + { + return Attribute::make(get: function () { + return $this->image_prove == null ? '' : asset($this->image_prove); + }); + } + + public function customer() + { + return $this->belongsTo(Customer::class); + } + + public function update_customer_balance() + { + $customer = Customer::find($this->customer_id); + $customer->update(['deposit_balance' => $customer->deposit_balance + $this->debit]); + } } diff --git a/app/Models/PaylaterHistory.php b/app/Models/PaylaterHistory.php new file mode 100644 index 0000000..9853922 --- /dev/null +++ b/app/Models/PaylaterHistory.php @@ -0,0 +1,13 @@ +value('value'); + } } diff --git a/app/Services/GeneralService.php b/app/Services/GeneralService.php index 2c1e63c..b8be990 100644 --- a/app/Services/GeneralService.php +++ b/app/Services/GeneralService.php @@ -2,6 +2,8 @@ namespace App\Services; +use App\Models\Setting; + class GeneralService { public static function script_parser($script) @@ -50,4 +52,20 @@ class GeneralService return $item; } + + public static function getEnablePayment() + { + $payment = [ + ['name' => Setting::PAYMENT_MANUAL, 'logo' => null, 'display_name' => 'Transfer Manual'] + ]; + + $midtrans_enable = Setting::getByKey('MIDTRANS_ENABLED'); + if ($midtrans_enable == 1) { + $payment[] = ['name' => Setting::PAYMENT_MIDTRANS, 'logo' => Setting::getByKey('MIDTRANS_LOGO')]; + } + + // Paylater + + return $payment; + } } diff --git a/app/Services/MidtransService.php b/app/Services/MidtransService.php index 1c904f6..cd583d8 100644 --- a/app/Services/MidtransService.php +++ b/app/Services/MidtransService.php @@ -2,55 +2,46 @@ namespace App\Services; +use App\Models\DepositHistory; use Midtrans\Config; use Midtrans\Snap; class MidtransService { - protected $order; + protected $deposit; - public function __construct($order, $serverKey) + public function __construct(DepositHistory $deposit, $serverKey) { Config::$serverKey = $serverKey; Config::$isProduction = app()->isProduction(); Config::$isSanitized = true; Config::$is3ds = true; - $this->order = $order; + $this->deposit = $deposit; } public function getSnapToken() { - $items = $this->order->items->map(function ($item) { - return [ - 'id' => $item->id, - 'price' => $item->amount, - 'quantity' => $item->quantity, - 'name' => $item->item->order_detail, - ]; - }); - - if ($this->order->total_discount > 0) { - $items->add([ - 'id' => 'Discount', - 'price' => -$this->order->total_discount, - 'quantity' => 1, - 'name' => 'DISCOUNT', - ]); - } - $params = [ 'transaction_details' => [ - 'order_id' => $this->order->order_code, - 'gross_amount' => $this->order->total_amount, + 'order_id' => $this->deposit->id, + 'gross_amount' => $this->deposit->debit, ], - 'item_details' => $items->toArray(), + 'item_details' => [[ + 'id' => $this->deposit->id, + 'price' => $this->deposit->debit, + 'quantity' => 1, + 'name' => $this->deposit->description, + ]], 'customer_details' => [ - 'name' => $this->order->customer->name, - 'email' => $this->order->customer->email, - 'phone' => $this->order->customer->phone, - 'address' => $this->order->customer->address, + 'name' => $this->deposit->customer->fullname, + 'email' => $this->deposit->customer->email, + 'phone' => $this->deposit->customer->phone, + 'address' => $this->deposit->customer->address, ], + 'callbacks' => [ + 'finish' => route('customer.deposit.show', ['deposit' => $this->deposit->id]) + ] ]; $snapToken = Snap::getSnapToken($params); diff --git a/database/migrations/2023_05_24_130552_create_customers_table.php b/database/migrations/2023_05_24_130552_create_customers_table.php index 83027f5..c783247 100644 --- a/database/migrations/2023_05_24_130552_create_customers_table.php +++ b/database/migrations/2023_05_24_130552_create_customers_table.php @@ -26,6 +26,7 @@ return new class extends Migration $table->string('google_id')->nullable(); $table->decimal('deposit_balance', 20, 2)->default(0); $table->decimal('coin_balance', 20, 2)->default(0); + $table->decimal('paylater_balance', 20, 2)->default(0); $table->smallInteger('identity_verified')->nullable(); $table->string('identity_image')->nullable(); $table->ulid('customer_level_id')->nullable(); diff --git a/database/migrations/2023_05_24_130646_create_deposit_histories_table.php b/database/migrations/2023_05_24_130646_create_deposit_histories_table.php index 96c8846..2deb771 100644 --- a/database/migrations/2023_05_24_130646_create_deposit_histories_table.php +++ b/database/migrations/2023_05_24_130646_create_deposit_histories_table.php @@ -18,14 +18,15 @@ return new class extends Migration $table->decimal('credit', 20, 2)->default(0); $table->text('description')->nullable(); $table->ulid('customer_id')->nullable(); + $table->ulid('account_id')->nullable(); $table->string('related_type')->nullable(); $table->string('related_id')->nullable(); $table->smallInteger('is_valid')->default(0); $table->string('image_prove')->nullable(); + $table->string('payment_channel')->nullable(); $table->string('payment_token')->nullable(); $table->string('payment_status')->nullable(); $table->string('payment_response')->nullable(); - $table->string('payment_channel')->nullable(); $table->string('payment_type')->nullable(); $table->timestamps(); diff --git a/database/migrations/2023_06_03_033734_create_paylater_histories_table.php b/database/migrations/2023_06_03_033734_create_paylater_histories_table.php new file mode 100644 index 0000000..b56049b --- /dev/null +++ b/database/migrations/2023_06_03_033734_create_paylater_histories_table.php @@ -0,0 +1,37 @@ +ulid('id')->primary(); + + $table->decimal('debit', 20, 2)->default(0); + $table->decimal('credit', 20, 2)->default(0); + $table->text('description')->nullable(); + $table->ulid('customer_id')->nullable(); + + $table->timestamps(); + $table->softDeletes(); + $table->ulid('created_by')->nullable(); + $table->ulid('updated_by')->nullable(); + $table->ulid('deleted_by')->nullable(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('paylater_histories'); + } +}; diff --git a/public/uploads/NJl0MsrOekJjqc2eTqTntDarwqGDmfWTRPvatfqj.png b/public/uploads/NJl0MsrOekJjqc2eTqTntDarwqGDmfWTRPvatfqj.png new file mode 100644 index 0000000..b6f830c Binary files /dev/null and b/public/uploads/NJl0MsrOekJjqc2eTqTntDarwqGDmfWTRPvatfqj.png differ diff --git a/public/uploads/O28lqSXxDFqVCmtJ3xYgZgMpCTblUNFbhnGFUaky.png b/public/uploads/O28lqSXxDFqVCmtJ3xYgZgMpCTblUNFbhnGFUaky.png new file mode 100644 index 0000000..893a840 Binary files /dev/null and b/public/uploads/O28lqSXxDFqVCmtJ3xYgZgMpCTblUNFbhnGFUaky.png differ diff --git a/public/uploads/Ro0dn8ddZ3YD83BPGRmpwDBi8MDgseSk3Vrz9rXA.png b/public/uploads/Ro0dn8ddZ3YD83BPGRmpwDBi8MDgseSk3Vrz9rXA.png new file mode 100644 index 0000000..b6f830c Binary files /dev/null and b/public/uploads/Ro0dn8ddZ3YD83BPGRmpwDBi8MDgseSk3Vrz9rXA.png differ diff --git a/public/uploads/TOvqXsbQBjbJTIM6he1ylcPLK5VM38BXWBLSSaLE.png b/public/uploads/TOvqXsbQBjbJTIM6he1ylcPLK5VM38BXWBLSSaLE.png new file mode 100644 index 0000000..b6f830c Binary files /dev/null and b/public/uploads/TOvqXsbQBjbJTIM6he1ylcPLK5VM38BXWBLSSaLE.png differ diff --git a/public/uploads/Tb00StkRPTn9EOhn1osSFQAZNKJpbRjCFYH22Qul.png b/public/uploads/Tb00StkRPTn9EOhn1osSFQAZNKJpbRjCFYH22Qul.png new file mode 100644 index 0000000..485ae9e Binary files /dev/null and b/public/uploads/Tb00StkRPTn9EOhn1osSFQAZNKJpbRjCFYH22Qul.png differ diff --git a/public/uploads/xBp4LHXEpYZMFVMefQWomjZtXzZF6eSI3PitI9xH.png b/public/uploads/xBp4LHXEpYZMFVMefQWomjZtXzZF6eSI3PitI9xH.png new file mode 100644 index 0000000..485ae9e Binary files /dev/null and b/public/uploads/xBp4LHXEpYZMFVMefQWomjZtXzZF6eSI3PitI9xH.png differ diff --git a/resources/js/Layouts/Partials/routes.js b/resources/js/Layouts/Partials/routes.js index 2fb4418..9c46eae 100644 --- a/resources/js/Layouts/Partials/routes.js +++ b/resources/js/Layouts/Partials/routes.js @@ -24,38 +24,7 @@ export default [ active: 'dashboard', permission: 'view-dashboard', }, - { - name: 'Customer', - show: true, - icon: HiUserCircle, - route: route('customer.index'), - active: 'customer.*', - permission: 'view-customer', - }, - { - name: 'Customer Level', - show: true, - icon: HiUserCircle, - route: route('customer-level.index'), - active: 'customer-level.*', - permission: 'view-customer-level', - }, - { - name: 'Bank Akun', - show: true, - icon: HiBanknotes, - route: route('account.index'), - active: 'account.*', - permission: 'view-account', - }, - { - name: 'Setting', - show: true, - icon: HiCog, - route: route('setting.index'), - active: 'setting.*', - permission: 'view-setting', - }, + { name: 'Lokasi', show: true, @@ -95,6 +64,30 @@ export default [ }, ], }, + + { + name: 'Customer', + show: true, + icon: HiUser, + items: [ + { + name: 'Customer', + show: true, + icon: HiUserCircle, + route: route('customer.index'), + active: 'customer.*', + permission: 'view-customer', + }, + { + name: 'Level', + show: true, + icon: HiUserCircle, + route: route('customer-level.index'), + active: 'customer-level.*', + permission: 'view-customer-level', + }, + ], + }, { name: 'User', show: true, @@ -118,4 +111,20 @@ export default [ }, ], }, + { + name: 'Bank Akun', + show: true, + icon: HiBanknotes, + route: route('account.index'), + active: 'account.*', + permission: 'view-account', + }, + { + name: 'Setting', + show: true, + icon: HiCog, + route: route('setting.index'), + active: 'setting.*', + permission: 'view-setting', + }, ] diff --git a/resources/js/Pages/Home/Deposit/Detail.jsx b/resources/js/Pages/Home/Deposit/Detail.jsx new file mode 100644 index 0000000..427a450 --- /dev/null +++ b/resources/js/Pages/Home/Deposit/Detail.jsx @@ -0,0 +1,257 @@ +import React, { useState, useEffect } from 'react' +import { Head, router, useForm, usePage } from '@inertiajs/react' +import { HiChevronLeft } from 'react-icons/hi2' + +import CustomerLayout from '@/Layouts/CustomerLayout' +import { formatIDR } from '@/utils' +import FormFile from '@/Components/FormFile' +import { isEmpty } from 'lodash' +import Alert from '@/Components/Alert' + +const PayButton = () => { + const { + props: { deposit, midtrans_client_key, is_production, direct, flash }, + } = usePage() + + const [loading, setLoading] = useState(false) + + const handleResult = (result) => { + fetch(route('api.midtrans.payment', deposit), { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ result }), + }).finally(() => { + router.get(route(route().current(), deposit)) + }) + } + + const onClickPay = () => { + if (loading) { + return + } + setLoading(true) + window.snap.pay(deposit.payment_token, { + // Optional + onSuccess: function (result) { + console.log(result) + handleResult(result) + setLoading(false) + }, + // Optional + onPending: function (result) { + console.log(result) + handleResult(result) + setLoading(false) + }, + // Optional + onError: function (result) { + console.log(result) + handleResult(result) + setLoading(false) + }, + onClose: function () { + setLoading(false) + }, + }) + } + + useEffect(() => { + //change this to the script source you want to load, for example this is snap.js sandbox env + let midtransScriptUrl = 'https://app.sandbox.midtrans.com/snap/snap.js' + if (is_production) { + midtransScriptUrl = 'https://app.midtrans.com/snap/snap.js' + } + //change this according to your client-key + let scriptTag = document.createElement('script') + scriptTag.src = midtransScriptUrl + // optional if you want to set script attribute + // for example snap.js have data-client-key attribute + scriptTag.setAttribute('data-client-key', midtrans_client_key) + + document.body.appendChild(scriptTag) + + if (direct === 'true') { + setTimeout(() => { + onClickPay() + }, 1000) + } + + return () => { + document.body.removeChild(scriptTag) + } + }, []) + + return ( +
{payment.display_name}
+ ) : ( + + )} +