From c63245bf6894a25914cb0f871edfcdcddd51013d Mon Sep 17 00:00:00 2001 From: Yoga Pangestu Date: Wed, 5 Nov 2025 13:49:18 +0700 Subject: [PATCH] feat(order): throw transaction jika stok produk tidak mencukupi --- .../Forms/Studio/Manage/OrderForm.php | 149 ++++++++++-------- app/Traits/Order/WithUpdateStock.php | 26 ++- 2 files changed, 98 insertions(+), 77 deletions(-) diff --git a/app/Livewire/Forms/Studio/Manage/OrderForm.php b/app/Livewire/Forms/Studio/Manage/OrderForm.php index 1943d7e..79ef6a9 100644 --- a/app/Livewire/Forms/Studio/Manage/OrderForm.php +++ b/app/Livewire/Forms/Studio/Manage/OrderForm.php @@ -115,81 +115,90 @@ public function store() // Wrap the entire operation in a database transaction // Ensures atomicity — if any step fails, all changes are rolled back - DB::transaction(function () use ($invoiceNumber, $cogs, $subtotal, $discount, $total, $items, $pointsEarned) { - $member = User::whereHas('customer', fn ($q) => $q->where('id', $this->member_id)) - ->with(['membership' => fn ($q) => $q->lockForUpdate()]) - ->first(); - - $order = Order::create([ - 'outlet_id' => $this->outlet_id, - 'user_id' => auth()->id(), - 'voucher_id' => $this->voucher_id == '' ? null : $this->voucher_id, - 'customer_id' => $this->member_id == '' ? null : $this->member_id, - 'invoice_number' => $invoiceNumber, - 'cogs' => $cogs, - 'subtotal' => $subtotal, - 'discount' => $discount, - 'total' => $total, - 'channel' => $this->channel, - 'status' => $this->status, - 'payment_method' => $this->payment_method, - 'ordered_at' => now(), - ]); - - // Attach all order items to the newly created order - $items->each(function ($item) use ($order) { - $item->order_id = $order->id; - $item->save(); - - // Deduct item stock from the outlet inventory - $this->decreaseOutletStock($order->outlet, $item); - }); - - // Automatically record payment for the order - Payment::create([ - 'order_id' => $order->id, - 'user_id' => auth()->id(), - 'method' => $this->payment_method, - 'amount' => $total, - 'status' => PaymentStatus::PAID->value, - ]); - - // Deduct voucher quantity - if ($this->voucher_id) { - $userVoucher = DB::table('user_voucher') - ->where('user_id', $member->id) - ->where('voucher_id', $this->voucher_id) - ->lockForUpdate() + try { + DB::transaction(function () use ($invoiceNumber, $cogs, $subtotal, $discount, $total, $items, $pointsEarned) { + $member = User::whereHas('customer', fn ($q) => $q->where('id', $this->member_id)) + ->with(['membership' => fn ($q) => $q->lockForUpdate()]) ->first(); - if ($userVoucher) { - if ($userVoucher->quantity > 1) { - DB::table('user_voucher') - ->where('id', $userVoucher->id) - ->update([ - 'quantity' => $userVoucher->quantity - 1, - 'updated_at' => now(), - ]); - } else { - DB::table('user_voucher') - ->where('id', $userVoucher->id) - ->delete(); + $order = Order::create([ + 'outlet_id' => $this->outlet_id, + 'user_id' => auth()->id(), + 'voucher_id' => $this->voucher_id == '' ? null : $this->voucher_id, + 'customer_id' => $this->member_id == '' ? null : $this->member_id, + 'invoice_number' => $invoiceNumber, + 'cogs' => $cogs, + 'subtotal' => $subtotal, + 'discount' => $discount, + 'total' => $total, + 'channel' => $this->channel, + 'status' => $this->status, + 'payment_method' => $this->payment_method, + 'ordered_at' => now(), + ]); + + foreach ($items as $item) { + // Deduct item stock from the outlet inventory + $result = $this->decreaseOutletStock($order->outlet, $item); + + if ($result && ! $result['status']) { + throw new \Exception($result['message']); + } + + // Attach all order items to the newly created order + $item->order_id = $order->id; + $item->save(); + } + + // Automatically record payment for the order + Payment::create([ + 'order_id' => $order->id, + 'user_id' => auth()->id(), + 'method' => $this->payment_method, + 'amount' => $total, + 'status' => PaymentStatus::PAID->value, + ]); + + // Deduct voucher quantity + if ($this->voucher_id) { + $userVoucher = DB::table('user_voucher') + ->where('user_id', $member->id) + ->where('voucher_id', $this->voucher_id) + ->lockForUpdate() + ->first(); + + if ($userVoucher) { + if ($userVoucher->quantity > 1) { + DB::table('user_voucher') + ->where('id', $userVoucher->id) + ->update([ + 'quantity' => $userVoucher->quantity - 1, + 'updated_at' => now(), + ]); + } else { + DB::table('user_voucher') + ->where('id', $userVoucher->id) + ->delete(); + } } } - } - // Update member tier points - if ($member && $member->membership) { - $member->membership()->update([ - 'tier_points' => DB::raw("tier_points + $pointsEarned"), - 'reward_points' => DB::raw("reward_points + $pointsEarned"), - 'last_transaction_at' => now(), - ]); - } - }); + // Update member tier points + if ($member && $member->membership) { + $member->membership()->update([ + 'tier_points' => DB::raw("tier_points + $pointsEarned"), + 'reward_points' => DB::raw("reward_points + $pointsEarned"), + 'last_transaction_at' => now(), + ]); + } + }); + } catch (\Exception $e) { + return [ + 'status' => false, + 'message' => $e->getMessage(), + ]; + } - return [ - 'status' => true, - ]; + return ['status' => true]; } } diff --git a/app/Traits/Order/WithUpdateStock.php b/app/Traits/Order/WithUpdateStock.php index bbf7ca5..2eb02a6 100644 --- a/app/Traits/Order/WithUpdateStock.php +++ b/app/Traits/Order/WithUpdateStock.php @@ -73,16 +73,28 @@ public function decreaseOutletStock($outlet, $item) return; } - $existing = $outlet->{$relation}()->withPivot('stock')->where("{$relation}.id", $item->orderable_id)->first(); + $existing = $outlet->{$relation}() + ->withPivot('stock') + ->where("{$relation}.id", $item->orderable_id) + ->lockForUpdate() + ->first(); - if ($existing) { - $currentStock = $existing->pivot->stock ?? 0; - $newStock = max(0, $currentStock - $quantity); - $outlet->{$relation}()->updateExistingPivot($item->orderable_id, [ - 'stock' => $newStock, - ]); + if (! $existing) { + return ['status' => false, 'message' => "Produk {$item->orderable->name} tidak ditemukan di {$outlet->name}."]; } + $currentStock = $existing->pivot->stock ?? 0; + + if ($currentStock < $quantity) { + return ['status' => false, 'message' => "Stok produk {$item->orderable->name} tidak mencukupi. Tersisa {$currentStock}, diminta {$quantity}. Silakan hubungi Administrator."]; + } + + $newStock = $currentStock - $quantity; + + $outlet->{$relation}()->updateExistingPivot($item->orderable_id, [ + 'stock' => $newStock, + ]); + activity('Order') ->performedOn($item->orderable) ->causedBy(auth()->user())