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/2026081109_m09d3_product_storage.${suffix}.sql`)), `M09-D3 ${suffix} migration is missing` ); } const migration = read('database/migrations/2026081109_m09d3_product_storage.up.sql'); for (const pattern of [ 'qipai_product_storage_records', 'qipai_product_storage_items', 'qipai_product_storage_events', 'qipai_product_storage_event_items', 'PARTIALLY_RETRIEVED', 'claim_credential_hash', 'remaining_quantity', 'uq_qipai_product_storage_source_order', 'PRODUCT_STORAGE_EVENT_IMMUTABLE', 'PRODUCT_STORAGE_EVENT_ITEM_IMMUTABLE', 'goods.storage.read', 'goods.storage.manage' ]) assert.ok(migration.includes(pattern), `M09-D3 migration is missing ${pattern}`); const service = read('backend/src/products/product-storage-service.ts'); for (const pattern of [ 'ProductStorageService', 'createFromOrder', 'createManual', 'rotateCredential', 'expireDueForManagement', 'timingSafeEqual', 'FOR UPDATE', 'FOR UPDATE SKIP LOCKED', 'qipai_product_storage_event_items', 'qipai_outbox_events', 'qipai_audit_logs', 'PRODUCT_STORAGE_SOURCE_ORDER_ALREADY_STORED' ]) assert.ok(service.includes(pattern), `product storage service is missing ${pattern}`); assert.match(service, /createHash\('sha256'\)/); assert.match(service, /beginTransaction\(\)[\s\S]*commit\(\)[\s\S]*rollback\(\)/); assert.match(service, /claimCredential: null/); const routes = read('backend/src/routes/product-storages.ts'); for (const pattern of [ '/app-api/product-storages', '/admin-api', '/app-api/management', '/credential/rotate', '/expire-due', 'goods.storage.read', 'goods.storage.manage' ]) assert.ok(routes.includes(pattern), `product storage routes are missing ${pattern}`); const app = read('backend/src/app.ts'); const server = read('backend/src/server.ts'); assert.match(app, /registerProductStorageRoutes/); assert.match(app, /productStorages\?: ProductStorageRouteOptions/); assert.match(server, /new ProductStorageService\(pool\)/); const rbac = read('backend/src/auth/rbac-repository.ts'); assert.match(rbac, /'goods\.storage\.read', 'goods\.storage\.manage'/); const packageJson = read('backend/package.json'); assert.match(packageJson, /product-storage-service\.test\.mjs/); assert.match(packageJson, /product-storage-route\.test\.mjs/); const mysqlTest = read('backend/tests/mysql-migration-roundtrip.test.mjs'); for (const pattern of [ 'assertProductStorageLifecycle', 'only one concurrent full retrieval may succeed', 'PRODUCT_STORAGE_CREDENTIAL_INVALID', 'PRODUCT_STORAGE_EVENT_IMMUTABLE', 'PRODUCT_STORAGE_EVENT_ITEM_IMMUTABLE', 'expireDueForManagement' ]) assert.ok(mysqlTest.includes(pattern), `live MySQL test is missing ${pattern}`); console.log( 'PASS: M09-D3 storage ownership, one-time credential, partial/concurrent retrieval and immutable-ledger gates are present.' );