import assert from 'node:assert/strict'; import { existsSync, readFileSync } from 'node:fs'; import { join } from 'node:path'; const root = process.cwd(); const read = (path) => readFileSync(join(root, path), 'utf8'); for (const suffix of ['up', 'down', 'verify']) { assert.ok( existsSync(join(root, `database/migrations/2026081107_m09d1_product_inventory_foundation.${suffix}.sql`)), `M09-D1 ${suffix} migration is missing` ); } const migration = read('database/migrations/2026081107_m09d1_product_inventory_foundation.up.sql'); for (const pattern of [ 'qipai_product_categories', 'qipai_products', 'qipai_product_skus', 'qipai_product_store_listings', 'qipai_product_store_hours', 'qipai_product_inventory', 'qipai_product_inventory_requests', 'qipai_product_inventory_ledger', 'delivery_enabled', 'storage_enabled', 'crosses_midnight', 'manual_paused_at', 'loss_quantity', 'loss_delta', 'uq_qipai_product_inventory_request', 'ADD COLUMN checksum', 'uq_qipai_product_inventory_ledger_request', 'uq_qipai_product_inventory_ledger_version', 'PRODUCT_INVENTORY_LEDGER_IMMUTABLE', 'product.catalog.read', 'inventory.adjust' ]) assert.ok(migration.includes(pattern), `migration is missing ${pattern}`); assert.match(migration, /sale_price_cents INT UNSIGNED/); assert.match(migration, /available_quantity BIGINT UNSIGNED/); assert.match(migration, /operation IN \([\s\S]*'INBOUND'[\s\S]*'STOCKTAKE'[\s\S]*'LOSS'/); const catalog = read('backend/src/products/product-catalog-repository.ts'); for (const pattern of [ 'ProductCatalogRepository', 'expectedVersion', 'PRODUCT_CATEGORY_VERSION_CONFLICT', 'PRODUCT_VERSION_CONFLICT', 'PRODUCT_SKU_VERSION_CONFLICT', 'PRODUCT_LISTING_VERSION_CONFLICT', 'PRODUCT_STORE_SETTINGS_VERSION_CONFLICT', 'crossesMidnight', 'manualPaused' ]) assert.ok(catalog.includes(pattern), `product catalog is missing ${pattern}`); assert.match(catalog, /INSERT INTO qipai_audit_logs/); const inventory = read('backend/src/inventory/inventory-service.ts'); for (const pattern of [ 'InventoryService', 'lockMany', 'releaseMany', 'deductMany', 'INVENTORY_INSUFFICIENT_AVAILABLE', 'INVENTORY_INSUFFICIENT_LOCKED', 'INVENTORY_IDEMPOTENCY_CONFLICT', 'FOR UPDATE', 'qipai_product_inventory_ledger', 'qipai_audit_logs' ]) assert.ok(inventory.includes(pattern), `inventory service is missing ${pattern}`); const productRoutes = read('backend/src/routes/products.ts'); const inventoryRoutes = read('backend/src/routes/inventory.ts'); assert.match(productRoutes, /product-categories/); assert.match(productRoutes, /\/admin-api\/products/); assert.match(productRoutes, /product-sales-settings/); assert.match(inventoryRoutes, /inventory\/stocks/); assert.match(inventoryRoutes, /\/adjust/); const mysqlTest = read('backend/tests/mysql-migration-roundtrip.test.mjs'); for (const pattern of [ 'assertProductInventoryFoundation', 'INVENTORY_INSUFFICIENT_AVAILABLE', 'PRODUCT_INVENTORY_LEDGER_IMMUTABLE', 'concurrent inventory lock', 'cross-midnight product hours' ]) assert.ok(mysqlTest.includes(pattern), `live MySQL test is missing ${pattern}`); console.log('PASS: M09-D1 catalog, store-hours, stock concurrency and immutable-ledger gates are present.');