349 行
11 KiB
JavaScript
349 行
11 KiB
JavaScript
const { enqueueIntegrationEvent } = require('./outbox');
|
|
|
|
const ERP_JOURNAL_EVENT_TYPES = Object.freeze(['erp.journal.export', 'erp.reimbursement.paid']);
|
|
const REIMBURSEMENT_JOURNAL_DEDUPE_PREFIX = 'reimbursement';
|
|
const TRAVEL_ORDER_JOURNAL_DEDUPE_PREFIX = 'travel_order';
|
|
|
|
function toDateOnly(value) {
|
|
if (!value) {
|
|
return null;
|
|
}
|
|
try {
|
|
return new Date(value).toISOString().slice(0, 10);
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function toIsoString(value) {
|
|
if (!value) {
|
|
return null;
|
|
}
|
|
try {
|
|
return new Date(value).toISOString();
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function resolveReimbursementLegalEntityCode(reimbursement) {
|
|
return reimbursement?.owner?.legal_entity?.code ?? reimbursement?.submitter?.legal_entity?.code ?? null;
|
|
}
|
|
|
|
function resolveTravelRequestLegalEntityCode(travelOrder) {
|
|
return travelOrder?.travel_request?.legal_entity?.code ?? null;
|
|
}
|
|
|
|
function resolveTravelRequestCostCenterCode(travelOrder) {
|
|
return travelOrder?.travel_request?.cost_center?.code ?? null;
|
|
}
|
|
|
|
function resolveTravelRequestBudgetPoolCode(travelOrder) {
|
|
return travelOrder?.travel_request?.budget_pool?.code ?? null;
|
|
}
|
|
|
|
function buildReimbursementJournalPayload(input) {
|
|
const reimbursement = input?.reimbursement ?? {};
|
|
const occurredAt =
|
|
toIsoString(reimbursement.paid_at) ??
|
|
(reimbursement.payment_date ? `${toDateOnly(reimbursement.payment_date)}T00:00:00.000Z` : null);
|
|
|
|
return {
|
|
journalType: 'reimbursement_paid',
|
|
sourceEventName: 'reimbursement.paid',
|
|
entityType: 'reimbursement_form',
|
|
entityId: reimbursement.id,
|
|
documentNo: reimbursement.form_number ?? null,
|
|
currencyCode: 'CNY',
|
|
totalAmount: reimbursement.total_amount?.toString?.() ?? null,
|
|
occurredAt,
|
|
paymentVoucherNo: reimbursement.payment_voucher_no ?? null,
|
|
paymentDate: toDateOnly(reimbursement.payment_date),
|
|
dimensions: {
|
|
departmentId: reimbursement.department_id ?? null,
|
|
ownerEmployeeId: reimbursement.owner_id ?? null,
|
|
submitterEmployeeId: reimbursement.submitter_id ?? null,
|
|
legalEntityCode: resolveReimbursementLegalEntityCode(reimbursement),
|
|
costCenterCode: null,
|
|
},
|
|
reimbursementId: reimbursement.id,
|
|
formNumber: reimbursement.form_number ?? null,
|
|
};
|
|
}
|
|
|
|
function buildTravelOrderPaymentJournalPayload(input) {
|
|
const travelOrder = input?.travelOrder ?? {};
|
|
const travelRequest = travelOrder?.travel_request ?? {};
|
|
const occurredAt =
|
|
toIsoString(travelOrder.paid_at) ??
|
|
(travelOrder.payment_date ? `${toDateOnly(travelOrder.payment_date)}T00:00:00.000Z` : null) ??
|
|
toIsoString(travelOrder.updated_at) ??
|
|
toIsoString(travelOrder.created_at);
|
|
|
|
return {
|
|
journalType: 'travel_order_payment',
|
|
sourceEventName: 'travel_order.paid',
|
|
entityType: 'travel_order',
|
|
entityId: travelOrder.id ?? null,
|
|
documentNo: travelOrder.external_order_no ?? null,
|
|
currencyCode: travelOrder.currency ?? 'CNY',
|
|
totalAmount: travelOrder.amount?.toString?.() ?? null,
|
|
occurredAt,
|
|
paymentVoucherNo: travelOrder.payment_voucher_no ?? null,
|
|
paymentDate: toDateOnly(travelOrder.payment_date),
|
|
paymentChannel: travelOrder.payment_channel ?? null,
|
|
dimensions: {
|
|
departmentId: travelOrder.department_id ?? travelRequest.department_id ?? null,
|
|
employeeId: travelOrder.employee_id ?? null,
|
|
applicantEmployeeId: travelRequest.applicant_employee_id ?? null,
|
|
submitterEmployeeId: travelRequest.submitter_id ?? null,
|
|
legalEntityCode: resolveTravelRequestLegalEntityCode(travelOrder),
|
|
costCenterCode: resolveTravelRequestCostCenterCode(travelOrder),
|
|
budgetPoolCode: resolveTravelRequestBudgetPoolCode(travelOrder),
|
|
},
|
|
references: {
|
|
travelOrderId: travelOrder.id ?? null,
|
|
travelRequestId: travelOrder.travel_request_id ?? null,
|
|
requestNo: travelRequest.request_no ?? null,
|
|
externalOrderNo: travelOrder.external_order_no ?? null,
|
|
source: travelOrder.source ?? null,
|
|
linkedReimbursementId: travelOrder.linked_reimbursement_id ?? null,
|
|
},
|
|
};
|
|
}
|
|
|
|
function buildReimbursementJournalDedupeKey(reimbursementId) {
|
|
return `${REIMBURSEMENT_JOURNAL_DEDUPE_PREFIX}:${reimbursementId}:paid`;
|
|
}
|
|
|
|
function buildTravelOrderPaymentJournalDedupeKey(travelOrderId) {
|
|
return `${TRAVEL_ORDER_JOURNAL_DEDUPE_PREFIX}:${travelOrderId}:paid`;
|
|
}
|
|
|
|
async function enqueueReimbursementJournalExport(prisma, config, input) {
|
|
const payload = input?.payload ?? buildReimbursementJournalPayload({ reimbursement: input?.reimbursement });
|
|
|
|
const enqueued = await enqueueIntegrationEvent(prisma, {
|
|
eventType: 'erp.journal.export',
|
|
payload,
|
|
dedupeKey: buildReimbursementJournalDedupeKey(payload.entityId),
|
|
maxAttempts: config.integrationOutboxMaxAttempts,
|
|
actorId: input?.actorId ?? null,
|
|
requeueOnFailure: true,
|
|
});
|
|
|
|
return {
|
|
...enqueued,
|
|
eventType: 'erp.journal.export',
|
|
payload,
|
|
};
|
|
}
|
|
|
|
async function enqueueTravelOrderPaymentJournalExport(prisma, config, input) {
|
|
const payload = input?.payload ?? buildTravelOrderPaymentJournalPayload({ travelOrder: input?.travelOrder });
|
|
|
|
const enqueued = await enqueueIntegrationEvent(prisma, {
|
|
eventType: 'erp.journal.export',
|
|
payload,
|
|
dedupeKey: buildTravelOrderPaymentJournalDedupeKey(payload.entityId),
|
|
maxAttempts: config.integrationOutboxMaxAttempts,
|
|
actorId: input?.actorId ?? null,
|
|
requeueOnFailure: true,
|
|
});
|
|
|
|
return {
|
|
...enqueued,
|
|
eventType: 'erp.journal.export',
|
|
payload,
|
|
};
|
|
}
|
|
|
|
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 mapIntegrationOutboxState(record) {
|
|
if (!record) {
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
id: record.id,
|
|
eventType: record.event_type,
|
|
status: record.status,
|
|
lastError: record.last_error ?? null,
|
|
processedAt: record.processed_at?.toISOString?.() ?? null,
|
|
updatedAt: record.updated_at?.toISOString?.() ?? null,
|
|
};
|
|
}
|
|
|
|
async function findLatestReimbursementJournalOutbox(prisma, reimbursementId) {
|
|
const current = await prisma.integrationOutbox.findFirst({
|
|
where: {
|
|
event_type: 'erp.journal.export',
|
|
dedupe_key: buildReimbursementJournalDedupeKey(reimbursementId),
|
|
},
|
|
orderBy: { updated_at: 'desc' },
|
|
});
|
|
|
|
if (current) {
|
|
return current;
|
|
}
|
|
|
|
return prisma.integrationOutbox.findFirst({
|
|
where: {
|
|
event_type: 'erp.reimbursement.paid',
|
|
dedupe_key: reimbursementId,
|
|
},
|
|
orderBy: { updated_at: 'desc' },
|
|
});
|
|
}
|
|
|
|
async function findLatestTravelOrderPaymentJournalOutbox(prisma, travelOrderId) {
|
|
return prisma.integrationOutbox.findFirst({
|
|
where: {
|
|
event_type: 'erp.journal.export',
|
|
dedupe_key: buildTravelOrderPaymentJournalDedupeKey(travelOrderId),
|
|
},
|
|
orderBy: { updated_at: 'desc' },
|
|
});
|
|
}
|
|
|
|
async function buildReimbursementJournalPreview(prisma, reimbursement) {
|
|
const [binding, outbox] = await Promise.all([
|
|
prisma.integrationBinding.findUnique({
|
|
where: {
|
|
provider_entity_type_local_id: {
|
|
provider: 'erp',
|
|
entity_type: 'reimbursement_journal',
|
|
local_id: reimbursement.id,
|
|
},
|
|
},
|
|
}),
|
|
findLatestReimbursementJournalOutbox(prisma, reimbursement.id),
|
|
]);
|
|
|
|
const journal = buildReimbursementJournalPayload({ reimbursement });
|
|
const exportState = {
|
|
externalId: binding?.external_id ?? null,
|
|
lastOutboxId: outbox?.id ?? null,
|
|
lastOutboxStatus: outbox?.status ?? null,
|
|
lastError: outbox?.last_error ?? null,
|
|
exportedAt: binding?.last_synced_at?.toISOString?.() ?? outbox?.processed_at?.toISOString?.() ?? null,
|
|
};
|
|
|
|
return {
|
|
journal,
|
|
exportState,
|
|
binding: mapIntegrationBinding(binding),
|
|
outbox: mapIntegrationOutboxState(outbox),
|
|
};
|
|
}
|
|
|
|
async function buildTravelOrderPaymentJournalPreview(prisma, travelOrderInput) {
|
|
const travelOrder = travelOrderInput?.travel_request
|
|
? travelOrderInput
|
|
: await prisma.travelOrder.findUnique({
|
|
where: { id: travelOrderInput.id },
|
|
select: {
|
|
id: true,
|
|
source: true,
|
|
external_order_no: true,
|
|
employee_id: true,
|
|
department_id: true,
|
|
linked_reimbursement_id: true,
|
|
travel_request_id: true,
|
|
amount: true,
|
|
currency: true,
|
|
payment_voucher_no: true,
|
|
payment_date: true,
|
|
payment_channel: true,
|
|
paid_at: true,
|
|
created_at: true,
|
|
updated_at: true,
|
|
travel_request: {
|
|
select: {
|
|
id: true,
|
|
request_no: true,
|
|
department_id: true,
|
|
applicant_employee_id: true,
|
|
submitter_id: true,
|
|
legal_entity: { select: { code: true } },
|
|
cost_center: { select: { code: true } },
|
|
budget_pool: { select: { code: true } },
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
if (!travelOrder) {
|
|
return {
|
|
journal: null,
|
|
exportState: {
|
|
externalId: null,
|
|
lastOutboxId: null,
|
|
lastOutboxStatus: null,
|
|
lastError: null,
|
|
exportedAt: null,
|
|
},
|
|
binding: null,
|
|
outbox: null,
|
|
};
|
|
}
|
|
|
|
const [binding, outbox] = await Promise.all([
|
|
prisma.integrationBinding.findUnique({
|
|
where: {
|
|
provider_entity_type_local_id: {
|
|
provider: 'erp',
|
|
entity_type: 'travel_order_payment_journal',
|
|
local_id: travelOrder.id,
|
|
},
|
|
},
|
|
}),
|
|
findLatestTravelOrderPaymentJournalOutbox(prisma, travelOrder.id),
|
|
]);
|
|
|
|
const journal = buildTravelOrderPaymentJournalPayload({ travelOrder });
|
|
const exportState = {
|
|
externalId: binding?.external_id ?? null,
|
|
lastOutboxId: outbox?.id ?? null,
|
|
lastOutboxStatus: outbox?.status ?? null,
|
|
lastError: outbox?.last_error ?? null,
|
|
exportedAt: binding?.last_synced_at?.toISOString?.() ?? outbox?.processed_at?.toISOString?.() ?? null,
|
|
};
|
|
|
|
return {
|
|
journal,
|
|
exportState,
|
|
binding: mapIntegrationBinding(binding),
|
|
outbox: mapIntegrationOutboxState(outbox),
|
|
};
|
|
}
|
|
|
|
module.exports = {
|
|
ERP_JOURNAL_EVENT_TYPES,
|
|
buildReimbursementJournalDedupeKey,
|
|
buildReimbursementJournalPayload,
|
|
buildReimbursementJournalPreview,
|
|
buildTravelOrderPaymentJournalDedupeKey,
|
|
buildTravelOrderPaymentJournalPayload,
|
|
buildTravelOrderPaymentJournalPreview,
|
|
enqueueReimbursementJournalExport,
|
|
enqueueTravelOrderPaymentJournalExport,
|
|
findLatestReimbursementJournalOutbox,
|
|
findLatestTravelOrderPaymentJournalOutbox,
|
|
};
|