forked from wangziqi/gongxue-base
feat: add commerce adjustment operations
This commit is contained in:
@@ -2541,6 +2541,180 @@ async function testCommerce() {
|
||||
});
|
||||
assert.equal(crossTenantProviderBillJobsDenied.code, 'TENANT_ADMIN_REQUIRED', 'provider bill jobs must be tenant isolated');
|
||||
|
||||
const operationReconItems = await request('/api/commerce/reconciliation/items', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
query: { batchId: reconciliationImport.item.id, matchStatus: 'amount_mismatch' },
|
||||
});
|
||||
const operationReconItem = operationReconItems.items?.find(item => item.matchStatus === 'amount_mismatch');
|
||||
assert.ok(operationReconItem?.id, 'operation anomaly test should have an open actionable reconciliation item');
|
||||
|
||||
const operationIssue = await request('/api/commerce/reconciliation/issues/create', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
method: 'POST',
|
||||
body: {
|
||||
itemId: operationReconItem.id,
|
||||
summary: '运营台展示未关闭对账差错',
|
||||
note: '保持 open 供运营台测试',
|
||||
metadata: { source: 'operation-anomaly-test' },
|
||||
},
|
||||
});
|
||||
assert.equal(operationIssue.item?.status, 'open', 'operation anomaly issue should remain open');
|
||||
|
||||
const studentOperationsDenied = await request('/api/commerce/operations/anomalies', {
|
||||
expectStatus: 403,
|
||||
});
|
||||
assert.equal(studentOperationsDenied.code, 'TENANT_ADMIN_REQUIRED', 'students must not access finance operation anomalies');
|
||||
|
||||
const operationAnomalies = await request('/api/commerce/operations/anomalies', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
query: { provider: 'manual' },
|
||||
});
|
||||
assert.ok(
|
||||
operationAnomalies.items?.some(item => item.type === 'reconciliation_issue' && item.id === operationIssue.item.id),
|
||||
'operation anomaly console should surface open reconciliation issues',
|
||||
);
|
||||
|
||||
const voucherSourceOrderBefore = await request('/api/commerce/orders/status', {
|
||||
query: { orderNo: reconMissingProviderOrder.item.orderNo },
|
||||
});
|
||||
const voucherSourcePaymentBefore = voucherSourceOrderBefore.item?.payment;
|
||||
const voucherEntitlementsBefore = await request('/api/commerce/entitlements', {});
|
||||
|
||||
const studentAdjustmentDenied = await request('/api/commerce/adjustment-vouchers', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
reconciliationIssueId: reconciliationIssue.item.id,
|
||||
title: 'student should not create adjustment voucher',
|
||||
},
|
||||
expectStatus: 403,
|
||||
});
|
||||
assert.equal(studentAdjustmentDenied.code, 'TENANT_ADMIN_REQUIRED', 'students must not create adjustment vouchers');
|
||||
|
||||
const adjustmentVoucherNo = `ADJ-INTEGRATION-${Date.now()}`;
|
||||
const adjustmentVoucher = await request('/api/commerce/adjustment-vouchers', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
method: 'POST',
|
||||
body: {
|
||||
voucherNo: adjustmentVoucherNo,
|
||||
reconciliationIssueId: reconciliationIssue.item.id,
|
||||
adjustmentType: 'write_off',
|
||||
direction: 'decrease',
|
||||
amountCents: reconMissingProviderOrder.item.amountCents,
|
||||
title: '供应商缺失账单人工核销凭证',
|
||||
description: '仅作为财务复核证据,不直接修改订单状态',
|
||||
externalUrl: 'https://finance.example.test/proofs/adj-integration-001',
|
||||
metadata: { source: 'api-integration-test' },
|
||||
},
|
||||
});
|
||||
assert.ok(adjustmentVoucher.item?.id, 'tenant admin should create adjustment voucher');
|
||||
assert.equal(adjustmentVoucher.item?.status, 'submitted', 'adjustment voucher should default to submitted');
|
||||
assert.equal(adjustmentVoucher.item?.reconciliationIssueId, reconciliationIssue.item.id, 'voucher should link reconciliation issue');
|
||||
assert.equal(adjustmentVoucher.item?.orderNo, reconMissingProviderOrder.item.orderNo, 'voucher should copy order identity from issue');
|
||||
|
||||
const duplicateAdjustmentVoucher = await request('/api/commerce/adjustment-vouchers', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
method: 'POST',
|
||||
body: {
|
||||
voucherNo: adjustmentVoucherNo,
|
||||
reconciliationIssueId: reconciliationIssue.item.id,
|
||||
title: 'duplicate voucher number should be rejected',
|
||||
},
|
||||
expectStatus: 409,
|
||||
});
|
||||
assert.equal(duplicateAdjustmentVoucher.code, 'ADJUSTMENT_VOUCHER_DUPLICATE', 'duplicate voucher number should return business conflict');
|
||||
|
||||
const invalidAdjustmentSource = await request('/api/commerce/adjustment-vouchers', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
method: 'POST',
|
||||
body: {
|
||||
reconciliationIssueId: 'not-a-uuid',
|
||||
title: 'invalid source id should be rejected',
|
||||
},
|
||||
expectStatus: 400,
|
||||
});
|
||||
assert.equal(invalidAdjustmentSource.code, 'INVALID_UUID', 'adjustment voucher should reject malformed source UUIDs');
|
||||
|
||||
const listedAdjustmentVouchers = await request('/api/commerce/adjustment-vouchers', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
query: { status: 'submitted', orderNo: reconMissingProviderOrder.item.orderNo },
|
||||
});
|
||||
assert.ok(
|
||||
listedAdjustmentVouchers.items?.some(item => item.id === adjustmentVoucher.item.id),
|
||||
'adjustment voucher list should filter submitted vouchers by order number',
|
||||
);
|
||||
|
||||
const adjustmentReport = await request('/api/commerce/adjustment-vouchers/report', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
query: { startDate: billDate, endDate: billDate },
|
||||
});
|
||||
assert.ok(adjustmentReport.item?.totalCount >= 1, 'adjustment voucher report should include created voucher');
|
||||
assert.ok(
|
||||
adjustmentReport.item?.byStatus?.some(item => item.status === 'submitted' && item.count >= 1),
|
||||
'adjustment voucher report should aggregate by status',
|
||||
);
|
||||
|
||||
const approvedAdjustmentVoucher = await request('/api/commerce/adjustment-vouchers/status', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
method: 'POST',
|
||||
body: {
|
||||
voucherId: adjustmentVoucher.item.id,
|
||||
status: 'approved',
|
||||
reviewNote: '财务复核通过,仅保留审计凭证',
|
||||
metadata: { reviewedFrom: 'api-integration-test' },
|
||||
},
|
||||
});
|
||||
assert.equal(approvedAdjustmentVoucher.item?.status, 'approved', 'reviewer should approve submitted adjustment voucher');
|
||||
assert.equal(approvedAdjustmentVoucher.item?.reviewedBy, TENANT_ADMIN_USER_ID, 'voucher approval should record reviewer');
|
||||
|
||||
const adjustmentVoucherEvents = await request('/api/commerce/adjustment-vouchers/events', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
query: { voucherId: adjustmentVoucher.item.id },
|
||||
});
|
||||
assert.ok(
|
||||
adjustmentVoucherEvents.items?.some(item => item.eventType === 'submitted' && item.toStatus === 'submitted'),
|
||||
'voucher events should include submit event',
|
||||
);
|
||||
assert.ok(
|
||||
adjustmentVoucherEvents.items?.some(item => item.eventType === 'approved' && item.toStatus === 'approved'),
|
||||
'voucher events should include approval event',
|
||||
);
|
||||
|
||||
const closedVoucherChangeDenied = await request('/api/commerce/adjustment-vouchers/status', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
method: 'POST',
|
||||
body: {
|
||||
voucherId: adjustmentVoucher.item.id,
|
||||
status: 'rejected',
|
||||
reviewNote: 'closed voucher should not flip to rejected',
|
||||
},
|
||||
expectStatus: 409,
|
||||
});
|
||||
assert.equal(closedVoucherChangeDenied.code, 'ADJUSTMENT_VOUCHER_CLOSED', 'closed adjustment voucher must be immutable');
|
||||
|
||||
const crossTenantVoucherDenied = await request('/api/commerce/adjustment-vouchers', {
|
||||
tenantId: PARTNER_TENANT_ID,
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
query: { voucherNo: adjustmentVoucherNo },
|
||||
expectStatus: 403,
|
||||
});
|
||||
assert.equal(crossTenantVoucherDenied.code, 'TENANT_ADMIN_REQUIRED', 'adjustment vouchers must be tenant isolated');
|
||||
|
||||
const voucherSourceOrderAfter = await request('/api/commerce/orders/status', {
|
||||
query: { orderNo: reconMissingProviderOrder.item.orderNo },
|
||||
});
|
||||
const voucherEntitlementsAfter = await request('/api/commerce/entitlements', {});
|
||||
assert.equal(voucherSourceOrderAfter.item?.status, voucherSourceOrderBefore.item?.status, 'adjustment voucher approval must not mutate order status');
|
||||
assert.equal(
|
||||
voucherSourceOrderAfter.item?.payment?.status,
|
||||
voucherSourcePaymentBefore?.status,
|
||||
'adjustment voucher approval must not mutate payment status',
|
||||
);
|
||||
assert.equal(
|
||||
voucherEntitlementsAfter.items?.length,
|
||||
voucherEntitlementsBefore.items?.length,
|
||||
'adjustment voucher approval must not grant or revoke entitlements',
|
||||
);
|
||||
|
||||
const fakeWechatPay = await startFakeWechatPayServer();
|
||||
const wechatAccount = await request('/api/tenant-admin/payment-accounts', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
@@ -6210,6 +6384,7 @@ async function testTenantMemberPermissionsAndAudit() {
|
||||
assert.ok(permissionMatrix.permissions?.some(item => item.key === 'badges:grant'), 'permission matrix should expose badge grant permission');
|
||||
assert.ok(permissionMatrix.permissions?.some(item => item.key === 'tenant:reconciliation:read'), 'permission matrix should expose reconciliation read permission');
|
||||
assert.ok(permissionMatrix.permissions?.some(item => item.key === 'tenant:reconciliation:write'), 'permission matrix should expose reconciliation write permission');
|
||||
assert.ok(permissionMatrix.permissions?.some(item => item.key === 'tenant:reconciliation:review'), 'permission matrix should expose reconciliation review permission');
|
||||
assert.ok(permissionMatrix.menuGroups?.some(item => item.key === 'sales'), 'permission matrix should expose menu groups');
|
||||
assert.ok(permissionMatrix.roleDefaults?.tenant_operator?.includes('dashboard:read'), 'tenant operator defaults should include dashboard read');
|
||||
assert.ok(permissionMatrix.roleDefaults?.tenant_operator?.includes('marketing:*'), 'permission matrix should include role defaults');
|
||||
|
||||
Reference in New Issue
Block a user