feat(P19ERP2-1,P19ERP2-2): 实现采购ERP凭证与摘要出站服务
这个提交包含在:
@@ -0,0 +1,704 @@
|
||||
const crypto = require('node:crypto');
|
||||
|
||||
const { enqueueIntegrationEvent } = require('./outbox');
|
||||
|
||||
const PROCUREMENT_TRACK_STATUSES = Object.freeze({
|
||||
notTriggered: 'not_triggered',
|
||||
skipped: 'skipped',
|
||||
});
|
||||
|
||||
const PROCUREMENT_ERP_TARGETS = Object.freeze({
|
||||
commitmentJournal: 'commitment_journal',
|
||||
accrualJournal: 'accrual_journal',
|
||||
archiveSummary: 'archive_summary',
|
||||
});
|
||||
|
||||
function normalizeString(value) {
|
||||
const text = String(value ?? '').trim();
|
||||
return text || null;
|
||||
}
|
||||
|
||||
function toIsoString(value) {
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return new Date(value).toISOString();
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeMoney(value) {
|
||||
if (value == null) {
|
||||
return '0.00';
|
||||
}
|
||||
|
||||
if (typeof value === 'string') {
|
||||
const trimmed = value.trim();
|
||||
return trimmed || '0.00';
|
||||
}
|
||||
|
||||
if (typeof value?.toString === 'function') {
|
||||
return value.toString();
|
||||
}
|
||||
|
||||
return '0.00';
|
||||
}
|
||||
|
||||
function resolveCurrencyCode(purchaseRequest) {
|
||||
return normalizeString(purchaseRequest?.currency_code) ?? 'CNY';
|
||||
}
|
||||
|
||||
function resolveLegalEntityCode(purchaseRequest) {
|
||||
return normalizeString(purchaseRequest?.legal_entity?.code);
|
||||
}
|
||||
|
||||
function resolveCostCenterCode(purchaseRequest) {
|
||||
return normalizeString(purchaseRequest?.cost_center?.code);
|
||||
}
|
||||
|
||||
function resolveSupplierCode(purchaseRequest) {
|
||||
return normalizeString(purchaseRequest?.supplier?.code);
|
||||
}
|
||||
|
||||
function resolveBudgetPoolCode(purchaseRequest) {
|
||||
return normalizeString(purchaseRequest?.budget_pool?.code);
|
||||
}
|
||||
|
||||
function resolveDocumentCategoryCode(purchaseRequest) {
|
||||
return normalizeString(purchaseRequest?.document_category?.code);
|
||||
}
|
||||
|
||||
function stableNormalizeJson(value) {
|
||||
if (Array.isArray(value)) {
|
||||
return value.map((item) => stableNormalizeJson(item));
|
||||
}
|
||||
|
||||
if (value && typeof value === 'object' && value.constructor === Object) {
|
||||
return Object.keys(value)
|
||||
.sort()
|
||||
.reduce((result, key) => {
|
||||
result[key] = stableNormalizeJson(value[key]);
|
||||
return result;
|
||||
}, {});
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
function stableStringifyJson(value) {
|
||||
return JSON.stringify(stableNormalizeJson(value));
|
||||
}
|
||||
|
||||
function hashProcurementPayload(value) {
|
||||
return crypto.createHash('sha256').update(stableStringifyJson(value)).digest('hex');
|
||||
}
|
||||
|
||||
function buildPurchaseCommitmentJournalPayload(input) {
|
||||
const purchaseRequest = input?.purchaseRequest ?? {};
|
||||
|
||||
return {
|
||||
journalType: 'purchase_request_commitment',
|
||||
sourceEventName: 'purchase_request.approved',
|
||||
entityType: 'purchase_request',
|
||||
entityId: purchaseRequest.id ?? null,
|
||||
documentNo: purchaseRequest.request_no ?? null,
|
||||
currencyCode: resolveCurrencyCode(purchaseRequest),
|
||||
totalAmount: normalizeMoney(purchaseRequest.total_amount),
|
||||
occurredAt:
|
||||
toIsoString(input?.occurredAt) ??
|
||||
toIsoString(purchaseRequest.updated_at) ??
|
||||
toIsoString(purchaseRequest.created_at),
|
||||
dimensions: {
|
||||
departmentId: purchaseRequest.department_id ?? null,
|
||||
applicantEmployeeId: purchaseRequest.applicant_id ?? null,
|
||||
submitterEmployeeId: purchaseRequest.submitter_id ?? null,
|
||||
legalEntityCode: resolveLegalEntityCode(purchaseRequest),
|
||||
costCenterCode: resolveCostCenterCode(purchaseRequest),
|
||||
supplierCode: resolveSupplierCode(purchaseRequest),
|
||||
budgetPoolCode: resolveBudgetPoolCode(purchaseRequest),
|
||||
},
|
||||
references: {
|
||||
purchaseRequestId: purchaseRequest.id ?? null,
|
||||
documentCategoryCode: resolveDocumentCategoryCode(purchaseRequest),
|
||||
requestNo: purchaseRequest.request_no ?? null,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function buildPurchaseAccrualJournalPayload(input) {
|
||||
const purchaseRequest = input?.purchaseRequest ?? {};
|
||||
const archive = input?.archive ?? purchaseRequest.order_archive ?? {};
|
||||
|
||||
return {
|
||||
journalType: 'purchase_order_accrual',
|
||||
sourceEventName: 'purchase_order.archived',
|
||||
entityType: 'purchase_order_archive',
|
||||
entityId: archive.id ?? null,
|
||||
documentNo: archive.po_no ?? null,
|
||||
currencyCode: resolveCurrencyCode(purchaseRequest),
|
||||
totalAmount: normalizeMoney(purchaseRequest.total_amount),
|
||||
occurredAt:
|
||||
toIsoString(input?.occurredAt) ??
|
||||
toIsoString(archive.archived_at) ??
|
||||
toIsoString(purchaseRequest.updated_at) ??
|
||||
toIsoString(purchaseRequest.created_at),
|
||||
dimensions: {
|
||||
departmentId: purchaseRequest.department_id ?? null,
|
||||
applicantEmployeeId: purchaseRequest.applicant_id ?? null,
|
||||
submitterEmployeeId: purchaseRequest.submitter_id ?? null,
|
||||
legalEntityCode: resolveLegalEntityCode(purchaseRequest),
|
||||
costCenterCode: resolveCostCenterCode(purchaseRequest),
|
||||
supplierCode: resolveSupplierCode(purchaseRequest),
|
||||
budgetPoolCode: resolveBudgetPoolCode(purchaseRequest),
|
||||
},
|
||||
references: {
|
||||
purchaseRequestId: purchaseRequest.id ?? null,
|
||||
purchaseOrderArchiveId: archive.id ?? null,
|
||||
requestNo: purchaseRequest.request_no ?? null,
|
||||
poNo: archive.po_no ?? null,
|
||||
contractNo: archive.contract_no ?? null,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function buildPurchaseArchiveSummaryPayload(input) {
|
||||
const purchaseRequest = input?.purchaseRequest ?? {};
|
||||
const archive = input?.archive ?? purchaseRequest.order_archive ?? {};
|
||||
const archivedFiles = Array.isArray(archive.archived_files)
|
||||
? archive.archived_files.map((file) => ({
|
||||
storagePath: file?.storagePath ?? null,
|
||||
originalName: file?.originalName ?? null,
|
||||
mimeType: file?.mimeType ?? null,
|
||||
fileSize: Number(file?.fileSize ?? 0),
|
||||
}))
|
||||
: [];
|
||||
|
||||
return {
|
||||
exportType: 'purchase_order_archive',
|
||||
sourceEventName: 'purchase_order.archived',
|
||||
entityType: 'purchase_order_archive',
|
||||
entityId: archive.id ?? null,
|
||||
purchaseRequestId: purchaseRequest.id ?? null,
|
||||
requestNo: purchaseRequest.request_no ?? null,
|
||||
poNo: archive.po_no ?? null,
|
||||
contractNo: archive.contract_no ?? null,
|
||||
currencyCode: resolveCurrencyCode(purchaseRequest),
|
||||
totalAmount: normalizeMoney(purchaseRequest.total_amount),
|
||||
archivedAt:
|
||||
toIsoString(input?.archivedAt) ??
|
||||
toIsoString(archive.archived_at) ??
|
||||
toIsoString(purchaseRequest.updated_at) ??
|
||||
toIsoString(purchaseRequest.created_at),
|
||||
supplier: purchaseRequest.supplier
|
||||
? {
|
||||
id: purchaseRequest.supplier.id,
|
||||
code: purchaseRequest.supplier.code ?? null,
|
||||
name: purchaseRequest.supplier.name ?? null,
|
||||
}
|
||||
: {
|
||||
id: purchaseRequest.supplier_id ?? null,
|
||||
code: null,
|
||||
name: null,
|
||||
},
|
||||
dimensions: {
|
||||
legalEntityCode: resolveLegalEntityCode(purchaseRequest),
|
||||
costCenterCode: resolveCostCenterCode(purchaseRequest),
|
||||
departmentId: purchaseRequest.department_id ?? null,
|
||||
},
|
||||
files: archivedFiles,
|
||||
};
|
||||
}
|
||||
|
||||
function buildPurchaseCommitmentJournalDedupeKey(purchaseRequestId) {
|
||||
return `purchase_request:${purchaseRequestId}:commitment`;
|
||||
}
|
||||
|
||||
function buildPurchaseAccrualJournalDedupeKey(archiveId) {
|
||||
return `purchase_order_archive:${archiveId}:accrual`;
|
||||
}
|
||||
|
||||
function buildPurchaseArchiveSummaryDedupeKey(archiveId) {
|
||||
return `purchase_order_archive:${archiveId}:summary`;
|
||||
}
|
||||
|
||||
function mapIntegrationBinding(record) {
|
||||
if (!record) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
id: record.id,
|
||||
provider: record.provider,
|
||||
entityType: record.entity_type,
|
||||
localId: record.local_id,
|
||||
externalId: record.external_id ?? null,
|
||||
externalCode: record.external_code ?? null,
|
||||
lastSyncedAt: record.last_synced_at?.toISOString?.() ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
function redactBinding(binding, canManage) {
|
||||
if (!binding) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (canManage) {
|
||||
return binding;
|
||||
}
|
||||
|
||||
return {
|
||||
...binding,
|
||||
externalId: null,
|
||||
externalCode: null,
|
||||
};
|
||||
}
|
||||
|
||||
function mapIntegrationOutboxState(record) {
|
||||
if (!record) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
id: record.id,
|
||||
eventType: record.event_type,
|
||||
status: record.status,
|
||||
attempts: Number(record.attempts ?? 0),
|
||||
lastError: record.last_error ?? null,
|
||||
processedAt: record.processed_at?.toISOString?.() ?? null,
|
||||
updatedAt: record.updated_at?.toISOString?.() ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
async function findOutboxByKey(prisma, eventType, dedupeKey) {
|
||||
if (!prisma || !eventType || !dedupeKey) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return prisma.integrationOutbox.findUnique({
|
||||
where: {
|
||||
event_type_dedupe_key: {
|
||||
event_type: eventType,
|
||||
dedupe_key: dedupeKey,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function findBindingByLocalId(prisma, entityType, localId) {
|
||||
if (!prisma || !entityType || !localId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return prisma.integrationBinding.findUnique({
|
||||
where: {
|
||||
provider_entity_type_local_id: {
|
||||
provider: 'erp',
|
||||
entity_type: entityType,
|
||||
local_id: localId,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function enqueuePurchaseCommitmentJournalExport(prisma, config, input) {
|
||||
const payload =
|
||||
input?.payload ??
|
||||
buildPurchaseCommitmentJournalPayload({
|
||||
purchaseRequest: input?.purchaseRequest,
|
||||
occurredAt: input?.occurredAt,
|
||||
});
|
||||
|
||||
const enqueued = await enqueueIntegrationEvent(prisma, {
|
||||
eventType: 'erp.journal.export',
|
||||
payload,
|
||||
dedupeKey: buildPurchaseCommitmentJournalDedupeKey(payload.entityId),
|
||||
maxAttempts: config.integrationOutboxMaxAttempts,
|
||||
actorId: input?.actorId ?? null,
|
||||
requeueOnFailure: input?.requeueOnFailure === true,
|
||||
});
|
||||
|
||||
return {
|
||||
...enqueued,
|
||||
eventType: 'erp.journal.export',
|
||||
erpTarget: PROCUREMENT_ERP_TARGETS.commitmentJournal,
|
||||
payload,
|
||||
};
|
||||
}
|
||||
|
||||
async function enqueuePurchaseAccrualJournalExport(prisma, config, input) {
|
||||
const payload =
|
||||
input?.payload ??
|
||||
buildPurchaseAccrualJournalPayload({
|
||||
purchaseRequest: input?.purchaseRequest,
|
||||
archive: input?.archive,
|
||||
occurredAt: input?.occurredAt,
|
||||
});
|
||||
|
||||
const enqueued = await enqueueIntegrationEvent(prisma, {
|
||||
eventType: 'erp.journal.export',
|
||||
payload,
|
||||
dedupeKey: buildPurchaseAccrualJournalDedupeKey(payload.entityId),
|
||||
maxAttempts: config.integrationOutboxMaxAttempts,
|
||||
actorId: input?.actorId ?? null,
|
||||
requeueOnFailure: input?.requeueOnFailure === true,
|
||||
});
|
||||
|
||||
return {
|
||||
...enqueued,
|
||||
eventType: 'erp.journal.export',
|
||||
erpTarget: PROCUREMENT_ERP_TARGETS.accrualJournal,
|
||||
payload,
|
||||
};
|
||||
}
|
||||
|
||||
async function enqueuePurchaseArchiveSummaryExport(prisma, config, input) {
|
||||
const payload =
|
||||
input?.payload ??
|
||||
buildPurchaseArchiveSummaryPayload({
|
||||
purchaseRequest: input?.purchaseRequest,
|
||||
archive: input?.archive,
|
||||
archivedAt: input?.archivedAt,
|
||||
});
|
||||
|
||||
const enqueued = await enqueueIntegrationEvent(prisma, {
|
||||
eventType: 'erp.procurement.export',
|
||||
payload,
|
||||
dedupeKey: buildPurchaseArchiveSummaryDedupeKey(payload.entityId),
|
||||
maxAttempts: config.integrationOutboxMaxAttempts,
|
||||
actorId: input?.actorId ?? null,
|
||||
requeueOnFailure: input?.requeueOnFailure === true,
|
||||
resetOnPayloadChange: true,
|
||||
});
|
||||
|
||||
return {
|
||||
...enqueued,
|
||||
eventType: 'erp.procurement.export',
|
||||
erpTarget: PROCUREMENT_ERP_TARGETS.archiveSummary,
|
||||
payload,
|
||||
};
|
||||
}
|
||||
|
||||
async function loadPurchaseRequestErpContext(prisma, purchaseRequestId) {
|
||||
const purchaseRequest = await prisma.purchaseRequest.findUnique({
|
||||
where: { id: purchaseRequestId },
|
||||
select: {
|
||||
id: true,
|
||||
request_no: true,
|
||||
applicant_id: true,
|
||||
submitter_id: true,
|
||||
department_id: true,
|
||||
legal_entity_id: true,
|
||||
cost_center_id: true,
|
||||
supplier_id: true,
|
||||
document_category_id: true,
|
||||
budget_pool_id: true,
|
||||
status: true,
|
||||
currency_code: true,
|
||||
total_amount: true,
|
||||
created_at: true,
|
||||
updated_at: true,
|
||||
applicant: {
|
||||
select: {
|
||||
id: true,
|
||||
employee_no: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
submitter: {
|
||||
select: {
|
||||
id: true,
|
||||
employee_no: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
legal_entity: {
|
||||
select: {
|
||||
id: true,
|
||||
code: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
cost_center: {
|
||||
select: {
|
||||
id: true,
|
||||
code: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
supplier: {
|
||||
select: {
|
||||
id: true,
|
||||
code: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
document_category: {
|
||||
select: {
|
||||
id: true,
|
||||
code: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
budget_pool: {
|
||||
select: {
|
||||
id: true,
|
||||
code: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
order_archive: {
|
||||
select: {
|
||||
id: true,
|
||||
po_no: true,
|
||||
contract_no: true,
|
||||
archived_files: true,
|
||||
archived_at: true,
|
||||
archived_by: true,
|
||||
},
|
||||
},
|
||||
workflow_instances: {
|
||||
where: { entity_type: 'purchase_request' },
|
||||
orderBy: { created_at: 'desc' },
|
||||
take: 1,
|
||||
select: { id: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!purchaseRequest) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const workflowInstanceId = purchaseRequest.workflow_instances?.[0]?.id ?? null;
|
||||
const approvalHistory = workflowInstanceId
|
||||
? await prisma.workflowHistory.findFirst({
|
||||
where: {
|
||||
instance_id: workflowInstanceId,
|
||||
action: 'approve',
|
||||
},
|
||||
orderBy: { created_at: 'desc' },
|
||||
select: { created_at: true },
|
||||
})
|
||||
: null;
|
||||
|
||||
return {
|
||||
purchaseRequest,
|
||||
approvalOccurredAt:
|
||||
approvalHistory?.created_at ?? purchaseRequest.updated_at ?? purchaseRequest.created_at ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
function hasAnyBinding(binding) {
|
||||
if (!binding) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return Boolean(binding.externalId || binding.externalCode || binding.lastSyncedAt);
|
||||
}
|
||||
|
||||
function resolveTrackStageStatus({ eligible, outbox, binding, bindings, gateEnabled }) {
|
||||
if (outbox?.status) {
|
||||
return outbox.status;
|
||||
}
|
||||
|
||||
if (binding && hasAnyBinding(binding)) {
|
||||
return 'succeeded';
|
||||
}
|
||||
|
||||
if (bindings && Object.values(bindings).some((item) => hasAnyBinding(item))) {
|
||||
return 'succeeded';
|
||||
}
|
||||
|
||||
if (!eligible) {
|
||||
return PROCUREMENT_TRACK_STATUSES.notTriggered;
|
||||
}
|
||||
|
||||
if (!gateEnabled) {
|
||||
return PROCUREMENT_TRACK_STATUSES.skipped;
|
||||
}
|
||||
|
||||
return PROCUREMENT_TRACK_STATUSES.notTriggered;
|
||||
}
|
||||
|
||||
function buildPurchaseTrackSummary(context) {
|
||||
const purchaseRequest = context?.purchaseRequest ?? {};
|
||||
const archive = purchaseRequest.order_archive ?? null;
|
||||
|
||||
return {
|
||||
id: purchaseRequest.id,
|
||||
requestNo: purchaseRequest.request_no ?? null,
|
||||
status: purchaseRequest.status,
|
||||
totalAmount: normalizeMoney(purchaseRequest.total_amount),
|
||||
supplier: purchaseRequest.supplier
|
||||
? {
|
||||
id: purchaseRequest.supplier.id,
|
||||
code: purchaseRequest.supplier.code ?? null,
|
||||
name: purchaseRequest.supplier.name ?? null,
|
||||
}
|
||||
: null,
|
||||
archiveOrder: archive
|
||||
? {
|
||||
id: archive.id,
|
||||
poNo: archive.po_no,
|
||||
contractNo: archive.contract_no ?? null,
|
||||
archivedAt: toIsoString(archive.archived_at),
|
||||
}
|
||||
: null,
|
||||
};
|
||||
}
|
||||
|
||||
async function buildPurchaseRequestErpTrack(prisma, config, purchaseRequestId, options = {}) {
|
||||
const context = options.context ?? (await loadPurchaseRequestErpContext(prisma, purchaseRequestId));
|
||||
if (!context) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const purchaseRequest = context.purchaseRequest;
|
||||
const archive = purchaseRequest.order_archive ?? null;
|
||||
const canManage = Boolean(options.canManage);
|
||||
const gateEnabled = Boolean(options.gate?.effectiveEnabled ?? options.gate?.enabled ?? true);
|
||||
|
||||
const [
|
||||
commitmentOutboxRow,
|
||||
accrualOutboxRow,
|
||||
summaryOutboxRow,
|
||||
commitmentBindingRow,
|
||||
accrualBindingRow,
|
||||
poBindingRow,
|
||||
contractBindingRow,
|
||||
] = await Promise.all([
|
||||
findOutboxByKey(
|
||||
prisma,
|
||||
'erp.journal.export',
|
||||
buildPurchaseCommitmentJournalDedupeKey(purchaseRequest.id),
|
||||
),
|
||||
archive
|
||||
? findOutboxByKey(
|
||||
prisma,
|
||||
'erp.journal.export',
|
||||
buildPurchaseAccrualJournalDedupeKey(archive.id),
|
||||
)
|
||||
: Promise.resolve(null),
|
||||
archive
|
||||
? findOutboxByKey(
|
||||
prisma,
|
||||
'erp.procurement.export',
|
||||
buildPurchaseArchiveSummaryDedupeKey(archive.id),
|
||||
)
|
||||
: Promise.resolve(null),
|
||||
findBindingByLocalId(prisma, 'purchase_request_commitment_journal', purchaseRequest.id),
|
||||
archive
|
||||
? findBindingByLocalId(prisma, 'purchase_order_accrual_journal', archive.id)
|
||||
: Promise.resolve(null),
|
||||
archive ? findBindingByLocalId(prisma, 'purchase_order_po', archive.id) : Promise.resolve(null),
|
||||
archive
|
||||
? findBindingByLocalId(prisma, 'purchase_order_contract', archive.id)
|
||||
: Promise.resolve(null),
|
||||
]);
|
||||
|
||||
const commitmentPreview = buildPurchaseCommitmentJournalPayload({
|
||||
purchaseRequest,
|
||||
occurredAt: context.approvalOccurredAt,
|
||||
});
|
||||
const accrualPreview = archive
|
||||
? buildPurchaseAccrualJournalPayload({
|
||||
purchaseRequest,
|
||||
archive,
|
||||
occurredAt: archive.archived_at,
|
||||
})
|
||||
: {};
|
||||
const summaryPreview = archive
|
||||
? buildPurchaseArchiveSummaryPayload({
|
||||
purchaseRequest,
|
||||
archive,
|
||||
archivedAt: archive.archived_at,
|
||||
})
|
||||
: {};
|
||||
|
||||
const commitmentBinding = redactBinding(mapIntegrationBinding(commitmentBindingRow), canManage);
|
||||
const accrualBinding = redactBinding(mapIntegrationBinding(accrualBindingRow), canManage);
|
||||
const poBinding = redactBinding(mapIntegrationBinding(poBindingRow), canManage);
|
||||
const contractBinding = redactBinding(mapIntegrationBinding(contractBindingRow), canManage);
|
||||
|
||||
const commitmentOutbox = mapIntegrationOutboxState(commitmentOutboxRow);
|
||||
const accrualOutbox = mapIntegrationOutboxState(accrualOutboxRow);
|
||||
const summaryOutbox = mapIntegrationOutboxState(summaryOutboxRow);
|
||||
|
||||
const commitmentEligible = purchaseRequest.status === 'approved';
|
||||
const archiveEligible = Boolean(archive);
|
||||
|
||||
return {
|
||||
purchaseRequest: buildPurchaseTrackSummary(context),
|
||||
stages: {
|
||||
commitmentJournal: {
|
||||
eligible: commitmentEligible,
|
||||
reason: commitmentEligible ? null : 'purchase_request_not_approved',
|
||||
status: resolveTrackStageStatus({
|
||||
eligible: commitmentEligible,
|
||||
outbox: commitmentOutbox,
|
||||
binding: commitmentBinding,
|
||||
gateEnabled,
|
||||
}),
|
||||
preview: commitmentPreview,
|
||||
outbox: commitmentOutbox,
|
||||
binding: commitmentBinding,
|
||||
},
|
||||
accrualJournal: {
|
||||
eligible: archiveEligible,
|
||||
reason: archiveEligible ? null : 'purchase_order_archive_missing',
|
||||
status: resolveTrackStageStatus({
|
||||
eligible: archiveEligible,
|
||||
outbox: accrualOutbox,
|
||||
binding: accrualBinding,
|
||||
gateEnabled,
|
||||
}),
|
||||
preview: accrualPreview,
|
||||
outbox: accrualOutbox,
|
||||
binding: accrualBinding,
|
||||
},
|
||||
archiveSummary: {
|
||||
eligible: archiveEligible,
|
||||
reason: archiveEligible ? null : 'purchase_order_archive_missing',
|
||||
status: resolveTrackStageStatus({
|
||||
eligible: archiveEligible,
|
||||
outbox: summaryOutbox,
|
||||
bindings: {
|
||||
po: poBinding,
|
||||
contract: contractBinding,
|
||||
},
|
||||
gateEnabled,
|
||||
}),
|
||||
preview: summaryPreview,
|
||||
outbox: summaryOutbox,
|
||||
bindings: {
|
||||
po: poBinding,
|
||||
contract: contractBinding,
|
||||
},
|
||||
},
|
||||
},
|
||||
permissions: {
|
||||
canRetry: Boolean(canManage && gateEnabled),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
PROCUREMENT_ERP_TARGETS,
|
||||
PROCUREMENT_TRACK_STATUSES,
|
||||
buildPurchaseAccrualJournalDedupeKey,
|
||||
buildPurchaseAccrualJournalPayload,
|
||||
buildPurchaseArchiveSummaryDedupeKey,
|
||||
buildPurchaseArchiveSummaryPayload,
|
||||
buildPurchaseCommitmentJournalDedupeKey,
|
||||
buildPurchaseCommitmentJournalPayload,
|
||||
buildPurchaseRequestErpTrack,
|
||||
enqueuePurchaseAccrualJournalExport,
|
||||
enqueuePurchaseArchiveSummaryExport,
|
||||
enqueuePurchaseCommitmentJournalExport,
|
||||
hashProcurementPayload,
|
||||
loadPurchaseRequestErpContext,
|
||||
stableStringifyJson,
|
||||
};
|
||||
在新工单中引用
屏蔽一个用户