domain = 'test-' . Str::random(8) . '.erp.local'; $tenantId = 'test_tenant_' . \Illuminate\Support\Str::random(8); // Ensure we are in central context tenancy()->central(function () use ($tenantId) { // Create a tenant $this->tenant = Tenant::create([ 'id' => $tenantId, 'name' => 'Test Tenant', ]); $this->tenant->domains()->create(['domain' => $this->domain]); }); // Initialize to create User and Token tenancy()->initialize($this->tenant); Artisan::call('tenants:migrate'); $this->user = User::factory()->create([ 'email' => 'admin@test.local', 'name' => 'Admin', ]); $this->token = $this->user->createToken('POS-Test-Token')->plainTextToken; $category = \App\Modules\Inventory\Models\Category::create(['name' => 'General', 'code' => 'GEN']); // Create a product for testing Product::create([ 'name' => 'Existing Product', 'code' => 'P-001', 'external_pos_id' => 'EXT-001', 'sku' => 'SKU-001', 'price' => 100, 'is_active' => true, 'category_id' => $category->id, ]); // End tenancy initialization to simulate external request tenancy()->end(); } protected function tearDown(): void { if ($this->tenant) { $this->tenant->delete(); } parent::tearDown(); } public function test_upsert_product_creates_new_product() { \Laravel\Sanctum\Sanctum::actingAs($this->user, ['*']); $productData = [ 'external_pos_id' => 'POS-P-999', 'name' => '新款可口可樂', 'price' => 29.0, 'barcode' => '471000999', 'category' => '飲品', 'unit' => '瓶', ]; $response = $this->withHeaders([ 'X-Tenant-Domain' => $this->domain, 'Accept' => 'application/json', ])->postJson('/api/v1/integration/products/upsert', $productData); $response->assertStatus(200) ->assertJsonPath('data.external_pos_id', 'POS-P-999') ->assertJsonStructure([ 'data' => [ 'id', 'external_pos_id', 'code', 'barcode' ] ]); // Verify in Tenant DB tenancy()->initialize($this->tenant); $this->assertDatabaseHas('products', [ 'external_pos_id' => 'POS-P-999', 'name' => '新款可口可樂', ]); tenancy()->end(); } public function test_upsert_product_updates_existing_product() { \Laravel\Sanctum\Sanctum::actingAs($this->user, ['*']); $payload = [ 'external_pos_id' => 'EXT-001', 'name' => 'Updated Name', 'price' => 150, 'category' => '飲品', 'unit' => '瓶', ]; $response = $this->withHeaders([ 'X-Tenant-Domain' => $this->domain, 'Accept' => 'application/json', ])->postJson('/api/v1/integration/products/upsert', $payload); $response->assertStatus(200); tenancy()->initialize($this->tenant); $this->assertDatabaseHas('products', [ 'external_pos_id' => 'EXT-001', 'name' => 'Updated Name', 'price' => 150, ]); tenancy()->end(); } public function test_create_order_deducts_inventory() { // Setup inventory first tenancy()->initialize($this->tenant); $product = Product::where('external_pos_id', 'EXT-001')->first(); // We need a warehouse $warehouse = \App\Modules\Inventory\Models\Warehouse::create(['name' => 'Main Warehouse', 'code' => 'MAIN']); // Add initial stock \App\Modules\Inventory\Models\Inventory::create([ 'product_id' => $product->id, 'warehouse_id' => $warehouse->id, 'quantity' => 100, 'batch_number' => 'NO-BATCH', // 改為系統預設值 'arrival_date' => now()->toDateString(), 'origin_country' => 'TW', ]); $warehouseId = $warehouse->id; tenancy()->end(); \Laravel\Sanctum\Sanctum::actingAs($this->user, ['*']); $payload = [ 'external_order_id' => 'ORD-001', 'warehouse_code' => 'MAIN', 'sold_at' => now()->toIso8601String(), 'items' => [ [ 'product_id' => $product->id, 'qty' => 5, 'price' => 100 ] ] ]; $response = $this->withHeaders([ 'X-Tenant-Domain' => $this->domain, 'Accept' => 'application/json', ])->postJson('/api/v1/integration/orders', $payload); $response->assertStatus(201) ->assertJsonPath('message', 'Order synced and stock deducted successfully'); $response->assertStatus(201) ->assertJsonPath('message', 'Order synced and stock deducted successfully'); // Verify Order and Inventory tenancy()->initialize($this->tenant); $this->assertDatabaseHas('sales_orders', [ 'external_order_id' => 'ORD-001', ]); $this->assertDatabaseHas('sales_order_items', [ 'product_id' => $product->id, // We need to fetch ID again or rely on correct ID 'quantity' => 5, ]); // Verify stock deducted: 100 - 5 = 95 $this->assertDatabaseHas('inventories', [ 'product_id' => $product->id, 'warehouse_id' => $warehouseId, 'quantity' => 95, ]); $order = \App\Modules\Integration\Models\SalesOrder::where('external_order_id', 'ORD-001')->first(); $this->assertDatabaseHas('inventory_transactions', [ 'reference_type' => \App\Modules\Integration\Models\SalesOrder::class, 'reference_id' => $order->id, 'quantity' => -5, 'type' => '出庫', ]); } public function test_order_creation_with_batch_number_deduction() { tenancy()->initialize($this->tenant); $product = Product::where('code', 'P-001')->first(); $warehouse = \App\Modules\Inventory\Models\Warehouse::firstOrCreate(['code' => 'MAIN'], ['name' => 'Main Warehouse']); // Scenario: Two records for the same product, one with batch, one without \App\Modules\Inventory\Models\Inventory::create([ 'product_id' => $product->id, 'warehouse_id' => $warehouse->id, 'quantity' => 10, 'batch_number' => 'BATCH-A', 'arrival_date' => now()->toDateString(), ]); \App\Modules\Inventory\Models\Inventory::create([ 'product_id' => $product->id, 'warehouse_id' => $warehouse->id, 'quantity' => 5, 'batch_number' => 'NO-BATCH', 'arrival_date' => now()->toDateString(), ]); tenancy()->end(); \Laravel\Sanctum\Sanctum::actingAs($this->user, ['*']); // 1. Test deducting from SPECIFIC batch $payloadA = [ 'external_order_id' => 'ORD-BATCH-A', 'warehouse_code' => 'MAIN', 'items' => [ [ 'product_id' => $product->id, 'batch_number' => 'BATCH-A', 'qty' => 3, 'price' => 100 ] ] ]; $this->withHeaders(['X-Tenant-Domain' => $this->domain]) ->postJson('/api/v1/integration/orders', $payloadA) ->assertStatus(201); // 2. Test deducting from NULL batch (default) $payloadNull = [ 'external_order_id' => 'ORD-BATCH-NULL', 'warehouse_code' => 'MAIN', 'items' => [ [ 'product_id' => $product->id, 'batch_number' => null, // 測試不傳入批號時應自動轉向 NO-BATCH 'qty' => 2, 'price' => 100 ] ] ]; $this->withHeaders(['X-Tenant-Domain' => $this->domain]) ->postJson('/api/v1/integration/orders', $payloadNull) ->assertStatus(201); tenancy()->initialize($this->tenant); // Verify BATCH-A: 10 - 3 = 7 $this->assertDatabaseHas('inventories', [ 'product_id' => $product->id, 'batch_number' => 'BATCH-A', 'quantity' => 7, ]); // Verify NO-BATCH batch: 5 - 2 = 3 $this->assertDatabaseHas('inventories', [ 'product_id' => $product->id, 'batch_number' => 'NO-BATCH', 'quantity' => 3, ]); tenancy()->end(); } public function test_upsert_auto_generates_code_and_barcode_if_missing() { \Laravel\Sanctum\Sanctum::actingAs($this->user, ['*']); $productData = [ 'external_pos_id' => 'POS-AUTO-GEN-01', 'name' => '自動編號商品', 'price' => 50.0, 'category' => '測試分類', 'unit' => '個', ]; $response = $this->withHeaders([ 'X-Tenant-Domain' => $this->domain, 'Accept' => 'application/json', ])->postJson('/api/v1/integration/products/upsert', $productData); $response->assertStatus(200); $data = $response->json('data'); $this->assertNotEmpty($data['code']); $this->assertNotEmpty($data['barcode']); $this->assertEquals(8, strlen($data['code'])); $this->assertEquals(13, strlen($data['barcode'])); // Ensure they are stored in DB tenancy()->initialize($this->tenant); $this->assertDatabaseHas('products', [ 'external_pos_id' => 'POS-AUTO-GEN-01', 'code' => $data['code'], 'barcode' => $data['barcode'], ]); tenancy()->end(); } public function test_inventory_query_returns_detailed_info() { tenancy()->initialize($this->tenant); // 1. 建立具有完整資訊的商品 $category = \App\Modules\Inventory\Models\Category::create(['name' => '測試大類']); $unit = \App\Modules\Inventory\Models\Unit::create(['name' => '測試單位']); $product = Product::create([ 'external_pos_id' => 'DETAIL-001', 'code' => 'DET-001', 'barcode' => '1234567890123', 'name' => '詳盡商品', 'category_id' => $category->id, 'base_unit_id' => $unit->id, 'price' => 123.45, 'brand' => '品牌A', 'specification' => '規格B', 'is_active' => true, ]); $warehouse = \App\Modules\Inventory\Models\Warehouse::create(['name' => '詳盡倉庫', 'code' => 'DETAIL-WH']); // 2. 增加庫存 \App\Modules\Inventory\Models\Inventory::create([ 'product_id' => $product->id, 'warehouse_id' => $warehouse->id, 'quantity' => 10.5, 'batch_number' => 'BATCH-DETAIL-01', 'arrival_date' => now(), 'expiry_date' => now()->addYear(), 'origin_country' => 'TW', ]); tenancy()->end(); \Laravel\Sanctum\Sanctum::actingAs($this->user, ['*']); // 3. 查詢該倉庫庫存 $response = $this->withHeaders([ 'X-Tenant-Domain' => $this->domain, 'Accept' => 'application/json', ])->getJson('/api/v1/integration/inventory/DETAIL-WH'); $response->assertStatus(200) ->assertJsonStructure([ 'status', 'warehouse_code', 'data' => [ '*' => [ 'product_id', 'external_pos_id', 'product_code', 'product_name', 'barcode', 'category_name', 'unit_name', 'price', 'brand', 'specification', 'batch_number', 'expiry_date', 'quantity' ] ] ]); $data = $response->json('data.0'); $this->assertEquals($product->id, $data['product_id']); $this->assertEquals('DETAIL-001', $data['external_pos_id']); $this->assertEquals('DET-001', $data['product_code']); $this->assertEquals('1234567890123', $data['barcode']); $this->assertEquals('測試大類', $data['category_name']); $this->assertEquals('測試單位', $data['unit_name']); $this->assertEquals(123.45, (float)$data['price']); $this->assertEquals('品牌A', $data['brand']); $this->assertEquals('規格B', $data['specification']); $this->assertEquals('BATCH-DETAIL-01', $data['batch_number']); $this->assertNotEmpty($data['expiry_date']); $this->assertEquals(10.5, $data['quantity']); } public function test_order_creation_with_insufficient_stock_creates_negative_no_batch() { tenancy()->initialize($this->tenant); $product = Product::where('code', 'P-001')->first(); $warehouse = \App\Modules\Inventory\Models\Warehouse::where('code', 'MAIN')->first(); // 清空該商品的現有庫存以利測試負數 \App\Modules\Inventory\Models\Inventory::where('product_id', $product->id)->delete(); tenancy()->end(); \Laravel\Sanctum\Sanctum::actingAs($this->user, ['*']); $payload = [ 'external_order_id' => 'ORD-NEGATIVE-TEST', 'warehouse_code' => 'MAIN', 'items' => [ [ 'product_id' => $product->id, 'batch_number' => 'ANY-BATCH-NAME', 'qty' => 10, 'price' => 100 ] ] ]; $this->withHeaders(['X-Tenant-Domain' => $this->domain]) ->postJson('/api/v1/integration/orders', $payload) ->assertStatus(201); tenancy()->initialize($this->tenant); // 驗證應該在 NO-BATCH 產生 -10 的庫存 $this->assertDatabaseHas('inventories', [ 'product_id' => $product->id, 'warehouse_id' => $warehouse->id, 'batch_number' => 'NO-BATCH', 'quantity' => -10, ]); tenancy()->end(); } }