feat: 實作 POS API 整合功能,包含商品與銷售訂單同步及韌性機制
This commit is contained in:
202
tests/Feature/Integration/PosApiTest.php
Normal file
202
tests/Feature/Integration/PosApiTest.php
Normal file
@@ -0,0 +1,202 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Integration;
|
||||
|
||||
use Tests\TestCase;
|
||||
use App\Modules\Core\Models\Tenant;
|
||||
use App\Modules\Core\Models\User;
|
||||
use App\Modules\Inventory\Models\Product;
|
||||
use App\Modules\Integration\Models\SalesOrder;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Stancl\Tenancy\Facades\Tenancy;
|
||||
|
||||
class PosApiTest extends TestCase
|
||||
{
|
||||
// Use RefreshDatabase to reset DB state.
|
||||
// Note: In Tenancy, this usually resets Central DB.
|
||||
// For Tenant DBs, we heavily rely on Stancl's behavior or need to act carefully.
|
||||
// For now, assume we can create a tenant and its DB will be migrated or accessible.
|
||||
|
||||
protected $tenant;
|
||||
protected $user;
|
||||
protected $token;
|
||||
protected $domain;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
\Artisan::call('migrate:fresh'); // Force fresh DB for isolation without RefreshDatabase trait
|
||||
|
||||
$this->domain = 'test-' . \Illuminate\Support\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, ['*']);
|
||||
|
||||
$payload = [
|
||||
'external_pos_id' => 'EXT-NEW-002',
|
||||
'name' => 'New Product',
|
||||
'price' => 200,
|
||||
'sku' => 'SKU-NEW',
|
||||
];
|
||||
|
||||
$response = $this->withHeaders([
|
||||
'X-Tenant-Domain' => $this->domain,
|
||||
'Accept' => 'application/json',
|
||||
])->postJson('/api/v1/integration/products/upsert', $payload);
|
||||
|
||||
$response->assertStatus(200)
|
||||
->assertJsonPath('message', 'Product synced successfully');
|
||||
|
||||
// Verify in Tenant DB
|
||||
tenancy()->initialize($this->tenant);
|
||||
$this->assertDatabaseHas('products', [
|
||||
'external_pos_id' => 'EXT-NEW-002',
|
||||
'name' => 'New Product',
|
||||
]);
|
||||
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,
|
||||
];
|
||||
|
||||
$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' => 'BATCH-TEST-001',
|
||||
'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_id' => $warehouseId,
|
||||
'sold_at' => now()->toIso8601String(),
|
||||
'items' => [
|
||||
[
|
||||
'pos_product_id' => 'EXT-001',
|
||||
'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');
|
||||
|
||||
// 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,
|
||||
]);
|
||||
|
||||
tenancy()->end();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user