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