diff --git a/app/Http/Controllers/Admin/Manage/OrderController.php b/app/Http/Controllers/Admin/Manage/OrderController.php index b30d6ca..420d221 100644 --- a/app/Http/Controllers/Admin/Manage/OrderController.php +++ b/app/Http/Controllers/Admin/Manage/OrderController.php @@ -36,7 +36,7 @@ public function create(): Response ->whereNull('order_id') ->latest() ->get(), - 'orderStatus' => OrderStatus::options(), + 'orderStatus' => collect(OrderStatus::options())->filter(fn ($opt) => $opt['value'] !== OrderStatus::CANCELLED->value)->values()->toArray(), 'orderChannels' => OrderChannel::options(), 'paymentMethods' => PaymentMethod::options(), 'priceTypes' => PriceType::options(), @@ -136,21 +136,51 @@ public function update(OrderRequest $request, Order $order): RedirectResponse try { DB::transaction(function () use ($validated, $order) { - $totalItemsPrice = collect($validated['items'])->sum(fn ($item) => $item['qty'] * $item['price']); - $cogs = collect($validated['items'])->sum(function ($item) { + $isCancelled = $validated['order_status'] === OrderStatus::CANCELLED->value; + $wasCancelled = ($order->order_status instanceof OrderStatus ? $order->order_status->value : $order->order_status) === OrderStatus::CANCELLED->value; + + // 1. Jika status SEKARANG adalah GAGAL + if ($isCancelled) { + // Kembalikan stok jika sebelumnya TIDAK gagal + if (! $wasCancelled) { + foreach ($order->items as $item) { + $item->product->increment('stock', $item->qty); + } + } + + // Update metadata saja, jangan hapus item (sesuai request) + $order->update([ + 'customer_name' => $validated['customer_name'], + 'discount' => $validated['discount'], + 'payment' => $validated['payment'], + 'payment_method' => $validated['payment_method'], + 'order_status' => $validated['order_status'], + 'order_channel' => $validated['order_channel'], + ]); + + return; + } + + // 2. Jika status SEKARANG TIDAK GAGAL + // Kembalikan stok lama jika sebelumnya TIDAK gagal (karena kita akan mengganti item) + if (! $wasCancelled) { + foreach ($order->items as $item) { + $item->product->increment('stock', $item->qty); + } + } + + // Proses update item (hapus lama, buat baru) + $order->items()->delete(); + + $items = $validated['items'] ?? []; + $totalItemsPrice = collect($items)->sum(fn ($item) => $item['qty'] * $item['price']); + $cogs = collect($items)->sum(function ($item) { $product = Product::find($item['product_id']); $purchasePrice = $product->prices()->where('price_type', PriceType::PURCHASE)->first()?->price ?? 0; return $purchasePrice * $item['qty']; }); - // Restore stock for old items - foreach ($order->items as $item) { - $item->product->increment('stock', $item->qty); - } - - $order->items()->delete(); - $order->update([ 'customer_name' => $validated['customer_name'], 'cogs' => $cogs, @@ -162,7 +192,7 @@ public function update(OrderRequest $request, Order $order): RedirectResponse 'order_channel' => $validated['order_channel'], ]); - foreach ($validated['items'] as $item) { + foreach ($items as $item) { $order->items()->create([ 'user_id' => auth()->id(), 'product_id' => $item['product_id'], @@ -172,6 +202,7 @@ public function update(OrderRequest $request, Order $order): RedirectResponse 'price_type' => $item['price_type'], ]); + // Potong stok (karena status sekarang bukan gagal) $product = Product::find($item['product_id']); if ($product->stock < $item['qty']) { throw new \InvalidArgumentException("Stok produk {$product->name} tidak mencukupi. Sisa stok: {$product->stock}"); diff --git a/app/Http/Requests/Admin/Manage/Order/OrderRequest.php b/app/Http/Requests/Admin/Manage/Order/OrderRequest.php index 1a40f4d..17a4927 100644 --- a/app/Http/Requests/Admin/Manage/Order/OrderRequest.php +++ b/app/Http/Requests/Admin/Manage/Order/OrderRequest.php @@ -27,20 +27,22 @@ public function authorize(): bool */ public function rules(): array { + $isCancelled = $this->input('order_status') === OrderStatus::CANCELLED->value; + return [ 'customer_name' => ['required', 'string', 'max:100'], - 'subtotal' => ['required', 'integer', 'min:0'], + 'subtotal' => ['nullable', 'integer', 'min:0'], 'discount' => ['required', 'integer', 'min:0'], 'payment' => ['required', 'integer', 'min:0'], 'payment_method' => ['required', Rule::enum(PaymentMethod::class)], 'order_status' => ['required', Rule::enum(OrderStatus::class)], 'order_channel' => ['required', Rule::enum(OrderChannel::class)], - 'items' => ['required', 'array', 'min:1'], - 'items.*.product_id' => ['required', Rule::exists('products', 'id')->whereNull('deleted_at')], - 'items.*.qty' => ['required', 'integer', 'min:1'], - 'items.*.price' => ['required', 'integer', 'min:0'], - 'items.*.total' => ['required', 'integer', 'min:0'], - 'items.*.price_type' => ['required', Rule::enum(PriceType::class)], + 'items' => [$isCancelled ? 'nullable' : 'required', 'array', $isCancelled ? 'min:0' : 'min:1'], + 'items.*.product_id' => ['required_unless:order_status,cancelled', Rule::exists('products', 'id')->whereNull('deleted_at')], + 'items.*.qty' => ['required_unless:order_status,cancelled', 'integer', 'min:1'], + 'items.*.price' => ['required_unless:order_status,cancelled', 'integer', 'min:0'], + 'items.*.total' => ['required_unless:order_status,cancelled', 'integer', 'min:0'], + 'items.*.price_type' => ['required_unless:order_status,cancelled', Rule::enum(PriceType::class)], ]; } } diff --git a/app/Observers/OrderObserver.php b/app/Observers/OrderObserver.php index ea3a1d4..22f83f5 100644 --- a/app/Observers/OrderObserver.php +++ b/app/Observers/OrderObserver.php @@ -2,6 +2,7 @@ namespace App\Observers; +use App\Enums\OrderStatus; use App\Models\Order; class OrderObserver @@ -40,8 +41,14 @@ public function updating(Order $order): void */ public function deleting(Order $order): void { - foreach ($order->items as $item) { - $item->product->increment('stock', $item->qty); + $isCancelled = ($order->order_status instanceof OrderStatus ? $order->order_status->value : $order->order_status) === OrderStatus::CANCELLED->value; + + if (! $isCancelled) { + foreach ($order->items as $item) { + $item->product->increment('stock', $item->qty); + } } + + $order->items()->delete(); } } diff --git a/tests/Feature/Admin/Manage/OrderTest.php b/tests/Feature/Admin/Manage/OrderTest.php index 10c27ab..b09022c 100644 --- a/tests/Feature/Admin/Manage/OrderTest.php +++ b/tests/Feature/Admin/Manage/OrderTest.php @@ -79,6 +79,9 @@ ->component('admin/manage/order/create') ->has('products') ->has('cartItems') + ->where('orderStatus', function ($status) { + return collect($status)->every(fn ($item) => $item['value'] !== OrderStatus::CANCELLED->value); + }) ); }); @@ -337,6 +340,54 @@ expect($product2->fresh()->stock)->toBe(7); }); + it('restores stock and preserves items when order status is changed to gagal', function () { + $product = Product::factory()->create(['stock' => 10]); + $order = Order::factory()->create(['order_status' => OrderStatus::PENDING->value]); + + $item = $order->items()->create([ + 'user_id' => auth()->id(), + 'product_id' => $product->id, + 'qty' => 3, + 'price' => 1000, + 'total' => 3000, + 'price_type' => PriceType::RETAIL->value, + ]); + + $product->decrement('stock', 3); + expect($product->fresh()->stock)->toBe(7); + + $newData = [ + 'customer_name' => $order->customer_name, + 'subtotal' => 3000, + 'discount' => 0, + 'payment' => 3000, + 'payment_method' => $order->payment_method, + 'order_status' => OrderStatus::CANCELLED->value, // Change to Gagal + 'order_channel' => $order->order_channel, + 'items' => [ + // Even if we send items, they shouldn't be deleted/updated based on my early return + [ + 'product_id' => $product->id, + 'qty' => 5, // different qty + 'price' => 1000, + 'total' => 5000, + 'price_type' => PriceType::RETAIL->value, + ], + ], + ]; + + patchJson(route('order.update', $order), $newData) + ->assertRedirect(); + + // Stock should be restored (7 + 3 = 10) + expect($product->fresh()->stock)->toBe(10); + + // Items should be preserved (original item with qty 3, NOT deleted/recreated with qty 5) + expect($order->items()->count())->toBe(1); + expect($order->items()->first()->qty)->toBe(3); + expect($order->items()->first()->id)->toBe($item->id); // Same ID means not deleted/recreated + }); + it('can delete an order and restore stock', function () { $product = Product::factory()->create(['stock' => 5]); $order = Order::factory()->create();