fix(M09-REGRESSION): 完成商品逐单对账与权限收口

This commit is contained in:
Codex
2026-08-11 08:25:12 +08:00
parent e91b979128
commit 41b9cf7349
21 changed files with 566 additions and 39 deletions
+1 -1
View File
@@ -18,7 +18,7 @@
"db:migrate:down": "npm run build && node dist/db/migrate-cli.js down",
"test:mysql:migration": "npm run build && node tests/mysql-migration-roundtrip.test.mjs",
"pretest": "npm run build && node tests/product-storage-service.test.mjs && node tests/product-storage-route.test.mjs",
"test": "npm run build && node tests/backend-contract.test.mjs && node tests/mqtt-service.test.mjs && node tests/migration-contract.test.mjs && node tests/mysql-pool-contract.test.mjs && node tests/migration-runner.test.mjs && node tests/legacy-money.test.mjs && node tests/legacy-read-repository.test.mjs && node tests/task-repository.test.mjs && node tests/platform-config-repository.test.mjs && node tests/auth.test.mjs && node tests/admin-auth.test.mjs && node tests/rbac.test.mjs && node tests/user-management.test.mjs && node tests/store-room.test.mjs && node tests/content-management.test.mjs && node tests/franchise.test.mjs && node tests/system-operations.test.mjs && node tests/store-discovery.test.mjs && node tests/store-access.test.mjs && node tests/pricing.test.mjs && node tests/order-state.test.mjs && node tests/order-query.test.mjs && node tests/order-management.test.mjs && node tests/order-share.test.mjs && node tests/payment.test.mjs && node tests/wechat-pay.test.mjs && node tests/third-party.test.mjs && node tests/profit-sharing.test.mjs && node tests/device.test.mjs && node tests/iot-protocol.test.mjs && node tests/device-control.test.mjs && node tests/order-device-automation.test.mjs && node tests/hardware-smoke-runner.test.mjs && node tests/wallet-ledger.test.mjs && node tests/recharge-service.test.mjs && node tests/recharge-route.test.mjs && node tests/marketing-benefit-service.test.mjs && node tests/member-profile-service.test.mjs && node tests/member-profile-route.test.mjs && node tests/cleaning-payout-service.test.mjs && node tests/cleaning-route.test.mjs && node tests/business-statistics.test.mjs && node tests/product-catalog.test.mjs && node tests/product-route.test.mjs && node tests/inventory-service.test.mjs && node tests/inventory-route.test.mjs && node tests/product-order-service.test.mjs && node tests/product-order-route.test.mjs"
"test": "npm run build && node tests/backend-contract.test.mjs && node tests/mqtt-service.test.mjs && node tests/migration-contract.test.mjs && node tests/mysql-pool-contract.test.mjs && node tests/migration-runner.test.mjs && node tests/legacy-money.test.mjs && node tests/legacy-read-repository.test.mjs && node tests/task-repository.test.mjs && node tests/platform-config-repository.test.mjs && node tests/auth.test.mjs && node tests/admin-auth.test.mjs && node tests/rbac.test.mjs && node tests/user-management.test.mjs && node tests/store-room.test.mjs && node tests/content-management.test.mjs && node tests/franchise.test.mjs && node tests/system-operations.test.mjs && node tests/store-discovery.test.mjs && node tests/store-access.test.mjs && node tests/pricing.test.mjs && node tests/order-state.test.mjs && node tests/order-query.test.mjs && node tests/order-management.test.mjs && node tests/order-share.test.mjs && node tests/payment.test.mjs && node tests/wechat-pay.test.mjs && node tests/third-party.test.mjs && node tests/profit-sharing.test.mjs && node tests/device.test.mjs && node tests/iot-protocol.test.mjs && node tests/device-control.test.mjs && node tests/order-device-automation.test.mjs && node tests/hardware-smoke-runner.test.mjs && node tests/wallet-ledger.test.mjs && node tests/recharge-service.test.mjs && node tests/recharge-route.test.mjs && node tests/marketing-benefit-service.test.mjs && node tests/member-profile-service.test.mjs && node tests/member-profile-route.test.mjs && node tests/cleaning-payout-service.test.mjs && node tests/cleaning-route.test.mjs && node tests/business-statistics.test.mjs && node tests/product-catalog.test.mjs && node tests/product-route.test.mjs && node tests/inventory-service.test.mjs && node tests/inventory-route.test.mjs && node tests/product-order-service.test.mjs && node tests/product-order-route.test.mjs && node tests/product-reconciliation.test.mjs"
},
"dependencies": {
"@fastify/cors": "^11.2.0",
+5 -4
View File
@@ -310,8 +310,8 @@ export class InventoryService {
ON p.tenant_id = s.tenant_id AND p.id = s.product_id AND p.deleted_at IS NULL
WHERE ${where}
ORDER BY p.sort_order, p.id, s.id
LIMIT ? OFFSET ?`,
[...params, pageSize, (page - 1) * pageSize]
LIMIT ${pageSize} OFFSET ${(page - 1) * pageSize}`,
params
);
return {
items: rows.map(mapStock),
@@ -346,8 +346,8 @@ export class InventoryService {
trace_id AS traceId, reason, metadata, created_at AS createdAt
FROM qipai_product_inventory_ledger
WHERE tenant_id = ? AND store_id = ? AND inventory_id = ?
ORDER BY id DESC LIMIT ? OFFSET ?`,
[...params, pageSize, (page - 1) * pageSize]
ORDER BY id DESC LIMIT ${pageSize} OFFSET ${(page - 1) * pageSize}`,
params
);
return {
items: rows.map(mapLedger),
@@ -1013,6 +1013,7 @@ export class InventoryService {
private assertStoreScope(actor: ManagementActor, storeId: string, write: boolean) {
assertId(storeId, 'INVENTORY_STORE_ID_INVALID');
if (actor.access.capabilities.includes('tenant.manage')
|| actor.access.capabilities.includes('platform.manage')
|| actor.access.roles.includes('PLATFORM_ADMIN')) return;
const capability = write ? 'inventory.adjust' : 'inventory.read';
const hasCapability = actor.access.capabilities.includes(capability)
@@ -378,6 +378,54 @@ export class ProductOrderService {
return this.transaction((connection) => this.loadOrderView(connection, actor, orderId, true));
}
async reconcileForManagement(actor: ProductOrderActor, orderId: string) {
assertId(orderId);
return this.transaction(async (connection) => {
const order = await this.loadOrder(connection, actor.tenantId, orderId, false);
this.assertManagementStore(actor, order.storeId);
const detail = await this.loadOrderView(connection, actor, orderId, true);
const [inventoryLedger] = await connection.execute<RowDataPacket[]>(
`SELECT l.id, l.inventory_id AS inventoryId, l.sku_id AS skuId,
p.name AS productName, s.name AS skuName, l.request_id AS requestId,
l.operation, l.available_delta AS availableDelta,
l.locked_delta AS lockedDelta, l.loss_delta AS lossDelta,
CAST(JSON_UNQUOTE(JSON_EXTRACT(l.metadata,
'$.inventoryReservationDelta')) AS SIGNED) AS reservationDelta,
CAST(JSON_UNQUOTE(JSON_EXTRACT(l.metadata,
'$.inventorySaleDelta')) AS SIGNED) AS saleDelta,
l.available_after AS availableAfter, l.locked_after AS lockedAfter,
l.loss_after AS lossAfter, l.version_after AS versionAfter,
l.created_at AS createdAt
FROM qipai_product_inventory_ledger l
INNER JOIN qipai_product_skus s
ON s.tenant_id = l.tenant_id AND s.id = l.sku_id
INNER JOIN qipai_products p
ON p.tenant_id = s.tenant_id AND p.id = s.product_id
WHERE l.tenant_id = ? AND l.store_id = ?
AND l.business_type = 'PRODUCT_ORDER' AND l.business_id = ?
ORDER BY l.created_at, l.id`,
[actor.tenantId, order.storeId, order.inventoryBusinessId]
);
const [storageSummaries] = await connection.execute<RowDataPacket[]>(
`SELECT r.id, r.storage_no AS storageNo, r.status,
r.total_quantity AS recordTotalQuantity,
r.remaining_quantity AS recordRemainingQuantity,
COALESCE(SUM(i.total_quantity), 0) AS itemTotalQuantity,
COALESCE(SUM(i.remaining_quantity), 0) AS itemRemainingQuantity,
COUNT(i.id) AS itemCount
FROM qipai_product_storage_records r
LEFT JOIN qipai_product_storage_items i
ON i.tenant_id = r.tenant_id AND i.store_id = r.store_id
AND i.storage_id = r.id
WHERE r.tenant_id = ? AND r.store_id = ? AND r.source_order_id = ?
GROUP BY r.id, r.storage_no, r.status, r.total_quantity, r.remaining_quantity
ORDER BY r.id`,
[actor.tenantId, order.storeId, order.id]
);
return buildProductOrderReconciliation(detail, inventoryLedger, storageSummaries);
});
}
async managementAction(
actor: ProductOrderActor,
orderId: string,
@@ -1437,6 +1485,106 @@ function publicPayment(row: PaymentRow, idempotent: boolean) {
};
}
export function buildProductOrderReconciliation(
detail: Record<string, any>,
inventoryLedger: Array<Record<string, any>>,
storageSummaries: Array<Record<string, any>>
) {
const items = Array.isArray(detail.items) ? detail.items : [];
const payments = Array.isArray(detail.payments) ? detail.payments : [];
const refunds = Array.isArray(detail.refunds) ? detail.refunds : [];
const itemAmountCents = items.reduce(
(sum: number, item: Record<string, any>) => sum + numeric(item.subtotalCents), 0
);
const itemQuantity = items.reduce(
(sum: number, item: Record<string, any>) => sum + numeric(item.quantity), 0
);
const capturedAmountCents = payments
.filter((payment: Record<string, any>) => ['SUCCEEDED', 'REFUNDED'].includes(payment.status))
.reduce((sum: number, payment: Record<string, any>) => sum + numeric(payment.amountCents), 0);
const paymentRefundedCents = payments.reduce(
(sum: number, payment: Record<string, any>) => sum + numeric(payment.refundedAmountCents), 0
);
const succeededRefundCents = refunds
.filter((refund: Record<string, any>) => refund.status === 'SUCCEEDED')
.reduce((sum: number, refund: Record<string, any>) => sum + numeric(refund.amountCents), 0);
const reservationBalance = inventoryLedger.reduce(
(sum, entry) => sum + numeric(entry.reservationDelta), 0
);
const saleBalance = inventoryLedger.reduce(
(sum, entry) => sum + numeric(entry.saleDelta), 0
);
const expectedInventory = detail.inventoryStatus === 'LOCKED'
? { reservationBalance: numeric(detail.totalQuantity), saleBalance: 0 }
: detail.inventoryStatus === 'DEDUCTED'
? { reservationBalance: 0, saleBalance: numeric(detail.totalQuantity) }
: { reservationBalance: 0, saleBalance: 0 };
const storages = storageSummaries.map((storage) => ({
id: String(storage.id), storageNo: storage.storageNo, status: storage.status,
recordTotalQuantity: numeric(storage.recordTotalQuantity),
recordRemainingQuantity: numeric(storage.recordRemainingQuantity),
itemTotalQuantity: numeric(storage.itemTotalQuantity),
itemRemainingQuantity: numeric(storage.itemRemainingQuantity),
itemCount: numeric(storage.itemCount),
consistent: numeric(storage.recordTotalQuantity) === numeric(storage.itemTotalQuantity)
&& numeric(storage.recordRemainingQuantity) === numeric(storage.itemRemainingQuantity)
&& numeric(storage.itemRemainingQuantity) >= 0
&& numeric(storage.itemRemainingQuantity) <= numeric(storage.itemTotalQuantity)
}));
const checks = [
reconciliationCheck('ORDER_ITEM_AMOUNT', numeric(detail.totalAmountCents), itemAmountCents),
reconciliationCheck('ORDER_ITEM_QUANTITY', numeric(detail.totalQuantity), itemQuantity),
reconciliationCheck('ORDER_ITEM_COUNT', numeric(detail.itemCount), items.length),
reconciliationCheck('PAYMENT_CAPTURED_AMOUNT', numeric(detail.paidAmountCents), capturedAmountCents),
reconciliationCheck('PAYMENT_REFUNDED_AMOUNT', numeric(detail.refundedAmountCents), paymentRefundedCents),
reconciliationCheck('REFUND_SUCCEEDED_AMOUNT', numeric(detail.refundedAmountCents), succeededRefundCents),
reconciliationCheck(
'INVENTORY_RESERVATION_BALANCE', expectedInventory.reservationBalance, reservationBalance
),
reconciliationCheck('INVENTORY_SALE_BALANCE', expectedInventory.saleBalance, saleBalance),
{
key: 'STORAGE_ITEM_BALANCE', passed: storages.every((storage) => storage.consistent),
expected: storages.length, actual: storages.filter((storage) => storage.consistent).length
}
];
return {
order: {
id: String(detail.id), orderNo: detail.orderNo, status: detail.status,
inventoryStatus: detail.inventoryStatus
},
amounts: {
orderAmountCents: numeric(detail.totalAmountCents), itemAmountCents,
paidAmountCents: numeric(detail.paidAmountCents), capturedAmountCents,
refundedAmountCents: numeric(detail.refundedAmountCents),
paymentRefundedCents, succeededRefundCents,
netRevenueCents: numeric(detail.paidAmountCents) - numeric(detail.refundedAmountCents)
},
inventory: {
status: detail.inventoryStatus, reservationBalance, saleBalance,
ledger: inventoryLedger.map((entry) => ({
...entry, id: String(entry.id), inventoryId: String(entry.inventoryId),
skuId: String(entry.skuId), availableDelta: numeric(entry.availableDelta),
lockedDelta: numeric(entry.lockedDelta), lossDelta: numeric(entry.lossDelta),
reservationDelta: numeric(entry.reservationDelta), saleDelta: numeric(entry.saleDelta),
availableAfter: numeric(entry.availableAfter), lockedAfter: numeric(entry.lockedAfter),
lossAfter: numeric(entry.lossAfter), versionAfter: numeric(entry.versionAfter)
}))
},
storages,
checks,
consistent: checks.every((item) => item.passed)
};
}
function reconciliationCheck(key: string, expected: number, actual: number) {
return { key, passed: expected === actual, expected, actual };
}
function numeric(value: unknown) {
const parsed = Number(value ?? 0);
return Number.isFinite(parsed) ? parsed : 0;
}
function assertId(value: string) {
if (!idPattern.test(value)) throw new ProductOrderError('PRODUCT_ORDER_INPUT_INVALID');
}
+1
View File
@@ -192,6 +192,7 @@ async function requireInventoryActor(
auth.session.user.id
);
const manager = access.capabilities.includes('tenant.manage')
|| access.capabilities.includes('platform.manage')
|| access.roles.includes('PLATFORM_ADMIN');
const allowed = manager || (write
? access.capabilities.includes('inventory.adjust')
+14 -1
View File
@@ -68,7 +68,8 @@ const testCallbackSchema = z.object({
export interface ProductOrderRouteOptions {
service: Pick<ProductOrderService,
'create' | 'listForCustomer' | 'getForCustomer' | 'cancelForCustomer'
| 'listForManagement' | 'getForManagement' | 'managementAction'
| 'listForManagement' | 'getForManagement' | 'reconcileForManagement'
| 'managementAction'
| 'createPaymentForCustomer' | 'completeTestPaymentForCustomer'
| 'completeTestRefundForManagement'>;
authRepository: Pick<AuthRepository, 'validateSession'>;
@@ -187,6 +188,17 @@ export async function registerProductOrderRoutes(
}));
});
app.get(`${prefix}/product-orders/:orderId/reconciliation`, async (request, reply) => {
const actor = await authenticate(request, reply, options, true, false);
const params = orderParams.safeParse(request.params);
if (!actor || !params.success) return actor ? invalid(reply, request.traceId) : undefined;
return handle(reply, request.traceId, async () => ({
code: 0,
data: await options.service.reconcileForManagement(actor, params.data.orderId),
traceId: request.traceId
}));
});
app.post(`${prefix}/product-orders/:orderId/actions`, async (request, reply) => {
const actor = await authenticate(request, reply, options, true, true);
const params = orderParams.safeParse(request.params);
@@ -243,6 +255,7 @@ async function authenticate(
);
if (management) {
const manager = access.capabilities.includes('tenant.manage')
|| access.capabilities.includes('platform.manage')
|| access.roles.includes('PLATFORM_ADMIN');
const capability = write ? 'goods.order.manage' : 'goods.order.read';
if (!manager && !access.capabilities.includes(capability)
+12 -1
View File
@@ -95,6 +95,17 @@ const forbidden = await app.inject({
assert.equal(forbidden.statusCode, 403);
assert.equal(forbidden.json().code, 'INVENTORY_OPERATION_FORBIDDEN');
currentAccess = {
roles: ['STAFF'], capabilities: ['platform.manage'], storeIds: []
};
assert.equal((await app.inject({
method: 'GET', url: '/admin-api/inventory/stocks?storeId=11', headers
})).statusCode, 200, 'platform.manage must authorize inventory reads');
assert.equal((await app.inject({
method: 'POST', url: '/admin-api/inventory/stocks/5/inbound', headers,
payload: { storeId: '11', requestId: 'platform-inbound', reason: 'platform check', quantity: 1 }
})).statusCode, 200, 'platform.manage must authorize inventory writes');
currentAccess = {
roles: ['STAFF'], capabilities: ['inventory.read'], storeIds: ['11']
};
@@ -105,7 +116,7 @@ const listed = await app.inject({
});
assert.equal(listed.statusCode, 200);
assert.deepEqual(listed.json().data, { items: [], total: 0, page: 2, pageSize: 25 });
const listCall = calls.find((call) => call.method === 'listStocks');
const listCall = calls.findLast((call) => call.method === 'listStocks');
assert.equal(listCall.args[0].tenantId, '7');
assert.equal(listCall.args[0].traceId, 'm09d1-inventory-route');
assert.deepEqual(listCall.args[1], {
+7
View File
@@ -338,6 +338,13 @@ await assert.rejects(
() => service.listStocks(scopedOut, { storeId: '11', page: 1, pageSize: 20 }),
(error) => error instanceof InventoryError && error.code === 'INVENTORY_STORE_SCOPE_FORBIDDEN'
);
const platformManager = {
...manager,
access: { roles: ['STAFF'], capabilities: ['platform.manage'], storeIds: [] }
};
assert.equal((await service.listStocks(
platformManager, { storeId: '11', page: 1, pageSize: 20 }
)).total, 4, 'platform.manage must bypass store grants for inventory reads');
state.lockOrder.length = 0;
const batchBase = {
@@ -4228,6 +4228,30 @@ async function assertProductOrderPaymentInventory(pool, context) {
callbackId: 'm09d2-first-refund-callback', amountCents: 1400
}
);
const completedReconciliation = await service.reconcileForManagement(
adminActor, firstOrder.id
);
assert.equal(completedReconciliation.consistent, true);
assert.equal(completedReconciliation.amounts.netRevenueCents, 0);
assert.equal(completedReconciliation.inventory.saleBalance, 2);
assert.ok(completedReconciliation.inventory.ledger.some(
(entry) => entry.operation === 'LOCK'
));
assert.ok(completedReconciliation.inventory.ledger.some(
(entry) => entry.operation === 'DEDUCT'
));
assert.ok(completedReconciliation.checks.every((check) => check.passed));
const platformActor = {
...adminActor,
access: { roles: ['STAFF'], capabilities: ['platform.manage'], storeIds: [] }
};
assert.equal((await service.reconcileForManagement(
platformActor, firstOrder.id
)).consistent, true);
assert.ok((await inventory.listStocks(
platformActor, { storeId, page: 1, pageSize: 20 }
)).total >= 1);
const restockOrder = await service.create(customerActor, {
...firstInput, requestId: 'm09d2-order-restock', note: 'restock order',
@@ -4526,6 +4550,14 @@ async function assertProductStorageLifecycle(pool, context) {
assert.equal(retrieved.movements.reduce(
(sum, movement) => sum + Number(movement.quantity), 0
), 3);
const storageReconciliation = await orders.reconcileForManagement(
adminActor, sourceOrder.id
);
assert.equal(storageReconciliation.consistent, true);
assert.equal(storageReconciliation.storages.length, 1);
assert.equal(storageReconciliation.storages[0].recordTotalQuantity, 3);
assert.equal(storageReconciliation.storages[0].recordRemainingQuantity, 0);
assert.ok(storageReconciliation.checks.every((check) => check.passed));
await assert.rejects(
() => storages.listForManagement({
@@ -38,6 +38,9 @@ const service = {
items: [order], total: 1, page: input.page, pageSize: input.pageSize
})),
getForManagement: record('getForManagement', () => order),
reconcileForManagement: record('reconcileForManagement', () => ({
order: { id: '101', orderNo: 'PG101' }, consistent: true, checks: []
})),
managementAction: record('managementAction', (_actor, _id, input) => ({
...order, status: input.action === 'ACCEPT' ? 'ACCEPTED' : order.status
})),
@@ -146,6 +149,11 @@ const managementList = await app.inject({
assert.equal(managementList.statusCode, 200);
const managementCall = calls.find((call) => call.method === 'listForManagement');
assert.equal(managementCall.args[0].source, 'MANAGEMENT');
const reconciliation = await app.inject({
method: 'GET', url: '/admin-api/product-orders/101/reconciliation', headers
});
assert.equal(reconciliation.statusCode, 200);
assert.equal(reconciliation.json().data.consistent, true);
const readOnlyAction = await app.inject({
method: 'POST', url: '/admin-api/product-orders/101/actions', headers,
@@ -170,5 +178,16 @@ const refundCompleted = await app.inject({
assert.equal(refundCompleted.statusCode, 200);
assert.equal(refundCompleted.json().data.status, 'SUCCEEDED');
currentAccess = {
roles: ['STAFF'], capabilities: ['platform.manage'], storeIds: []
};
assert.equal((await app.inject({
method: 'GET', url: '/admin-api/product-orders?storeId=11', headers
})).statusCode, 200, 'platform.manage must authorize product-order reads');
assert.equal((await app.inject({
method: 'POST', url: '/admin-api/product-orders/101/actions', headers,
payload: { requestId: 'platform-accept-1', action: 'ACCEPT', reason: '' }
})).statusCode, 200, 'platform.manage must authorize product-order writes');
await app.close();
console.log('PASS: M09-D2 product order routes enforce customer ownership, management permissions and test payment boundaries.');
@@ -0,0 +1,67 @@
import assert from 'node:assert/strict';
import { buildProductOrderReconciliation } from '../dist/products/product-order-service.js';
const detail = {
id: '101', orderNo: 'PG101', status: 'COMPLETED', inventoryStatus: 'DEDUCTED',
itemCount: 1, totalQuantity: 2, totalAmountCents: 500,
paidAmountCents: 500, refundedAmountCents: 0,
items: [{ id: '201', quantity: 2, subtotalCents: 500 }],
payments: [{ id: '301', status: 'SUCCEEDED', amountCents: 500, refundedAmountCents: 0 }],
refunds: []
};
const inventoryLedger = [
{
id: '401', inventoryId: '51', skuId: '5', operation: 'LOCK',
availableDelta: -2, lockedDelta: 2, lossDelta: 0,
reservationDelta: 2, saleDelta: 0,
availableAfter: 8, lockedAfter: 2, lossAfter: 0, versionAfter: 2
},
{
id: '402', inventoryId: '51', skuId: '5', operation: 'DEDUCT',
availableDelta: 0, lockedDelta: -2, lossDelta: 0,
reservationDelta: -2, saleDelta: 2,
availableAfter: 8, lockedAfter: 0, lossAfter: 0, versionAfter: 3
}
];
const storages = [{
id: '601', storageNo: 'PS601', status: 'PARTIALLY_RETRIEVED',
recordTotalQuantity: 2, recordRemainingQuantity: 1,
itemTotalQuantity: 2, itemRemainingQuantity: 1, itemCount: 1
}];
const consistent = buildProductOrderReconciliation(detail, inventoryLedger, storages);
assert.equal(consistent.consistent, true);
assert.equal(consistent.amounts.netRevenueCents, 500);
assert.deepEqual(
[consistent.inventory.reservationBalance, consistent.inventory.saleBalance], [0, 2]
);
assert.equal(consistent.storages[0].consistent, true);
assert.ok(consistent.checks.every((check) => check.passed));
const inconsistent = buildProductOrderReconciliation(
{ ...detail, paidAmountCents: 400, refundedAmountCents: 100 },
inventoryLedger,
[{ ...storages[0], itemRemainingQuantity: 2 }]
);
assert.equal(inconsistent.consistent, false);
assert.equal(
inconsistent.checks.find((check) => check.key === 'PAYMENT_CAPTURED_AMOUNT').passed, false
);
assert.equal(
inconsistent.checks.find((check) => check.key === 'REFUND_SUCCEEDED_AMOUNT').passed, false
);
assert.equal(
inconsistent.checks.find((check) => check.key === 'STORAGE_ITEM_BALANCE').passed, false
);
const released = buildProductOrderReconciliation({
...detail, status: 'CANCELLED', inventoryStatus: 'RELEASED',
paidAmountCents: 0, payments: [], refunds: []
}, [
inventoryLedger[0],
{ ...inventoryLedger[1], operation: 'RELEASE', reservationDelta: -2, saleDelta: 0 }
], []);
assert.equal(released.consistent, true);
assert.deepEqual([released.inventory.reservationBalance, released.inventory.saleBalance], [0, 0]);
console.log('PASS: M09-REGRESSION detects amount, payment, refund, inventory and storage drift.');