forked from wangziqi/gongxue-base
feat: add reconciliation issue workflow
This commit is contained in:
@@ -2205,6 +2205,114 @@ async function testCommerce() {
|
||||
'reconciliation anomalies endpoint should expose imported issues',
|
||||
);
|
||||
|
||||
const actionableReconItem = reconciliationItems.items?.find(item => item.orderNo === reconMissingProviderOrder.item.orderNo);
|
||||
assert.ok(actionableReconItem?.id, 'reconciliation test should have an actionable missing_provider item');
|
||||
|
||||
const studentReconciliationIssueDenied = await request('/api/commerce/reconciliation/issues/create', {
|
||||
method: 'POST',
|
||||
body: { itemId: actionableReconItem.id },
|
||||
expectStatus: 403,
|
||||
});
|
||||
assert.equal(studentReconciliationIssueDenied.code, 'TENANT_ADMIN_REQUIRED', 'students must not create reconciliation issues');
|
||||
|
||||
const reconciliationIssue = await request('/api/commerce/reconciliation/issues/create', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
method: 'POST',
|
||||
body: {
|
||||
itemId: actionableReconItem.id,
|
||||
assignedTo: TENANT_OPERATOR_USER_ID,
|
||||
dueAt: new Date(Date.now() + 86_400_000).toISOString(),
|
||||
summary: '人工核对供应商缺失账单',
|
||||
note: '集成测试创建差错工单',
|
||||
metadata: { source: 'api-integration-test' },
|
||||
},
|
||||
});
|
||||
assert.ok(reconciliationIssue.item?.id, 'tenant admin should create a reconciliation issue');
|
||||
assert.equal(reconciliationIssue.item?.status, 'open', 'created reconciliation issue should start open');
|
||||
assert.equal(reconciliationIssue.item?.assignedTo, TENANT_OPERATOR_USER_ID, 'created issue should keep assignee');
|
||||
assert.equal(reconciliationIssue.item?.matchStatus, 'missing_provider', 'created issue should copy match status');
|
||||
|
||||
const duplicateReconciliationIssue = await request('/api/commerce/reconciliation/issues/create', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
method: 'POST',
|
||||
body: { itemId: actionableReconItem.id },
|
||||
});
|
||||
assert.equal(duplicateReconciliationIssue.item?.id, reconciliationIssue.item.id, 'duplicate active issue creation should be idempotent');
|
||||
assert.equal(duplicateReconciliationIssue.idempotent, true, 'duplicate active issue creation should return idempotent=true');
|
||||
|
||||
const reconciliationIssues = await request('/api/commerce/reconciliation/issues', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
query: { status: 'open', assignedTo: TENANT_OPERATOR_USER_ID },
|
||||
});
|
||||
assert.ok(
|
||||
reconciliationIssues.items?.some(item => item.id === reconciliationIssue.item.id),
|
||||
'reconciliation issues endpoint should filter by status and assignee',
|
||||
);
|
||||
|
||||
const startedReconciliationIssue = await request('/api/commerce/reconciliation/issues/status', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
method: 'POST',
|
||||
body: {
|
||||
issueId: reconciliationIssue.item.id,
|
||||
action: 'start',
|
||||
note: '开始核查',
|
||||
},
|
||||
});
|
||||
assert.equal(startedReconciliationIssue.item?.status, 'investigating', 'start action should mark issue investigating');
|
||||
|
||||
const assignedReconciliationIssue = await request('/api/commerce/reconciliation/issues/status', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
method: 'POST',
|
||||
body: {
|
||||
issueId: reconciliationIssue.item.id,
|
||||
action: 'assign',
|
||||
assignedTo: TENANT_ADMIN_USER_ID,
|
||||
note: '转交财务管理员复核',
|
||||
},
|
||||
});
|
||||
assert.equal(assignedReconciliationIssue.item?.assignedTo, TENANT_ADMIN_USER_ID, 'assign action should update assignee');
|
||||
assert.equal(assignedReconciliationIssue.item?.status, 'investigating', 'assign action should not close issue');
|
||||
|
||||
const resolvedReconciliationIssue = await request('/api/commerce/reconciliation/issues/status', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
method: 'POST',
|
||||
body: {
|
||||
issueId: reconciliationIssue.item.id,
|
||||
action: 'resolve',
|
||||
resolutionType: 'manual_adjustment',
|
||||
note: '已生成线下调整凭证',
|
||||
metadata: { voucherNo: 'VOUCHER-RECON-001' },
|
||||
},
|
||||
});
|
||||
assert.equal(resolvedReconciliationIssue.item?.status, 'resolved', 'resolve action should close issue');
|
||||
assert.equal(resolvedReconciliationIssue.item?.resolutionType, 'manual_adjustment', 'resolve action should keep resolution type');
|
||||
assert.equal(resolvedReconciliationIssue.item?.resolvedBy, TENANT_ADMIN_USER_ID, 'resolve action should record resolver');
|
||||
|
||||
const reconciliationIssueEvents = await request('/api/commerce/reconciliation/issues/events', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
query: { issueId: reconciliationIssue.item.id },
|
||||
});
|
||||
assert.ok(
|
||||
reconciliationIssueEvents.items?.some(item => item.eventType === 'created' && item.toStatus === 'open'),
|
||||
'reconciliation issue events should include creation',
|
||||
);
|
||||
assert.ok(
|
||||
reconciliationIssueEvents.items?.some(item => item.eventType === 'start' && item.toStatus === 'investigating'),
|
||||
'reconciliation issue events should include start transition',
|
||||
);
|
||||
assert.ok(
|
||||
reconciliationIssueEvents.items?.some(item => item.eventType === 'resolve' && item.toStatus === 'resolved'),
|
||||
'reconciliation issue events should include resolution transition',
|
||||
);
|
||||
|
||||
const crossTenantReconciliationIssueDenied = await request('/api/commerce/reconciliation/issues/events', {
|
||||
tenantId: PARTNER_TENANT_ID,
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
query: { issueId: reconciliationIssue.item.id },
|
||||
expectStatus: 403,
|
||||
});
|
||||
assert.equal(crossTenantReconciliationIssueDenied.code, 'TENANT_ADMIN_REQUIRED', 'reconciliation issue events must be tenant isolated');
|
||||
|
||||
const crossTenantReconciliationDenied = await request('/api/commerce/reconciliation/items', {
|
||||
tenantId: PARTNER_TENANT_ID,
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
|
||||
Reference in New Issue
Block a user