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/2026081108_m09d2_product_order_payment_inventory.${suffix}.sql`)), `M09-D2 ${suffix} migration is missing` ); } const migration = read( 'database/migrations/2026081108_m09d2_product_order_payment_inventory.up.sql' ); for (const pattern of [ 'qipai_product_orders', 'qipai_product_order_items', 'qipai_product_order_events', 'qipai_product_payments', 'qipai_product_refunds', 'qipai_product_payment_callbacks', 'qipai_product_refund_events', 'PENDING_PAYMENT', 'READY_FOR_SELF_SERVICE', 'COMPENSATION_REQUIRED', 'inventory_request_id', 'request_fingerprint', 'PRODUCT_ORDER_ITEM_IMMUTABLE', 'PRODUCT_ORDER_EVENT_IMMUTABLE', 'PRODUCT_REFUND_EVENT_IMMUTABLE', 'goods.order.read', 'goods.order.manage' ]) assert.ok(migration.includes(pattern), `M09-D2 migration is missing ${pattern}`); assert.doesNotMatch(migration, /\bDECIMAL\b/i); assert.match(migration, /subtotal_cents = unit_price_cents \* quantity/); assert.match(migration, /fulfillment_mode = 'DELIVERY' AND room_order_id IS NOT NULL/); assert.match(migration, /fulfillment_mode = 'SELF_SERVICE' AND room_order_id IS NULL/); const service = read('backend/src/products/product-order-service.ts'); for (const pattern of [ 'ProductOrderService', 'createPaymentForCustomer', 'confirmPayment', 'confirmRefund', 'expirePendingOrders', 'resolveProductOrderTransition', 'lockMany', 'deductMany', 'releaseMany', 'returnMany', 'FOR UPDATE', 'PRODUCT_ORDER_IDEMPOTENCY_CONFLICT', 'PRODUCT_PAYMENT_CALLBACK_CONFLICT', 'PRODUCT_REFUND_CALLBACK_CONFLICT' ]) assert.ok(service.includes(pattern), `product order service is missing ${pattern}`); assert.match(service, /beginTransaction\(\)[\s\S]*commit\(\)[\s\S]*rollback\(\)/); assert.match(service, /order\.expiresAt <= this\.now\(\)/); const routes = read('backend/src/routes/product-orders.ts'); for (const pattern of [ '/app-api/product-orders', '/admin-api', '/app-api/management', 'goods.order.read', 'goods.order.manage', 'testAdapterEnabled', 'completeTestRefundForManagement' ]) assert.ok(routes.includes(pattern), `product order routes are missing ${pattern}`); const app = read('backend/src/app.ts'); const server = read('backend/src/server.ts'); assert.match(app, /registerProductOrderRoutes/); assert.match(app, /productOrders\?: ProductOrderRouteOptions/); assert.match(server, /new ProductOrderService\(pool, inventoryService\)/); assert.match(server, /testAdapterEnabled: config\.payment\.testAdapterEnabled/); const rbac = read('backend/src/auth/rbac-repository.ts'); assert.match(rbac, /'goods\.order\.read', 'goods\.order\.manage'/); const packageJson = read('backend/package.json'); assert.match(packageJson, /product-order-service\.test\.mjs/); assert.match(packageJson, /product-order-route\.test\.mjs/); const mysqlTest = read('backend/tests/mysql-migration-roundtrip.test.mjs'); for (const pattern of [ 'assertProductOrderPaymentInventory', 'concurrent payment callbacks must deduct inventory once', 'PRODUCT_ORDER_ITEM_IMMUTABLE', 'PRODUCT_ORDER_EVENT_IMMUTABLE', 'PRODUCT_REFUND_EVENT_IMMUTABLE', 'expirePendingOrders' ]) assert.ok(mysqlTest.includes(pattern), `live MySQL test is missing ${pattern}`); console.log( 'PASS: M09-D2 product order, payment callback, inventory compensation and immutable-event gates are present.' );