feat(M09-D2): 完成商品订单与库存占用生命周期
This commit is contained in:
@@ -0,0 +1,174 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import Fastify from 'fastify';
|
||||
import { signAccessToken } from '../dist/auth/jwt.js';
|
||||
import { ProductOrderError } from '../dist/products/product-order-service.js';
|
||||
import { registerProductOrderRoutes } from '../dist/routes/product-orders.js';
|
||||
|
||||
const secret = 'test-only-product-order-route-jwt-secret';
|
||||
const sessionId = '3a6ab573-1105-4bf9-b75b-e88e49eb3b82';
|
||||
const token = signAccessToken({
|
||||
sub: '21', sid: sessionId, tid: '7', aid: '9', rv: 1
|
||||
}, secret, 900);
|
||||
const headers = {
|
||||
authorization: `Bearer ${token}`,
|
||||
'x-trace-id': 'm09d2-product-order-route'
|
||||
};
|
||||
let currentAccess = { roles: ['CUSTOMER'], capabilities: [], storeIds: [] };
|
||||
const calls = [];
|
||||
const record = (method, result) => async (...args) => {
|
||||
calls.push({ method, args });
|
||||
return typeof result === 'function' ? result(...args) : result;
|
||||
};
|
||||
const order = {
|
||||
id: '101', storeId: '11', orderNo: 'PG101', status: 'PENDING_PAYMENT',
|
||||
fulfillmentMode: 'SELF_SERVICE', totalAmountCents: 500,
|
||||
inventoryStatus: 'LOCKED'
|
||||
};
|
||||
const service = {
|
||||
create: record('create', (_actor, input) => ({ ...order, storeId: input.storeId })),
|
||||
listForCustomer: record('listForCustomer', (_actor, input) => ({
|
||||
items: [order], total: 1, page: input.page, pageSize: input.pageSize
|
||||
})),
|
||||
getForCustomer: record('getForCustomer', (_actor, orderId) => {
|
||||
if (orderId === '999') throw new ProductOrderError('PRODUCT_ORDER_NOT_FOUND');
|
||||
return order;
|
||||
}),
|
||||
cancelForCustomer: record('cancelForCustomer', () => ({ ...order, status: 'CANCELLED' })),
|
||||
listForManagement: record('listForManagement', (_actor, input) => ({
|
||||
items: [order], total: 1, page: input.page, pageSize: input.pageSize
|
||||
})),
|
||||
getForManagement: record('getForManagement', () => order),
|
||||
managementAction: record('managementAction', (_actor, _id, input) => ({
|
||||
...order, status: input.action === 'ACCEPT' ? 'ACCEPTED' : order.status
|
||||
})),
|
||||
createPaymentForCustomer: record('createPaymentForCustomer', () => ({
|
||||
id: '401', orderId: '101', provider: 'TEST', status: 'PENDING', amountCents: 500
|
||||
})),
|
||||
completeTestPaymentForCustomer: record('completeTestPaymentForCustomer', () => ({
|
||||
paymentId: '401', orderId: '101', status: 'SUCCEEDED'
|
||||
})),
|
||||
completeTestRefundForManagement: record('completeTestRefundForManagement', () => ({
|
||||
refundId: '501', orderId: '101', status: 'SUCCEEDED'
|
||||
}))
|
||||
};
|
||||
|
||||
const app = Fastify({ logger: false });
|
||||
app.decorateRequest('traceId', '');
|
||||
app.addHook('onRequest', async (request, reply) => {
|
||||
request.traceId = request.headers['x-trace-id'] || request.id;
|
||||
reply.header('x-trace-id', request.traceId);
|
||||
});
|
||||
await registerProductOrderRoutes(app, {
|
||||
service,
|
||||
authRepository: {
|
||||
async validateSession() {
|
||||
return {
|
||||
id: sessionId, tenantId: '7', platformAppId: '9',
|
||||
expiresAt: new Date(Date.now() + 60_000),
|
||||
user: {
|
||||
id: '21', tenantId: '7', userType: 'CUSTOMER', status: 'ACTIVE',
|
||||
roleVersion: 1, nickname: '', avatarUrl: '', phone: ''
|
||||
}
|
||||
};
|
||||
}
|
||||
},
|
||||
accessControl: { async getAccessProfile() { return currentAccess; } },
|
||||
jwtSecret: secret,
|
||||
testAdapterEnabled: true
|
||||
});
|
||||
|
||||
const unauthorized = await app.inject({ method: 'GET', url: '/app-api/product-orders' });
|
||||
assert.equal(unauthorized.statusCode, 401);
|
||||
assert.equal(unauthorized.json().code, 'AUTH_SESSION_INVALID');
|
||||
|
||||
const invalidDelivery = await app.inject({
|
||||
method: 'POST', url: '/app-api/product-orders', headers,
|
||||
payload: {
|
||||
storeId: '11', requestId: 'create-invalid', fulfillmentMode: 'DELIVERY',
|
||||
note: '', items: [{ skuId: '5', quantity: 1, note: '' }]
|
||||
}
|
||||
});
|
||||
assert.equal(invalidDelivery.statusCode, 400);
|
||||
assert.equal(invalidDelivery.json().code, 'PRODUCT_ORDER_INPUT_INVALID');
|
||||
|
||||
const created = await app.inject({
|
||||
method: 'POST', url: '/app-api/product-orders', headers,
|
||||
payload: {
|
||||
storeId: '11', requestId: 'create-order-1', fulfillmentMode: 'SELF_SERVICE',
|
||||
note: '少冰', items: [{ skuId: '5', quantity: 2, note: '' }]
|
||||
}
|
||||
});
|
||||
assert.equal(created.statusCode, 201);
|
||||
assert.equal(created.json().data.id, '101');
|
||||
const createCall = calls.find((call) => call.method === 'create');
|
||||
assert.equal(createCall.args[0].tenantId, '7');
|
||||
assert.equal(createCall.args[0].userId, '21');
|
||||
assert.equal(createCall.args[0].source, 'CUSTOMER');
|
||||
assert.equal(createCall.args[0].traceId, 'm09d2-product-order-route');
|
||||
|
||||
const listed = await app.inject({
|
||||
method: 'GET', url: '/app-api/product-orders?storeId=11&page=2&pageSize=10', headers
|
||||
});
|
||||
assert.equal(listed.statusCode, 200);
|
||||
assert.equal(listed.json().data.page, 2);
|
||||
|
||||
const missing = await app.inject({
|
||||
method: 'GET', url: '/app-api/product-orders/999', headers
|
||||
});
|
||||
assert.equal(missing.statusCode, 404);
|
||||
assert.equal(missing.json().code, 'PRODUCT_ORDER_NOT_FOUND');
|
||||
|
||||
const payment = await app.inject({
|
||||
method: 'POST', url: '/app-api/product-orders/101/payments', headers,
|
||||
payload: { requestId: 'payment-create-1', provider: 'TEST' }
|
||||
});
|
||||
assert.equal(payment.statusCode, 201);
|
||||
assert.equal(payment.json().data.id, '401');
|
||||
|
||||
const paid = await app.inject({
|
||||
method: 'POST', url: '/app-api/product-payments/401/test-complete', headers,
|
||||
payload: { callbackId: 'payment-callback-1', amountCents: 500 }
|
||||
});
|
||||
assert.equal(paid.statusCode, 200);
|
||||
assert.equal(paid.json().data.status, 'SUCCEEDED');
|
||||
|
||||
const managementForbidden = await app.inject({
|
||||
method: 'GET', url: '/admin-api/product-orders?storeId=11', headers
|
||||
});
|
||||
assert.equal(managementForbidden.statusCode, 403);
|
||||
|
||||
currentAccess = {
|
||||
roles: ['STAFF'], capabilities: ['goods.order.read'], storeIds: ['11']
|
||||
};
|
||||
const managementList = await app.inject({
|
||||
method: 'GET', url: '/app-api/management/product-orders?storeId=11', headers
|
||||
});
|
||||
assert.equal(managementList.statusCode, 200);
|
||||
const managementCall = calls.find((call) => call.method === 'listForManagement');
|
||||
assert.equal(managementCall.args[0].source, 'MANAGEMENT');
|
||||
|
||||
const readOnlyAction = await app.inject({
|
||||
method: 'POST', url: '/admin-api/product-orders/101/actions', headers,
|
||||
payload: { requestId: 'accept-order-1', action: 'ACCEPT', reason: '' }
|
||||
});
|
||||
assert.equal(readOnlyAction.statusCode, 403);
|
||||
|
||||
currentAccess = {
|
||||
roles: ['STAFF'], capabilities: ['goods.order.read', 'goods.order.manage'], storeIds: ['11']
|
||||
};
|
||||
const accepted = await app.inject({
|
||||
method: 'POST', url: '/admin-api/product-orders/101/actions', headers,
|
||||
payload: { requestId: 'accept-order-1', action: 'ACCEPT', reason: '' }
|
||||
});
|
||||
assert.equal(accepted.statusCode, 200);
|
||||
assert.equal(accepted.json().data.status, 'ACCEPTED');
|
||||
|
||||
const refundCompleted = await app.inject({
|
||||
method: 'POST', url: '/admin-api/product-refunds/501/test-complete', headers,
|
||||
payload: { callbackId: 'refund-callback-1', amountCents: 500 }
|
||||
});
|
||||
assert.equal(refundCompleted.statusCode, 200);
|
||||
assert.equal(refundCompleted.json().data.status, 'SUCCEEDED');
|
||||
|
||||
await app.close();
|
||||
console.log('PASS: M09-D2 product order routes enforce customer ownership, management permissions and test payment boundaries.');
|
||||
Reference in New Issue
Block a user