Files
qipai/scripts/check-m09-d1-product-inventory.mjs
T

90 lines
3.2 KiB
JavaScript

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.');