seed(RolePermissionSeeder::class); User::factory()->create(); $this->seed(CashAccountSeeder::class); Storage::fake('public'); }); // ─── Helper ─────────────────────────────────────────────── function createOrderUserWithPermission(PermissionEnum ...$permissions): User { $user = User::factory()->create(); $user->givePermissionTo( array_merge( [PermissionEnum::DASHBOARD_VIEW->value], array_map(fn (PermissionEnum $p) => $p->value, $permissions) ) ); $user->forgetCachedPermissions(); return $user; } function createOrderWithItems(?User $user = null): Order { $user ??= User::factory()->create(); $order = Order::factory()->create([ 'created_by_id' => $user->id, 'channel' => OrderChannel::STORE->value, 'price_type' => PriceType::RETAIL->value, 'payment_type' => PaymentType::CASH->value, ]); $product = Product::factory()->create(); $variant = ProductVariant::factory()->create([ 'product_id' => $product->id, 'stock' => 100, ]); OrderItem::factory()->create([ 'order_id' => $order->id, 'product_variant_id' => $variant->id, 'quantity' => 5, 'unit_price' => 50000, 'subtotal' => 250000, ]); return $order; } function setupOrderDraftItems(User $user): ProductVariant { $product = Product::factory()->create(); $variant = ProductVariant::factory()->create([ 'product_id' => $product->id, 'stock' => 100, ]); // Create product price so CuttingResultPriceResolver can resolve it ProductPrice::factory()->create([ 'variant_id' => $variant->id, 'type' => PriceType::RETAIL, 'price' => 50000, ]); // Create draft item via API test()->actingAs($user) ->postJson(route('admin.manage.orders.draft_items.store'), [ 'product_variant_id' => $variant->id, 'price_type' => PriceType::RETAIL->value, 'stock_quality' => 'good', 'quantity' => 5, ]) ->assertOk(); return $variant; } // ─── Index ──────────────────────────────────────────────── describe('Order Index', function () { test('authenticated user with permission can view order index', function () { $user = createOrderUserWithPermission(PermissionEnum::ORDERS_VIEW); $this->actingAs($user) ->get(route('admin.manage.orders.index')) ->assertOk(); }); test('guest is redirected to login', function () { $this->get(route('admin.manage.orders.index')) ->assertRedirect(route('login')); }); test('user without permission is forbidden', function () { $user = User::factory()->create(); $this->actingAs($user) ->get(route('admin.manage.orders.index')) ->assertForbidden(); }); test('index displays orders', function () { $user = createOrderUserWithPermission(PermissionEnum::ORDERS_VIEW); createOrderWithItems($user); createOrderWithItems($user); $this->actingAs($user) ->get(route('admin.manage.orders.index')) ->assertOk(); }); test('index can search orders by order number', function () { $user = createOrderUserWithPermission(PermissionEnum::ORDERS_VIEW); $order = Order::factory()->create([ 'created_by_id' => $user->id, 'order_number' => 'ORD-20260704-TEST', ]); $this->actingAs($user) ->get(route('admin.manage.orders.index', ['search' => 'TEST'])) ->assertOk(); }); }); // ─── Create ─────────────────────────────────────────────── describe('Order Create', function () { test('authenticated user with permission can view create form', function () { $user = createOrderUserWithPermission(PermissionEnum::ORDERS_VIEW, PermissionEnum::ORDERS_CREATE); $this->actingAs($user) ->get(route('admin.manage.orders.create')) ->assertOk(); }); test('guest is redirected to login', function () { $this->get(route('admin.manage.orders.create')) ->assertRedirect(route('login')); }); test('user without create permission is forbidden', function () { $user = createOrderUserWithPermission(PermissionEnum::ORDERS_VIEW); $this->actingAs($user) ->get(route('admin.manage.orders.create')) ->assertForbidden(); }); }); // ─── Store ──────────────────────────────────────────────── describe('Order Store', function () { test('authenticated user with permission can create an order', function () { $user = createOrderUserWithPermission(PermissionEnum::ORDERS_VIEW, PermissionEnum::ORDERS_CREATE); $customer = Customer::factory()->create(); setupOrderDraftItems($user); $this->actingAs($user) ->post(route('admin.manage.orders.store'), [ 'customer_id' => $customer->id, 'channel' => OrderChannel::STORE->value, 'price_type' => PriceType::RETAIL->value, 'payment_type' => PaymentType::CASH->value, 'discount' => 0, ]) ->assertRedirect(route('admin.manage.orders.index')); $order = Order::query()->latest()->first(); expect($order)->not->toBeNull(); expect($order->status)->toBe(OrderStatus::PENDING); expect($order->items)->toHaveCount(1); }); test('guest cannot create an order', function () { $this->post(route('admin.manage.orders.store'), [ 'channel' => OrderChannel::STORE->value, 'price_type' => PriceType::RETAIL->value, 'payment_type' => PaymentType::CASH->value, ])->assertRedirect(route('login')); }); test('user without create permission is forbidden', function () { $user = createOrderUserWithPermission(PermissionEnum::ORDERS_VIEW); $this->actingAs($user) ->post(route('admin.manage.orders.store'), [ 'channel' => OrderChannel::STORE->value, 'price_type' => PriceType::RETAIL->value, 'payment_type' => PaymentType::CASH->value, ]) ->assertForbidden(); }); test('store fails without draft items', function () { $user = createOrderUserWithPermission(PermissionEnum::ORDERS_VIEW, PermissionEnum::ORDERS_CREATE); $this->actingAs($user) ->post(route('admin.manage.orders.store'), [ 'channel' => OrderChannel::STORE->value, 'price_type' => PriceType::RETAIL->value, 'payment_type' => PaymentType::CASH->value, 'discount' => 0, ]) ->assertSessionHasErrors('items'); }); test('creating order decrements stock', function () { $user = createOrderUserWithPermission(PermissionEnum::ORDERS_VIEW, PermissionEnum::ORDERS_CREATE); $variant = setupOrderDraftItems($user); $initialStock = $variant->stock; $this->actingAs($user) ->post(route('admin.manage.orders.store'), [ 'channel' => OrderChannel::STORE->value, 'price_type' => PriceType::RETAIL->value, 'payment_type' => PaymentType::CASH->value, 'discount' => 0, ]); expect($variant->fresh()->stock)->toBe($initialStock - 5); }); test('cashier creates order automatically sets status to completed and unsets marketing and customer', function () { $user = User::factory()->create(); $user->assignRole('cashier'); $user->givePermissionTo(PermissionEnum::ORDERS_CREATE->value); $user->forgetCachedPermissions(); setupOrderDraftItems($user); $marketing = User::factory()->create(); $customer = Customer::factory()->create(); $this->actingAs($user) ->post(route('admin.manage.orders.store'), [ 'customer_id' => $customer->id, 'marketing_id' => $marketing->id, 'channel' => OrderChannel::STORE->value, 'price_type' => PriceType::RETAIL->value, 'payment_type' => PaymentType::CASH->value, 'status' => OrderStatus::PENDING->value, 'discount' => 0, ]) ->assertRedirect(route('admin.manage.orders.index')); $order = Order::query()->latest()->first(); expect($order)->not->toBeNull(); expect($order->status)->toBe(OrderStatus::COMPLETED); expect($order->marketing_id)->toBeNull(); expect($order->customer_id)->toBeNull(); }); }); // ─── Show ───────────────────────────────────────────────── describe('Order Show', function () { test('authenticated user with permission can view order details', function () { $user = createOrderUserWithPermission(PermissionEnum::ORDERS_VIEW); $order = createOrderWithItems($user); $this->actingAs($user) ->get(route('admin.manage.orders.show', $order)) ->assertOk(); }); test('guest is redirected to login', function () { $order = createOrderWithItems(); $this->get(route('admin.manage.orders.show', $order)) ->assertRedirect(route('login')); }); }); // ─── Edit ───────────────────────────────────────────────── describe('Order Edit', function () { test('authenticated user with permission can view edit form', function () { $user = createOrderUserWithPermission(PermissionEnum::ORDERS_VIEW, PermissionEnum::ORDERS_UPDATE); $order = createOrderWithItems($user); $this->actingAs($user) ->get(route('admin.manage.orders.edit', $order)) ->assertOk(); }); test('guest is redirected to login', function () { $order = createOrderWithItems(); $this->get(route('admin.manage.orders.edit', $order)) ->assertRedirect(route('login')); }); test('user without update permission is forbidden', function () { $user = createOrderUserWithPermission(PermissionEnum::ORDERS_VIEW); $order = createOrderWithItems($user); $this->actingAs($user) ->get(route('admin.manage.orders.edit', $order)) ->assertForbidden(); }); test('completed order cannot be edited', function () { $user = createOrderUserWithPermission(PermissionEnum::ORDERS_VIEW, PermissionEnum::ORDERS_UPDATE); $order = Order::factory()->completed()->create(['created_by_id' => $user->id]); $this->actingAs($user) ->get(route('admin.manage.orders.edit', $order)) ->assertRedirect(route('admin.manage.orders.index')); }); }); // ─── Destroy ────────────────────────────────────────────── describe('Order Destroy', function () { test('authenticated user with permission can delete an order', function () { $user = createOrderUserWithPermission(PermissionEnum::ORDERS_VIEW, PermissionEnum::ORDERS_DELETE); $order = createOrderWithItems($user); $this->actingAs($user) ->delete(route('admin.manage.orders.destroy', $order)) ->assertRedirect(route('admin.manage.orders.index')); $this->assertSoftDeleted('orders', ['id' => $order->id]); }); test('guest cannot delete an order', function () { $order = createOrderWithItems(); $this->delete(route('admin.manage.orders.destroy', $order)) ->assertRedirect(route('login')); $this->assertNotSoftDeleted('orders', ['id' => $order->id]); }); test('user without delete permission is forbidden', function () { $user = createOrderUserWithPermission(PermissionEnum::ORDERS_VIEW); $order = createOrderWithItems($user); $this->actingAs($user) ->delete(route('admin.manage.orders.destroy', $order)) ->assertForbidden(); $this->assertNotSoftDeleted('orders', ['id' => $order->id]); }); test('deleting order increments stock back', function () { $user = createOrderUserWithPermission(PermissionEnum::ORDERS_VIEW, PermissionEnum::ORDERS_DELETE); $order = createOrderWithItems($user); $item = $order->items->first(); $variant = $item->productVariant; $stockBeforeDelete = $variant->fresh()->stock; $this->actingAs($user) ->delete(route('admin.manage.orders.destroy', $order)); expect($variant->fresh()->stock)->toBe($stockBeforeDelete + $item->quantity); }); }); // ─── Status Transition ──────────────────────────────────── describe('Order Status Transition', function () { test('authenticated user can send an order', function () { $user = createOrderUserWithPermission(PermissionEnum::ORDERS_VIEW, PermissionEnum::ORDERS_SEND); $order = Order::factory()->create([ 'created_by_id' => $user->id, 'status' => OrderStatus::PENDING->value, ]); $this->actingAs($user) ->post(route('admin.manage.orders.transition_status', $order), [ 'status' => OrderStatus::PROCESSING->value, ]) ->assertRedirect(route('admin.manage.orders.index')); expect($order->fresh()->status)->toBe(OrderStatus::PROCESSING); }); test('authenticated user can complete an order', function () { $user = createOrderUserWithPermission(PermissionEnum::ORDERS_VIEW, PermissionEnum::ORDERS_COMPLETE); $order = Order::factory()->create([ 'created_by_id' => $user->id, 'status' => OrderStatus::PROCESSING->value, ]); $this->actingAs($user) ->post(route('admin.manage.orders.transition_status', $order), [ 'status' => OrderStatus::COMPLETED->value, ]); expect($order->fresh()->status)->toBe(OrderStatus::COMPLETED); }); test('authenticated user can cancel an order', function () { $user = createOrderUserWithPermission(PermissionEnum::ORDERS_VIEW, PermissionEnum::ORDERS_CANCEL); $order = Order::factory()->create([ 'created_by_id' => $user->id, 'status' => OrderStatus::PENDING->value, ]); $this->actingAs($user) ->post(route('admin.manage.orders.transition_status', $order), [ 'status' => OrderStatus::CANCELLED->value, ]); expect($order->fresh()->status)->toBe(OrderStatus::CANCELLED); }); test('guest cannot transition status', function () { $order = Order::factory()->create(['status' => OrderStatus::PENDING->value]); $this->post(route('admin.manage.orders.transition_status', $order), [ 'status' => OrderStatus::PROCESSING->value, ])->assertRedirect(route('login')); }); test('invalid status transition is rejected', function () { $user = createOrderUserWithPermission(PermissionEnum::ORDERS_VIEW, PermissionEnum::ORDERS_COMPLETE); $order = Order::factory()->create([ 'created_by_id' => $user->id, 'status' => OrderStatus::PENDING->value, ]); // PENDING cannot go directly to COMPLETED $this->actingAs($user) ->post(route('admin.manage.orders.transition_status', $order), [ 'status' => OrderStatus::COMPLETED->value, ]) ->assertSessionHasErrors('status'); }); test('cancelling order increments stock back', function () { $user = createOrderUserWithPermission(PermissionEnum::ORDERS_VIEW, PermissionEnum::ORDERS_CANCEL); $order = createOrderWithItems($user); $item = $order->items->first(); $variant = $item->productVariant; $stockBeforeCancel = $variant->fresh()->stock; $this->actingAs($user) ->post(route('admin.manage.orders.transition_status', $order), [ 'status' => OrderStatus::CANCELLED->value, ]); expect($variant->fresh()->stock)->toBe($stockBeforeCancel + $item->quantity); }); }); // ─── Order Model ────────────────────────────────────────── describe('Order Model', function () { test('order uses soft deletes', function () { $order = Order::factory()->create(); $order->delete(); expect($order->trashed())->toBeTrue(); }); test('order has amounts cast to integer', function () { $order = Order::factory()->create([ 'subtotal' => 100000, 'discount' => 5000, 'total_amount' => 95000, ]); expect($order->subtotal)->toBeInt(); expect($order->discount)->toBeInt(); expect($order->total_amount)->toBeInt(); }); test('order has formatted amount accessors', function () { $order = Order::factory()->create([ 'subtotal' => 100000, 'discount' => 5000, 'total_amount' => 95000, ]); expect($order->subtotal_formatted)->toBe('Rp 100.000'); expect($order->discount_formatted)->toBe('Rp 5.000'); expect($order->total_amount_formatted)->toBe('Rp 95.000'); }); test('order has status cast to enum', function () { $order = Order::factory()->create(['status' => OrderStatus::PENDING->value]); expect($order->status)->toBe(OrderStatus::PENDING); }); test('order has status label accessor', function () { $order = Order::factory()->create(['status' => OrderStatus::PENDING->value]); expect($order->status_label)->toBe('Menunggu'); }); test('order belongs to customer', function () { $customer = Customer::factory()->create(); $order = Order::factory()->create(['customer_id' => $customer->id]); expect($order->customer)->not->toBeNull(); expect($order->customer->id)->toBe($customer->id); }); test('order belongs to created by user', function () { $user = User::factory()->create(); $order = Order::factory()->create(['created_by_id' => $user->id]); expect($order->createdBy)->not->toBeNull(); expect($order->createdBy->id)->toBe($user->id); }); test('order can have items', function () { $order = Order::factory()->create(); OrderItem::factory()->count(3)->create(['order_id' => $order->id]); expect($order->fresh()->items)->toHaveCount(3); }); test('order status is editable for pending and processing', function () { $pending = Order::factory()->create(['status' => OrderStatus::PENDING->value]); $processing = Order::factory()->create(['status' => OrderStatus::PROCESSING->value]); expect($pending->status->isEditable())->toBeTrue(); expect($processing->status->isEditable())->toBeTrue(); }); }); // ─── OrderItem Model ────────────────────────────────────── describe('OrderItem Model', function () { test('order item has amounts cast to integer', function () { $order = Order::factory()->create(); $item = OrderItem::factory()->create([ 'order_id' => $order->id, 'quantity' => 10, 'unit_price' => 50000, 'subtotal' => 500000, ]); expect($item->quantity)->toBeInt(); expect($item->unit_price)->toBeInt(); expect($item->subtotal)->toBeInt(); }); test('order item has formatted amount accessors', function () { $order = Order::factory()->create(); $item = OrderItem::factory()->create([ 'order_id' => $order->id, 'quantity' => 10, 'unit_price' => 50000, 'subtotal' => 500000, ]); expect($item->quantity_formatted)->toBe('10 pcs'); expect($item->unit_price_formatted)->toBe('Rp 50.000'); expect($item->subtotal_formatted)->toBe('Rp 500.000'); }); test('order item belongs to order', function () { $order = Order::factory()->create(); $item = OrderItem::factory()->create(['order_id' => $order->id]); expect($item->order)->not->toBeNull(); expect($item->order->id)->toBe($order->id); }); test('order item uses soft deletes', function () { $order = Order::factory()->create(); $item = OrderItem::factory()->create(['order_id' => $order->id]); $item->delete(); expect($item->trashed())->toBeTrue(); }); }); // ─── Order Catalog Stock Filter ───────────────────────────── describe('Order Catalog Stock Filter', function () { test('catalogItems excludes variants where all stock types are zero', function () { $user = createOrderUserWithPermission(PermissionEnum::ORDERS_VIEW, PermissionEnum::ORDERS_CREATE); $product = Product::factory()->create(['status' => ProductStatus::ACTIVE]); $variantWithStock = ProductVariant::factory()->create(['product_id' => $product->id, 'stock' => 10, 'reject_stock' => 0, 'retail_stock' => 0]); $variantAllZero = ProductVariant::factory()->create(['product_id' => $product->id, 'stock' => 0, 'reject_stock' => 0, 'retail_stock' => 0]); $variantRejectOnly = ProductVariant::factory()->create(['product_id' => $product->id, 'stock' => 0, 'reject_stock' => 5, 'retail_stock' => 0]); $variantRetailOnly = ProductVariant::factory()->create(['product_id' => $product->id, 'stock' => 0, 'reject_stock' => 0, 'retail_stock' => 3]); $service = app(OrderService::class); $catalog = $service->catalogItems(user: $user); $found = $catalog->firstWhere('id', $product->id); $variantIds = $found?->variants->pluck('id')->all() ?? []; expect($variantIds)->toContain($variantWithStock->id); expect($variantIds)->toContain($variantRejectOnly->id); expect($variantIds)->toContain($variantRetailOnly->id); expect($variantIds)->not->toContain($variantAllZero->id); }); test('catalogItems includes all-zero-stock variants already in order', function () { $user = createOrderUserWithPermission(PermissionEnum::ORDERS_VIEW, PermissionEnum::ORDERS_UPDATE); $product = Product::factory()->create(['status' => ProductStatus::ACTIVE]); $variantAllZero = ProductVariant::factory()->create([ 'product_id' => $product->id, 'stock' => 0, 'reject_stock' => 0, 'retail_stock' => 0, ]); $order = Order::factory()->create(['created_by_id' => $user->id]); OrderItem::factory()->create([ 'order_id' => $order->id, 'product_variant_id' => $variantAllZero->id, ]); $service = app(OrderService::class); $catalog = $service->catalogItems($order); $found = $catalog->firstWhere('id', $product->id); $variantIds = $found?->variants->pluck('id')->all() ?? []; expect($variantIds)->toContain($variantAllZero->id); }); });