feat(P19ERP2-1,P19ERP2-2): 实现采购ERP凭证与摘要出站服务
这个提交包含在:
@@ -0,0 +1,130 @@
|
||||
const purchaseRequestRoutes = require('../../../src/routes/purchaseRequests');
|
||||
|
||||
describe('purchase archive ERP helper', () => {
|
||||
const { handlePurchaseArchiveErpSync } = purchaseRequestRoutes.__testables;
|
||||
|
||||
function createRequest() {
|
||||
return {
|
||||
id: 'req-1',
|
||||
ip: '127.0.0.1',
|
||||
headers: {},
|
||||
employee: {
|
||||
id: 'actor-1',
|
||||
department_id: 'dept-1',
|
||||
permissionCodes: ['procurement.manage'],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createTx() {
|
||||
return {
|
||||
auditLog: {
|
||||
create: vi.fn().mockResolvedValue({ id: 'audit-1' }),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createContext() {
|
||||
return {
|
||||
purchaseRequest: {
|
||||
id: 'pr-1',
|
||||
department_id: 'dept-1',
|
||||
order_archive: {
|
||||
id: 'archive-1',
|
||||
po_no: 'PO-001',
|
||||
contract_no: 'CT-001',
|
||||
archived_at: new Date('2026-03-08T10:30:00.000Z'),
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
it('first archive enqueues accrual and summary', async () => {
|
||||
const tx = createTx();
|
||||
const loadContext = vi.fn().mockResolvedValue(createContext());
|
||||
const evaluateGate = vi.fn().mockResolvedValue({
|
||||
enabled: true,
|
||||
reason: 'env_inherit_department_allowed',
|
||||
});
|
||||
const enqueueAccrual = vi.fn().mockResolvedValue({
|
||||
id: 'outbox-accrual',
|
||||
status: 'pending',
|
||||
duplicated: false,
|
||||
requeued: false,
|
||||
eventType: 'erp.journal.export',
|
||||
erpTarget: 'accrual_journal',
|
||||
});
|
||||
const enqueueSummary = vi.fn().mockResolvedValue({
|
||||
id: 'outbox-summary',
|
||||
status: 'pending',
|
||||
duplicated: false,
|
||||
requeued: false,
|
||||
eventType: 'erp.procurement.export',
|
||||
erpTarget: 'archive_summary',
|
||||
});
|
||||
|
||||
const result = await handlePurchaseArchiveErpSync({
|
||||
tx,
|
||||
config: {},
|
||||
request: createRequest(),
|
||||
actorId: 'actor-1',
|
||||
purchaseRequestId: 'pr-1',
|
||||
archiveId: 'archive-1',
|
||||
isFirstArchive: true,
|
||||
loadContext,
|
||||
evaluateGate,
|
||||
enqueueAccrual,
|
||||
enqueueSummary,
|
||||
});
|
||||
|
||||
expect(enqueueAccrual).toHaveBeenCalledWith(
|
||||
tx,
|
||||
{},
|
||||
expect.objectContaining({ requeueOnFailure: false }),
|
||||
);
|
||||
expect(enqueueSummary).toHaveBeenCalledWith(
|
||||
tx,
|
||||
{},
|
||||
expect.objectContaining({ requeueOnFailure: false }),
|
||||
);
|
||||
expect(result.items).toHaveLength(2);
|
||||
expect(tx.auditLog.create).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('archive update only re-enqueues summary', async () => {
|
||||
const tx = createTx();
|
||||
const loadContext = vi.fn().mockResolvedValue(createContext());
|
||||
const evaluateGate = vi.fn().mockResolvedValue({
|
||||
enabled: true,
|
||||
reason: 'env_inherit_department_allowed',
|
||||
});
|
||||
const enqueueAccrual = vi.fn();
|
||||
const enqueueSummary = vi.fn().mockResolvedValue({
|
||||
id: 'outbox-summary',
|
||||
status: 'pending',
|
||||
duplicated: false,
|
||||
requeued: true,
|
||||
eventType: 'erp.procurement.export',
|
||||
erpTarget: 'archive_summary',
|
||||
});
|
||||
|
||||
const result = await handlePurchaseArchiveErpSync({
|
||||
tx,
|
||||
config: {},
|
||||
request: createRequest(),
|
||||
actorId: 'actor-1',
|
||||
purchaseRequestId: 'pr-1',
|
||||
archiveId: 'archive-1',
|
||||
isFirstArchive: false,
|
||||
loadContext,
|
||||
evaluateGate,
|
||||
enqueueAccrual,
|
||||
enqueueSummary,
|
||||
});
|
||||
|
||||
expect(enqueueAccrual).not.toHaveBeenCalled();
|
||||
expect(enqueueSummary).toHaveBeenCalled();
|
||||
expect(result.items).toHaveLength(1);
|
||||
expect(result.items[0].erpTarget).toBe('archive_summary');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,114 @@
|
||||
const workflowRoutes = require('../../../src/routes/workflow');
|
||||
|
||||
describe('workflow purchase ERP helper', () => {
|
||||
const { handlePurchaseApprovalErpSync } = workflowRoutes.__testables;
|
||||
|
||||
function createRequest() {
|
||||
return {
|
||||
id: 'req-1',
|
||||
ip: '127.0.0.1',
|
||||
headers: {},
|
||||
employee: {
|
||||
id: 'actor-1',
|
||||
department_id: 'dept-1',
|
||||
permissionCodes: ['procurement.manage'],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createTx() {
|
||||
return {
|
||||
auditLog: {
|
||||
create: vi.fn().mockResolvedValue({ id: 'audit-1' }),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
it('auto enqueues commitment journal when gate is enabled', async () => {
|
||||
const tx = createTx();
|
||||
const loadContext = vi.fn().mockResolvedValue({
|
||||
purchaseRequest: {
|
||||
id: 'pr-1',
|
||||
department_id: 'dept-1',
|
||||
},
|
||||
approvalOccurredAt: new Date('2026-03-08T10:00:00.000Z'),
|
||||
});
|
||||
const evaluateGate = vi.fn().mockResolvedValue({
|
||||
enabled: true,
|
||||
reason: 'env_inherit_department_allowed',
|
||||
});
|
||||
const enqueueCommitment = vi.fn().mockResolvedValue({
|
||||
id: 'outbox-1',
|
||||
status: 'pending',
|
||||
duplicated: false,
|
||||
requeued: false,
|
||||
eventType: 'erp.journal.export',
|
||||
});
|
||||
|
||||
const result = await handlePurchaseApprovalErpSync({
|
||||
tx,
|
||||
config: {},
|
||||
request: createRequest(),
|
||||
actorId: 'actor-1',
|
||||
purchaseRequestId: 'pr-1',
|
||||
loadContext,
|
||||
evaluateGate,
|
||||
enqueueCommitment,
|
||||
});
|
||||
|
||||
expect(enqueueCommitment).toHaveBeenCalledWith(
|
||||
tx,
|
||||
{},
|
||||
expect.objectContaining({
|
||||
actorId: 'actor-1',
|
||||
requeueOnFailure: false,
|
||||
}),
|
||||
);
|
||||
expect(tx.auditLog.create).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
data: expect.objectContaining({
|
||||
action: 'ERP_SYNC_ENQUEUED',
|
||||
target_type: 'purchase_request',
|
||||
target_id: 'pr-1',
|
||||
}),
|
||||
}),
|
||||
);
|
||||
expect(result.skipped).toBe(false);
|
||||
});
|
||||
|
||||
it('writes skip audit when approval gate is disabled', async () => {
|
||||
const tx = createTx();
|
||||
const loadContext = vi.fn().mockResolvedValue({
|
||||
purchaseRequest: {
|
||||
id: 'pr-1',
|
||||
department_id: 'dept-1',
|
||||
},
|
||||
approvalOccurredAt: new Date('2026-03-08T10:00:00.000Z'),
|
||||
});
|
||||
const evaluateGate = vi.fn().mockResolvedValue({ enabled: false, reason: 'flag_force_off' });
|
||||
const enqueueCommitment = vi.fn();
|
||||
|
||||
const result = await handlePurchaseApprovalErpSync({
|
||||
tx,
|
||||
config: {},
|
||||
request: createRequest(),
|
||||
actorId: 'actor-1',
|
||||
purchaseRequestId: 'pr-1',
|
||||
loadContext,
|
||||
evaluateGate,
|
||||
enqueueCommitment,
|
||||
});
|
||||
|
||||
expect(enqueueCommitment).not.toHaveBeenCalled();
|
||||
expect(tx.auditLog.create).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
data: expect.objectContaining({
|
||||
action: 'ERP_SYNC_SKIPPED_BY_FEATURE_FLAG',
|
||||
target_type: 'purchase_request',
|
||||
target_id: 'pr-1',
|
||||
}),
|
||||
}),
|
||||
);
|
||||
expect(result.skipped).toBe(true);
|
||||
});
|
||||
});
|
||||
在新工单中引用
屏蔽一个用户