feat(M09-D2): 完成商品订单与库存占用生命周期
This commit is contained in:
@@ -54,6 +54,9 @@ import {
|
||||
import {
|
||||
InventoryError, InventoryService
|
||||
} from '../dist/inventory/inventory-service.js';
|
||||
import {
|
||||
ProductOrderError, ProductOrderService
|
||||
} from '../dist/products/product-order-service.js';
|
||||
import {
|
||||
executeMigrationPlan,
|
||||
loadMigrationPlan,
|
||||
@@ -107,6 +110,13 @@ const expectedTables = [
|
||||
'qipai_product_inventory',
|
||||
'qipai_product_inventory_ledger',
|
||||
'qipai_product_inventory_requests',
|
||||
'qipai_product_order_events',
|
||||
'qipai_product_order_items',
|
||||
'qipai_product_orders',
|
||||
'qipai_product_payment_callbacks',
|
||||
'qipai_product_payments',
|
||||
'qipai_product_refund_events',
|
||||
'qipai_product_refunds',
|
||||
'qipai_product_skus',
|
||||
'qipai_product_store_hours',
|
||||
'qipai_product_store_listings',
|
||||
@@ -160,11 +170,28 @@ async function assertProductInventoryMigrationRetry(pool, fullUpPlan) {
|
||||
repoRoot,
|
||||
'database/migrations/2026081107_m09d1_product_inventory_foundation'
|
||||
);
|
||||
const [upSql, downSql] = await Promise.all([
|
||||
const productOrderMigrationBase = resolve(
|
||||
repoRoot,
|
||||
'database/migrations/2026081108_m09d2_product_order_payment_inventory'
|
||||
);
|
||||
const [upSql, downSql, productOrderDownSql] = await Promise.all([
|
||||
readFile(`${migrationBase}.up.sql`, 'utf8'),
|
||||
readFile(`${migrationBase}.down.sql`, 'utf8')
|
||||
readFile(`${migrationBase}.down.sql`, 'utf8'),
|
||||
readFile(`${productOrderMigrationBase}.down.sql`, 'utf8')
|
||||
]);
|
||||
const upStatements = splitSqlStatements(upSql);
|
||||
let productOrderDownAttempt = 0;
|
||||
const removeProductOrderDependents = async () => {
|
||||
productOrderDownAttempt += 1;
|
||||
await executeMigrationPlan(pool, {
|
||||
direction: 'down',
|
||||
file: `${productOrderMigrationBase}.retry-${productOrderDownAttempt}.down.sql`,
|
||||
checksum: `m09d2-before-m09d1-retry-${productOrderDownAttempt}`,
|
||||
statements: splitSqlStatements(productOrderDownSql)
|
||||
});
|
||||
};
|
||||
|
||||
await removeProductOrderDependents();
|
||||
await executeMigrationPlan(pool, {
|
||||
direction: 'down',
|
||||
file: `${migrationBase}.down.sql`,
|
||||
@@ -175,6 +202,7 @@ async function assertProductInventoryMigrationRetry(pool, fullUpPlan) {
|
||||
await executeMigrationPlan(pool, fullUpPlan);
|
||||
await executeMigrationPlan(pool, fullUpPlan);
|
||||
|
||||
await removeProductOrderDependents();
|
||||
const interruptedDownStatements = splitSqlStatements(downSql).slice(0, -1);
|
||||
await executeMigrationPlan(pool, {
|
||||
direction: 'down',
|
||||
@@ -236,14 +264,14 @@ async function readMigrationVersions(pool) {
|
||||
const [rows] = await pool.query(
|
||||
`SELECT version, name
|
||||
FROM qipai_schema_migrations
|
||||
WHERE version IN (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
WHERE version IN (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ORDER BY version`,
|
||||
['2026061601', '2026061802', '2026061803', '2026061804',
|
||||
'2026061805', '2026061806', '2026061807', '2026061808', '2026061809',
|
||||
'2026061810', '2026061811', '2026062012', '2026062013', '2026062014',
|
||||
'2026062015', '2026062216', '2026062217', '2026062218', '2026062219',
|
||||
'2026062220', '2026081002', '2026081003', '2026081004', '2026081005',
|
||||
'2026081006', '2026081107']
|
||||
'2026081006', '2026081107', '2026081108']
|
||||
);
|
||||
return rows;
|
||||
}
|
||||
@@ -1931,7 +1959,7 @@ async function assertSystemOperations(pool, context) {
|
||||
assert.equal(logs.items[0].metadata.nested.safe, 'visible');
|
||||
const overview = await repository.getSystemOverview(context.tenantId);
|
||||
assert.equal(overview.tenant.id, context.tenantId);
|
||||
assert.equal(overview.latestMigration.version, '2026081107');
|
||||
assert.equal(overview.latestMigration.version, '2026081108');
|
||||
assert.ok(overview.counts.userCount > 0);
|
||||
await repository.updateTenant(actor, context.tenantId, {
|
||||
name: overview.tenant.name, timezone: overview.tenant.timezone
|
||||
@@ -3997,6 +4025,292 @@ async function assertProductInventoryFoundation(pool, context) {
|
||||
);
|
||||
}
|
||||
|
||||
async function assertProductOrderPaymentInventory(pool, context) {
|
||||
const [adminRows] = await pool.query(
|
||||
`SELECT u.id FROM qipai_users u
|
||||
INNER JOIN qipai_user_roles ur
|
||||
ON ur.tenant_id = u.tenant_id AND ur.user_id = u.id
|
||||
INNER JOIN qipai_roles r
|
||||
ON r.tenant_id = ur.tenant_id AND r.id = ur.role_id
|
||||
WHERE u.tenant_id = ? AND r.code = 'TENANT_ADMIN'
|
||||
AND u.deleted_at IS NULL ORDER BY u.id LIMIT 1`,
|
||||
[context.tenantId]
|
||||
);
|
||||
const [customerRows] = await pool.query(
|
||||
`SELECT u.id FROM qipai_users u
|
||||
INNER JOIN qipai_user_roles ur
|
||||
ON ur.tenant_id = u.tenant_id AND ur.user_id = u.id
|
||||
INNER JOIN qipai_roles r
|
||||
ON r.tenant_id = ur.tenant_id AND r.id = ur.role_id
|
||||
WHERE u.tenant_id = ? AND r.code = 'CUSTOMER'
|
||||
AND u.deleted_at IS NULL ORDER BY u.id LIMIT 1`,
|
||||
[context.tenantId]
|
||||
);
|
||||
const [storeRows] = await pool.query(
|
||||
`SELECT id FROM qipai_stores
|
||||
WHERE tenant_id = ? AND name = 'M03A Store' AND deleted_at IS NULL
|
||||
ORDER BY id LIMIT 1`,
|
||||
[context.tenantId]
|
||||
);
|
||||
const [categoryRows] = await pool.query(
|
||||
`SELECT id FROM qipai_product_categories
|
||||
WHERE tenant_id = ? AND store_id = ? AND category_code = 'M09D1-DRINKS'
|
||||
AND status = 'ACTIVE' AND deleted_at IS NULL ORDER BY id LIMIT 1`,
|
||||
[context.tenantId, storeRows[0]?.id]
|
||||
);
|
||||
assert.ok(adminRows[0] && customerRows[0] && storeRows[0] && categoryRows[0],
|
||||
'M09-D2 requires the M09-D1 tenant, customer, store and category fixtures.');
|
||||
const adminId = String(adminRows[0].id);
|
||||
const customerId = String(customerRows[0].id);
|
||||
const storeId = String(storeRows[0].id);
|
||||
const categoryId = String(categoryRows[0].id);
|
||||
const rbac = new RbacRepository(pool);
|
||||
const adminAccess = await rbac.getAccessProfile(context.tenantId, adminId);
|
||||
for (const capability of ['goods.order.read', 'goods.order.manage']) {
|
||||
assert.ok(adminAccess.capabilities.includes(capability),
|
||||
`missing M09-D2 capability ${capability}`);
|
||||
}
|
||||
const adminActor = {
|
||||
tenantId: context.tenantId, userId: adminId, access: adminAccess,
|
||||
source: 'MANAGEMENT', traceId: 'm09d2-live-admin', ip: '127.0.0.1',
|
||||
userAgent: 'M09-D2 live MySQL admin test'
|
||||
};
|
||||
const customerActor = {
|
||||
tenantId: context.tenantId, userId: customerId,
|
||||
access: await rbac.getAccessProfile(context.tenantId, customerId),
|
||||
source: 'CUSTOMER', traceId: 'm09d2-live-customer', ip: '127.0.0.1',
|
||||
userAgent: 'M09-D2 live MySQL customer test'
|
||||
};
|
||||
const catalog = new ProductCatalogRepository(pool);
|
||||
const inventory = new InventoryService(pool);
|
||||
const service = new ProductOrderService(pool, inventory, {
|
||||
now: () => new Date('2026-08-10T15:00:00.000Z'),
|
||||
paymentHoldMinutes: 15
|
||||
});
|
||||
|
||||
const product = await catalog.createProduct(adminActor, {
|
||||
productCode: 'M09D2-ORDER-DRINK', name: 'M09D2 Order Drink', unitName: 'bottle',
|
||||
description: 'M09-D2 transactional product', coverUrl: '', images: [],
|
||||
deliveryEnabled: true, storageEnabled: true, status: 'ACTIVE', sortOrder: 2
|
||||
});
|
||||
const sku = await catalog.createSku(adminActor, product.productId, {
|
||||
skuCode: 'M09D2-ORDER-DRINK-500', name: '500ml', attributes: { volume: '500ml' },
|
||||
barcode: 'M09D2000001', imageUrl: '', salePriceCents: 700,
|
||||
marketPriceCents: 900, costPriceCents: 300,
|
||||
defaultInventoryPolicy: 'TRACKED', status: 'ACTIVE'
|
||||
});
|
||||
await catalog.putListing(adminActor, storeId, product.productId, 0, {
|
||||
categoryId, status: 'ACTIVE', fulfillmentMode: 'BOTH',
|
||||
salesStartAt: null, salesEndAt: null, sortOrder: 2
|
||||
});
|
||||
const configured = await inventory.configurePolicy(adminActor, {
|
||||
storeId, skuId: sku.skuId, requestId: 'm09d2-policy', reason: '订单测试库存策略',
|
||||
expectedVersion: 0, policyType: 'TRACKED', lowStockThreshold: 2
|
||||
});
|
||||
await inventory.inbound(adminActor, {
|
||||
storeId, skuId: sku.skuId, requestId: 'm09d2-inbound', reason: '订单测试入库',
|
||||
expectedVersion: configured.version, quantity: 20
|
||||
});
|
||||
|
||||
const firstInput = {
|
||||
storeId, requestId: 'm09d2-order-first', fulfillmentMode: 'SELF_SERVICE',
|
||||
roomOrderId: null, note: 'first order',
|
||||
items: [{ skuId: sku.skuId, quantity: 2, note: 'snapshot note' }]
|
||||
};
|
||||
const firstOrder = await service.create(customerActor, firstInput);
|
||||
assert.equal(firstOrder.status, 'PENDING_PAYMENT');
|
||||
assert.equal(firstOrder.totalAmountCents, 1400);
|
||||
assert.equal(firstOrder.items[0].productName, 'M09D2 Order Drink');
|
||||
assert.equal(firstOrder.items[0].unitPriceCents, 700);
|
||||
assert.equal((await service.create(customerActor, firstInput)).idempotent, true);
|
||||
await assert.rejects(
|
||||
() => service.create(customerActor, {
|
||||
...firstInput, items: [{ skuId: sku.skuId, quantity: 3, note: 'changed' }]
|
||||
}),
|
||||
(error) => error instanceof ProductOrderError
|
||||
&& error.code === 'PRODUCT_ORDER_IDEMPOTENCY_CONFLICT'
|
||||
);
|
||||
await assert.rejects(
|
||||
() => service.create(customerActor, {
|
||||
...firstInput, requestId: 'm09d2-invalid-room-order', fulfillmentMode: 'DELIVERY',
|
||||
roomOrderId: '999999999999999999'
|
||||
}),
|
||||
(error) => error instanceof ProductOrderError
|
||||
&& error.code === 'PRODUCT_ORDER_ROOM_ORDER_INVALID'
|
||||
);
|
||||
let [stockRows] = await pool.query(
|
||||
`SELECT available_quantity AS availableQuantity, locked_quantity AS lockedQuantity
|
||||
FROM qipai_product_inventory
|
||||
WHERE tenant_id = ? AND store_id = ? AND sku_id = ?`,
|
||||
[context.tenantId, storeId, sku.skuId]
|
||||
);
|
||||
assert.deepEqual({
|
||||
availableQuantity: Number(stockRows[0].availableQuantity),
|
||||
lockedQuantity: Number(stockRows[0].lockedQuantity)
|
||||
}, { availableQuantity: 18, lockedQuantity: 2 });
|
||||
|
||||
const firstPayment = await service.createPaymentForCustomer(
|
||||
customerActor, firstOrder.id, { requestId: 'm09d2-payment-first', provider: 'TEST' }
|
||||
);
|
||||
assert.equal((await service.createPaymentForCustomer(
|
||||
customerActor, firstOrder.id, { requestId: 'm09d2-payment-first', provider: 'TEST' }
|
||||
)).idempotent, true);
|
||||
await assert.rejects(
|
||||
() => service.createPaymentForCustomer(customerActor, firstOrder.id, {
|
||||
requestId: 'm09d2-payment-first', provider: 'WECHAT'
|
||||
}),
|
||||
(error) => error instanceof ProductOrderError
|
||||
&& error.code === 'PRODUCT_PAYMENT_IDEMPOTENCY_CONFLICT'
|
||||
);
|
||||
const paymentRace = await Promise.all([
|
||||
service.completeTestPaymentForCustomer(customerActor, firstPayment.id, {
|
||||
callbackId: 'm09d2-payment-first-callback-a', amountCents: 1400
|
||||
}),
|
||||
service.completeTestPaymentForCustomer(customerActor, firstPayment.id, {
|
||||
callbackId: 'm09d2-payment-first-callback-b', amountCents: 1400
|
||||
})
|
||||
]);
|
||||
assert.equal(paymentRace.filter((result) => result.idempotent === false).length, 1);
|
||||
assert.equal(paymentRace.filter((result) => result.idempotent === true).length, 1);
|
||||
[stockRows] = await pool.query(
|
||||
`SELECT available_quantity AS availableQuantity, locked_quantity AS lockedQuantity
|
||||
FROM qipai_product_inventory
|
||||
WHERE tenant_id = ? AND store_id = ? AND sku_id = ?`,
|
||||
[context.tenantId, storeId, sku.skuId]
|
||||
);
|
||||
assert.deepEqual({
|
||||
availableQuantity: Number(stockRows[0].availableQuantity),
|
||||
lockedQuantity: Number(stockRows[0].lockedQuantity)
|
||||
}, { availableQuantity: 18, lockedQuantity: 0 });
|
||||
const [deductRows] = await pool.query(
|
||||
`SELECT COUNT(*) AS total FROM qipai_product_inventory_ledger
|
||||
WHERE tenant_id = ? AND business_type = 'PRODUCT_ORDER'
|
||||
AND business_id = ? AND operation = 'DEDUCT'`,
|
||||
[context.tenantId, `PRODUCT_ORDER:${firstOrder.orderNo}`]
|
||||
);
|
||||
assert.equal(Number(deductRows[0].total), 1,
|
||||
'concurrent payment callbacks must deduct inventory once');
|
||||
|
||||
await service.managementAction(adminActor, firstOrder.id, {
|
||||
requestId: 'm09d2-first-accept', action: 'ACCEPT', reason: ''
|
||||
});
|
||||
await service.managementAction(adminActor, firstOrder.id, {
|
||||
requestId: 'm09d2-first-ready', action: 'MARK_READY', reason: ''
|
||||
});
|
||||
const completed = await service.managementAction(adminActor, firstOrder.id, {
|
||||
requestId: 'm09d2-first-complete', action: 'COMPLETE', reason: ''
|
||||
});
|
||||
assert.equal(completed.status, 'COMPLETED');
|
||||
const completedRefunding = await service.managementAction(adminActor, firstOrder.id, {
|
||||
requestId: 'm09d2-first-refund', action: 'REFUND', reason: '完成后退款不回库'
|
||||
});
|
||||
assert.equal(completedRefunding.refunds[0].inventoryDisposition, 'NO_RESTOCK');
|
||||
await service.completeTestRefundForManagement(
|
||||
adminActor, String(completedRefunding.refunds[0].id), {
|
||||
callbackId: 'm09d2-first-refund-callback', amountCents: 1400
|
||||
}
|
||||
);
|
||||
|
||||
const restockOrder = await service.create(customerActor, {
|
||||
...firstInput, requestId: 'm09d2-order-restock', note: 'restock order',
|
||||
items: [{ skuId: sku.skuId, quantity: 3, note: '' }]
|
||||
});
|
||||
const restockPayment = await service.createPaymentForCustomer(
|
||||
customerActor, restockOrder.id,
|
||||
{ requestId: 'm09d2-payment-restock', provider: 'TEST' }
|
||||
);
|
||||
await service.completeTestPaymentForCustomer(customerActor, restockPayment.id, {
|
||||
callbackId: 'm09d2-payment-restock-callback', amountCents: 2100
|
||||
});
|
||||
const restockRefunding = await service.managementAction(adminActor, restockOrder.id, {
|
||||
requestId: 'm09d2-restock-refund', action: 'REFUND', reason: '支付后取消回库'
|
||||
});
|
||||
const restockRefundId = String(restockRefunding.refunds[0].id);
|
||||
assert.equal(restockRefunding.refunds[0].inventoryDisposition, 'RESTOCK');
|
||||
const restocked = await service.completeTestRefundForManagement(
|
||||
adminActor, restockRefundId,
|
||||
{ callbackId: 'm09d2-restock-refund-callback', amountCents: 2100 }
|
||||
);
|
||||
assert.equal(restocked.status, 'SUCCEEDED');
|
||||
assert.equal((await service.completeTestRefundForManagement(
|
||||
adminActor, restockRefundId,
|
||||
{ callbackId: 'm09d2-restock-refund-callback', amountCents: 2100 }
|
||||
)).idempotent, true);
|
||||
|
||||
const cancelOrder = await service.create(customerActor, {
|
||||
...firstInput, requestId: 'm09d2-order-cancel', note: 'cancel order',
|
||||
items: [{ skuId: sku.skuId, quantity: 1, note: '' }]
|
||||
});
|
||||
const cancelled = await service.cancelForCustomer(customerActor, cancelOrder.id, {
|
||||
requestId: 'm09d2-order-cancel-action', reason: '顾客取消待支付订单'
|
||||
});
|
||||
assert.equal(cancelled.status, 'CANCELLED');
|
||||
assert.equal(cancelled.inventoryStatus, 'RELEASED');
|
||||
assert.equal((await service.cancelForCustomer(customerActor, cancelOrder.id, {
|
||||
requestId: 'm09d2-order-cancel-action', reason: '顾客取消待支付订单'
|
||||
})).status, 'CANCELLED');
|
||||
|
||||
const timeoutOrder = await service.create(customerActor, {
|
||||
...firstInput, requestId: 'm09d2-order-timeout', note: 'timeout order',
|
||||
items: [{ skuId: sku.skuId, quantity: 1, note: '' }]
|
||||
});
|
||||
await pool.query(
|
||||
`UPDATE qipai_product_orders SET expires_at = UTC_TIMESTAMP(3) - INTERVAL 1 SECOND
|
||||
WHERE tenant_id = ? AND id = ?`, [context.tenantId, timeoutOrder.id]
|
||||
);
|
||||
assert.ok((await service.expirePendingOrders()).expired >= 1);
|
||||
assert.equal((await service.getForCustomer(customerActor, timeoutOrder.id)).status, 'CANCELLED');
|
||||
|
||||
[stockRows] = await pool.query(
|
||||
`SELECT available_quantity AS availableQuantity, locked_quantity AS lockedQuantity
|
||||
FROM qipai_product_inventory
|
||||
WHERE tenant_id = ? AND store_id = ? AND sku_id = ?`,
|
||||
[context.tenantId, storeId, sku.skuId]
|
||||
);
|
||||
assert.deepEqual({
|
||||
availableQuantity: Number(stockRows[0].availableQuantity),
|
||||
lockedQuantity: Number(stockRows[0].lockedQuantity)
|
||||
}, { availableQuantity: 18, lockedQuantity: 0 },
|
||||
'completed consumption stays deducted while paid cancellation and pending cancellation restore stock');
|
||||
|
||||
await assert.rejects(
|
||||
() => service.listForManagement({
|
||||
...adminActor,
|
||||
access: { roles: ['STAFF'], capabilities: ['goods.order.read'], storeIds: [] }
|
||||
}, { storeId, page: 1, pageSize: 20 }),
|
||||
(error) => error instanceof ProductOrderError
|
||||
&& error.code === 'PRODUCT_ORDER_STORE_SCOPE_FORBIDDEN'
|
||||
);
|
||||
await assert.rejects(
|
||||
() => pool.query(
|
||||
`UPDATE qipai_product_order_items SET item_note = 'forbidden'
|
||||
WHERE tenant_id = ? AND order_id = ? LIMIT 1`,
|
||||
[context.tenantId, firstOrder.id]
|
||||
),
|
||||
(error) => /PRODUCT_ORDER_ITEM_IMMUTABLE/.test(error?.message ?? '')
|
||||
);
|
||||
await assert.rejects(
|
||||
() => pool.query(
|
||||
`DELETE FROM qipai_product_order_events
|
||||
WHERE tenant_id = ? AND order_id = ? LIMIT 1`,
|
||||
[context.tenantId, firstOrder.id]
|
||||
),
|
||||
(error) => /PRODUCT_ORDER_EVENT_IMMUTABLE/.test(error?.message ?? '')
|
||||
);
|
||||
await assert.rejects(
|
||||
() => pool.query(
|
||||
`UPDATE qipai_product_refund_events SET reason = 'forbidden'
|
||||
WHERE tenant_id = ? AND refund_id = ? LIMIT 1`,
|
||||
[context.tenantId, restockRefundId]
|
||||
),
|
||||
(error) => /PRODUCT_REFUND_EVENT_IMMUTABLE/.test(error?.message ?? '')
|
||||
);
|
||||
console.log(
|
||||
'PASS: M09-D2 order snapshots, concurrent payment deduct, cancellation release, '
|
||||
+ 'refund restock, timeout cleanup and immutable events are consistent.'
|
||||
);
|
||||
}
|
||||
|
||||
const config = loadConfig();
|
||||
assert.equal(config.mysql.passwordConfigured, true, 'Live migration test requires a temporary password.');
|
||||
assert.match(
|
||||
@@ -4049,7 +4363,8 @@ try {
|
||||
{ version: '2026081004', name: 'm08d_admin_password_auth' },
|
||||
{ version: '2026081005', name: 'm09b_cleaning_rules' },
|
||||
{ version: '2026081006', name: 'm09c_cleaning_settlement_integrity' },
|
||||
{ version: '2026081107', name: 'm09d1_product_inventory_foundation' }
|
||||
{ version: '2026081107', name: 'm09d1_product_inventory_foundation' },
|
||||
{ version: '2026081108', name: 'm09d2_product_order_payment_inventory' }
|
||||
]);
|
||||
await assertTaskDurability(pool);
|
||||
const loginContext = await assertPlatformTenantIsolation(pool);
|
||||
@@ -4073,13 +4388,14 @@ try {
|
||||
await assertIotMessages(pool, loginContext);
|
||||
await assertCleaningTaskTransactions(pool, loginContext);
|
||||
await assertProductInventoryFoundation(pool, loginContext);
|
||||
await assertProductOrderPaymentInventory(pool, loginContext);
|
||||
await assertLegacyCompatibility(pool);
|
||||
console.log('PASS: first up, verify, tenant isolation and revocable auth checks completed.');
|
||||
|
||||
await executeMigrationPlan(pool, plans.down);
|
||||
assert.deepEqual(await readCoreTables(pool), []);
|
||||
await assertLegacyCompatibility(pool);
|
||||
console.log('PASS: down removed all M01-B through M09-D1 migration tables.');
|
||||
console.log('PASS: down removed all M01-B through M09-D2 migration tables.');
|
||||
|
||||
await executeMigrationPlan(pool, plans.up);
|
||||
await executeMigrationPlan(pool, plans.verify);
|
||||
@@ -4110,7 +4426,8 @@ try {
|
||||
{ version: '2026081004', name: 'm08d_admin_password_auth' },
|
||||
{ version: '2026081005', name: 'm09b_cleaning_rules' },
|
||||
{ version: '2026081006', name: 'm09c_cleaning_settlement_integrity' },
|
||||
{ version: '2026081107', name: 'm09d1_product_inventory_foundation' }
|
||||
{ version: '2026081107', name: 'm09d1_product_inventory_foundation' },
|
||||
{ version: '2026081108', name: 'm09d2_product_order_payment_inventory' }
|
||||
]);
|
||||
await assertLegacyCompatibility(pool);
|
||||
console.log('PASS: second up and verify restored the schema.');
|
||||
|
||||
Reference in New Issue
Block a user