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