726 行
20 KiB
JavaScript
726 行
20 KiB
JavaScript
const { AppError } = require("../utils/AppError");
|
|
const { toMoneyString } = require("../utils/workflow");
|
|
const { summarizeBudgetLedgerByPool } = require("./purchaseBudget");
|
|
const { buildTravelOrderPaymentJournalPreview } = require("./integrations/erpJournal");
|
|
|
|
function buildTravelPaymentValidationError(details, statusCode = 409) {
|
|
return new AppError({
|
|
code: "VALID_001",
|
|
message: "参数校验失败",
|
|
statusCode,
|
|
details,
|
|
});
|
|
}
|
|
|
|
function toMoneyNumber(value) {
|
|
const numeric = Number(value ?? 0);
|
|
if (!Number.isFinite(numeric)) {
|
|
return 0;
|
|
}
|
|
return Number(numeric.toFixed(2));
|
|
}
|
|
|
|
function toMoneyFixed(value) {
|
|
return toMoneyNumber(value).toFixed(2);
|
|
}
|
|
|
|
function appendCorporateSummary(summary, entry) {
|
|
const poolId = entry?.budget_pool_id;
|
|
if (!poolId) {
|
|
return;
|
|
}
|
|
|
|
if (!summary.has(poolId)) {
|
|
summary.set(poolId, {
|
|
budgetPoolId: poolId,
|
|
authorize: 0,
|
|
settle: 0,
|
|
reverse: 0,
|
|
});
|
|
}
|
|
|
|
const current = summary.get(poolId);
|
|
const amount = toMoneyNumber(entry.amount);
|
|
if (entry.kind === "authorize") {
|
|
current.authorize = toMoneyNumber(current.authorize + amount);
|
|
} else if (entry.kind === "settle") {
|
|
current.settle = toMoneyNumber(current.settle + amount);
|
|
} else if (entry.kind === "reverse") {
|
|
current.reverse = toMoneyNumber(current.reverse + amount);
|
|
}
|
|
}
|
|
|
|
function summarizeCorporateBudgetEntries(entries = []) {
|
|
const summary = new Map();
|
|
|
|
for (const entry of entries) {
|
|
appendCorporateSummary(summary, entry);
|
|
}
|
|
|
|
return Array.from(summary.values()).map((item) => ({
|
|
...item,
|
|
outstanding: toMoneyNumber(Math.max(0, item.authorize - item.reverse)),
|
|
settlePending: toMoneyNumber(Math.max(0, item.authorize - item.reverse - item.settle)),
|
|
}));
|
|
}
|
|
|
|
async function listEntityBudgetEntries(tx, { entityType, entityId }) {
|
|
return tx.budgetLedgerEntry.findMany({
|
|
where: {
|
|
entity_type: entityType,
|
|
entity_id: entityId,
|
|
},
|
|
orderBy: [{ created_at: "asc" }, { id: "asc" }],
|
|
select: {
|
|
id: true,
|
|
budget_pool_id: true,
|
|
entity_type: true,
|
|
entity_id: true,
|
|
kind: true,
|
|
amount: true,
|
|
created_at: true,
|
|
},
|
|
});
|
|
}
|
|
|
|
async function listCorporateBudgetEntries(tx, { authorizationIds = [], transactionIds = [] } = {}) {
|
|
const filters = [];
|
|
|
|
if (authorizationIds.length) {
|
|
filters.push({
|
|
entity_type: "card_authorization",
|
|
entity_id: { in: authorizationIds },
|
|
});
|
|
}
|
|
|
|
if (transactionIds.length) {
|
|
filters.push({
|
|
entity_type: "card_transaction",
|
|
entity_id: { in: transactionIds },
|
|
});
|
|
}
|
|
|
|
if (!filters.length) {
|
|
return [];
|
|
}
|
|
|
|
return tx.budgetLedgerEntry.findMany({
|
|
where: { OR: filters },
|
|
orderBy: [{ created_at: "asc" }, { id: "asc" }],
|
|
select: {
|
|
id: true,
|
|
budget_pool_id: true,
|
|
entity_type: true,
|
|
entity_id: true,
|
|
kind: true,
|
|
amount: true,
|
|
created_at: true,
|
|
},
|
|
});
|
|
}
|
|
|
|
async function appendBudgetLedgerEntry(tx, {
|
|
budgetPoolId,
|
|
entityType,
|
|
entityId,
|
|
kind,
|
|
amount,
|
|
actorId,
|
|
note,
|
|
}) {
|
|
return tx.budgetLedgerEntry.create({
|
|
data: {
|
|
budget_pool_id: budgetPoolId,
|
|
entity_type: entityType,
|
|
entity_id: entityId,
|
|
kind,
|
|
amount: toMoneyFixed(amount),
|
|
note,
|
|
created_by: actorId,
|
|
},
|
|
});
|
|
}
|
|
|
|
async function applyBudgetPoolUsageDelta(tx, { budgetPoolId, deltaAmount, actorId }) {
|
|
if (!budgetPoolId || !deltaAmount) {
|
|
return;
|
|
}
|
|
|
|
const budgetPool = await tx.budgetPool.findUnique({
|
|
where: { id: budgetPoolId },
|
|
select: { id: true, used_amount: true },
|
|
});
|
|
|
|
if (!budgetPool) {
|
|
throw buildTravelPaymentValidationError({ budgetPoolId: "not_found" });
|
|
}
|
|
|
|
const nextUsedAmount = Math.max(0, toMoneyNumber(budgetPool.used_amount) + toMoneyNumber(deltaAmount));
|
|
await tx.budgetPool.update({
|
|
where: { id: budgetPoolId },
|
|
data: {
|
|
used_amount: toMoneyFixed(nextUsedAmount),
|
|
updated_by: actorId,
|
|
},
|
|
});
|
|
}
|
|
|
|
async function authorizeBudgetForEntity(tx, {
|
|
budgetPoolId,
|
|
entityId,
|
|
amount,
|
|
actorId,
|
|
note,
|
|
}) {
|
|
if (!budgetPoolId) {
|
|
return { applied: false, authorizedAmount: "0.00", duplicated: false };
|
|
}
|
|
|
|
const normalizedAmount = toMoneyNumber(amount);
|
|
if (normalizedAmount <= 0) {
|
|
return { applied: false, authorizedAmount: "0.00", duplicated: false };
|
|
}
|
|
|
|
const existingEntries = await listEntityBudgetEntries(tx, {
|
|
entityType: "card_authorization",
|
|
entityId,
|
|
});
|
|
|
|
if (existingEntries.some((item) => item.kind === "authorize")) {
|
|
const current = summarizeCorporateBudgetEntries(existingEntries)[0];
|
|
return {
|
|
applied: false,
|
|
duplicated: true,
|
|
authorizedAmount: toMoneyFixed(current?.authorize ?? normalizedAmount),
|
|
};
|
|
}
|
|
|
|
await appendBudgetLedgerEntry(tx, {
|
|
budgetPoolId,
|
|
entityType: "card_authorization",
|
|
entityId,
|
|
kind: "authorize",
|
|
amount: normalizedAmount,
|
|
actorId,
|
|
note,
|
|
});
|
|
|
|
await applyBudgetPoolUsageDelta(tx, {
|
|
budgetPoolId,
|
|
deltaAmount: normalizedAmount,
|
|
actorId,
|
|
});
|
|
|
|
return {
|
|
applied: true,
|
|
duplicated: false,
|
|
authorizedAmount: toMoneyFixed(normalizedAmount),
|
|
};
|
|
}
|
|
|
|
async function settleBudgetForEntity(tx, {
|
|
budgetPoolId,
|
|
entityId,
|
|
amount,
|
|
actorId,
|
|
note,
|
|
}) {
|
|
if (!budgetPoolId) {
|
|
return { applied: false, settledAmount: "0.00", duplicated: false };
|
|
}
|
|
|
|
const normalizedAmount = toMoneyNumber(amount);
|
|
if (normalizedAmount <= 0) {
|
|
return { applied: false, settledAmount: "0.00", duplicated: false };
|
|
}
|
|
|
|
const existingEntries = await listEntityBudgetEntries(tx, {
|
|
entityType: "card_transaction",
|
|
entityId,
|
|
});
|
|
|
|
if (existingEntries.some((item) => item.kind === "settle")) {
|
|
const current = summarizeCorporateBudgetEntries(existingEntries)[0];
|
|
return {
|
|
applied: false,
|
|
duplicated: true,
|
|
settledAmount: toMoneyFixed(current?.settle ?? normalizedAmount),
|
|
};
|
|
}
|
|
|
|
await appendBudgetLedgerEntry(tx, {
|
|
budgetPoolId,
|
|
entityType: "card_transaction",
|
|
entityId,
|
|
kind: "settle",
|
|
amount: normalizedAmount,
|
|
actorId,
|
|
note,
|
|
});
|
|
|
|
return {
|
|
applied: true,
|
|
duplicated: false,
|
|
settledAmount: toMoneyFixed(normalizedAmount),
|
|
};
|
|
}
|
|
|
|
async function reverseBudgetForEntity(tx, {
|
|
budgetPoolId,
|
|
entityId,
|
|
amount,
|
|
actorId,
|
|
note,
|
|
}) {
|
|
if (!budgetPoolId) {
|
|
return { applied: false, reversedAmount: "0.00", duplicated: false };
|
|
}
|
|
|
|
const normalizedAmount = toMoneyNumber(amount);
|
|
if (normalizedAmount <= 0) {
|
|
return { applied: false, reversedAmount: "0.00", duplicated: false };
|
|
}
|
|
|
|
const existingEntries = await listEntityBudgetEntries(tx, {
|
|
entityType: "card_transaction",
|
|
entityId,
|
|
});
|
|
|
|
if (existingEntries.some((item) => item.kind === "reverse")) {
|
|
const current = summarizeCorporateBudgetEntries(existingEntries)[0];
|
|
return {
|
|
applied: false,
|
|
duplicated: true,
|
|
reversedAmount: toMoneyFixed(current?.reverse ?? normalizedAmount),
|
|
};
|
|
}
|
|
|
|
await appendBudgetLedgerEntry(tx, {
|
|
budgetPoolId,
|
|
entityType: "card_transaction",
|
|
entityId,
|
|
kind: "reverse",
|
|
amount: normalizedAmount,
|
|
actorId,
|
|
note,
|
|
});
|
|
|
|
await applyBudgetPoolUsageDelta(tx, {
|
|
budgetPoolId,
|
|
deltaAmount: -normalizedAmount,
|
|
actorId,
|
|
});
|
|
|
|
return {
|
|
applied: true,
|
|
duplicated: false,
|
|
reversedAmount: toMoneyFixed(normalizedAmount),
|
|
};
|
|
}
|
|
|
|
function mapBudgetPoolSummary(summaryItems, budgetPoolMap, mode) {
|
|
return summaryItems.map((item) => {
|
|
const pool = budgetPoolMap.get(item.budgetPoolId);
|
|
const base = {
|
|
budgetPoolId: item.budgetPoolId,
|
|
budgetPoolCode: pool?.code ?? null,
|
|
budgetPoolName: pool?.name ?? null,
|
|
outstanding: toMoneyFixed(item.outstanding),
|
|
};
|
|
|
|
if (mode === "request") {
|
|
return {
|
|
...base,
|
|
reserve: toMoneyFixed(item.reserve),
|
|
confirm: toMoneyFixed(item.confirm),
|
|
release: toMoneyFixed(item.release),
|
|
confirmPending: toMoneyFixed(item.confirmPending),
|
|
};
|
|
}
|
|
|
|
return {
|
|
...base,
|
|
authorize: toMoneyFixed(item.authorize),
|
|
settle: toMoneyFixed(item.settle),
|
|
reverse: toMoneyFixed(item.reverse),
|
|
settlePending: toMoneyFixed(item.settlePending),
|
|
};
|
|
});
|
|
}
|
|
|
|
function buildTransactionAnomalies(transaction, order) {
|
|
const anomalies = [];
|
|
|
|
if (!transaction.travel_order_id) {
|
|
anomalies.push({
|
|
code: "missing_order",
|
|
level: "medium",
|
|
title: "未匹配差旅订单",
|
|
detail: `交易 ${transaction.external_txn_id} 尚未关联 travel_order。`,
|
|
transactionId: transaction.id,
|
|
travelOrderId: null,
|
|
});
|
|
}
|
|
|
|
if (order) {
|
|
if (String(order.currency ?? "") !== String(transaction.currency ?? "")) {
|
|
anomalies.push({
|
|
code: "currency_mismatch",
|
|
level: "high",
|
|
title: "币种不一致",
|
|
detail: `订单 ${order.external_order_no} 与交易 ${transaction.external_txn_id} 币种不一致。`,
|
|
transactionId: transaction.id,
|
|
travelOrderId: order.id,
|
|
});
|
|
}
|
|
|
|
if (toMoneyFixed(order.amount) !== toMoneyFixed(transaction.amount)) {
|
|
anomalies.push({
|
|
code: "amount_mismatch",
|
|
level: "high",
|
|
title: "金额不一致",
|
|
detail: `订单 ${order.external_order_no} 与交易 ${transaction.external_txn_id} 金额不一致。`,
|
|
transactionId: transaction.id,
|
|
travelOrderId: order.id,
|
|
});
|
|
}
|
|
}
|
|
|
|
if (transaction.freeze_reason_code) {
|
|
anomalies.push({
|
|
code: transaction.freeze_reason_code,
|
|
level: "medium",
|
|
title: "已标记人工冻结建议",
|
|
detail: transaction.freeze_reason_note || `交易 ${transaction.external_txn_id} 已被标记人工复核。`,
|
|
transactionId: transaction.id,
|
|
travelOrderId: transaction.travel_order_id ?? null,
|
|
});
|
|
}
|
|
|
|
return anomalies;
|
|
}
|
|
|
|
function isOrderAmountAndCurrencyMatched(order, transaction) {
|
|
if (!order || !transaction) {
|
|
return false;
|
|
}
|
|
|
|
return (
|
|
toMoneyFixed(order.amount) === toMoneyFixed(transaction.amount) &&
|
|
String(order.currency ?? "").toUpperCase() === String(transaction.currency ?? "").toUpperCase()
|
|
);
|
|
}
|
|
|
|
function mapAuthorization(item) {
|
|
return {
|
|
id: item.id,
|
|
source: item.source,
|
|
externalAuthNo: item.external_auth_no,
|
|
employeeId: item.employee_id,
|
|
departmentId: item.department_id ?? null,
|
|
travelRequestId: item.travel_request_id,
|
|
budgetPoolId: item.budget_pool_id ?? null,
|
|
legalEntityId: item.legal_entity_id,
|
|
costCenterId: item.cost_center_id,
|
|
authorizedAmount: toMoneyFixed(item.authorized_amount),
|
|
settledAmount: toMoneyFixed(item.settled_amount),
|
|
reversedAmount: toMoneyFixed(item.reversed_amount),
|
|
currency: item.currency,
|
|
occurredAt: item.occurred_at?.toISOString?.() ?? null,
|
|
expiresAt: item.expires_at?.toISOString?.() ?? null,
|
|
status: item.status,
|
|
createdAt: item.created_at?.toISOString?.() ?? null,
|
|
updatedAt: item.updated_at?.toISOString?.() ?? null,
|
|
};
|
|
}
|
|
|
|
function mapTransaction(item, ordersById) {
|
|
const order = item.travel_order_id ? ordersById.get(item.travel_order_id) ?? null : null;
|
|
|
|
return {
|
|
id: item.id,
|
|
source: item.source,
|
|
externalTxnId: item.external_txn_id,
|
|
cardAuthorizationId: item.card_authorization_id ?? null,
|
|
employeeId: item.employee_id,
|
|
departmentId: item.department_id ?? null,
|
|
travelRequestId: item.travel_request_id,
|
|
travelOrderId: item.travel_order_id ?? null,
|
|
amount: toMoneyFixed(item.amount),
|
|
currency: item.currency,
|
|
txnType: item.txn_type,
|
|
status: item.status,
|
|
occurredAt: item.occurred_at?.toISOString?.() ?? null,
|
|
freezeSuggestedAt: item.freeze_suggested_at?.toISOString?.() ?? null,
|
|
freezeSuggestedBy: item.freeze_suggested_by ?? null,
|
|
freezeReasonCode: item.freeze_reason_code ?? null,
|
|
freezeReasonNote: item.freeze_reason_note ?? null,
|
|
matchedOrder: order
|
|
? {
|
|
id: order.id,
|
|
externalOrderNo: order.external_order_no,
|
|
status: order.status,
|
|
amount: toMoneyFixed(order.amount),
|
|
currency: order.currency,
|
|
exactMatch: isOrderAmountAndCurrencyMatched(order, item),
|
|
}
|
|
: null,
|
|
};
|
|
}
|
|
|
|
function mapInvoice(item) {
|
|
return {
|
|
id: item.id,
|
|
status: item.status,
|
|
originalName: item.original_name ?? null,
|
|
totalAmount: toMoneyString(item.total_amount),
|
|
invoiceCode: item.invoice_code ?? null,
|
|
invoiceNumber: item.invoice_number ?? null,
|
|
invoiceDate: item.invoice_date?.toISOString?.()?.slice(0, 10) ?? null,
|
|
linkedReimbursementId: item.linked_reimbursement_id ?? null,
|
|
linkedTravelOrderId: item.travel_order_id ?? null,
|
|
};
|
|
}
|
|
|
|
async function buildTravelPaymentTrack(prisma, travelRequestId, { canManagePayments = false } = {}) {
|
|
const request = await prisma.travelRequest.findUnique({
|
|
where: { id: travelRequestId },
|
|
select: {
|
|
id: true,
|
|
request_no: true,
|
|
budget_pool_id: true,
|
|
travel_orders: {
|
|
orderBy: [{ occurred_at: "desc" }, { created_at: "desc" }],
|
|
select: {
|
|
id: true,
|
|
source: true,
|
|
external_order_no: true,
|
|
employee_id: true,
|
|
department_id: true,
|
|
linked_reimbursement_id: true,
|
|
travel_request_id: true,
|
|
occurred_at: true,
|
|
amount: true,
|
|
currency: true,
|
|
status: true,
|
|
payment_voucher_no: true,
|
|
payment_date: true,
|
|
payment_channel: true,
|
|
paid_at: true,
|
|
paid_by: true,
|
|
created_at: true,
|
|
updated_at: true,
|
|
},
|
|
},
|
|
card_authorizations: {
|
|
orderBy: [{ occurred_at: "desc" }, { created_at: "desc" }],
|
|
select: {
|
|
id: true,
|
|
source: true,
|
|
external_auth_no: true,
|
|
employee_id: true,
|
|
department_id: true,
|
|
travel_request_id: true,
|
|
budget_pool_id: true,
|
|
legal_entity_id: true,
|
|
cost_center_id: true,
|
|
authorized_amount: true,
|
|
settled_amount: true,
|
|
reversed_amount: true,
|
|
currency: true,
|
|
occurred_at: true,
|
|
expires_at: true,
|
|
status: true,
|
|
created_at: true,
|
|
updated_at: true,
|
|
},
|
|
},
|
|
card_transactions: {
|
|
orderBy: [{ occurred_at: "desc" }, { created_at: "desc" }],
|
|
select: {
|
|
id: true,
|
|
source: true,
|
|
external_txn_id: true,
|
|
card_authorization_id: true,
|
|
employee_id: true,
|
|
department_id: true,
|
|
travel_request_id: true,
|
|
travel_order_id: true,
|
|
amount: true,
|
|
currency: true,
|
|
txn_type: true,
|
|
status: true,
|
|
occurred_at: true,
|
|
freeze_suggested_at: true,
|
|
freeze_suggested_by: true,
|
|
freeze_reason_code: true,
|
|
freeze_reason_note: true,
|
|
created_at: true,
|
|
updated_at: true,
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
if (!request) {
|
|
throw new AppError({
|
|
code: "SYS_404",
|
|
message: "Not Found",
|
|
statusCode: 404,
|
|
});
|
|
}
|
|
|
|
const orderIds = request.travel_orders.map((item) => item.id);
|
|
const authorizationIds = request.card_authorizations.map((item) => item.id);
|
|
const transactionIds = request.card_transactions.map((item) => item.id);
|
|
|
|
const [boundInvoices, requestBudgetEntries, corporateBudgetEntries] = await Promise.all([
|
|
orderIds.length
|
|
? prisma.invoicePool.findMany({
|
|
where: { travel_order_id: { in: orderIds } },
|
|
orderBy: [{ created_at: "desc" }],
|
|
select: {
|
|
id: true,
|
|
status: true,
|
|
original_name: true,
|
|
total_amount: true,
|
|
invoice_code: true,
|
|
invoice_number: true,
|
|
invoice_date: true,
|
|
linked_reimbursement_id: true,
|
|
travel_order_id: true,
|
|
},
|
|
})
|
|
: Promise.resolve([]),
|
|
prisma.budgetLedgerEntry.findMany({
|
|
where: {
|
|
entity_type: "travel_request",
|
|
entity_id: travelRequestId,
|
|
},
|
|
orderBy: [{ created_at: "asc" }, { id: "asc" }],
|
|
select: {
|
|
id: true,
|
|
budget_pool_id: true,
|
|
kind: true,
|
|
amount: true,
|
|
created_at: true,
|
|
},
|
|
}),
|
|
listCorporateBudgetEntries(prisma, { authorizationIds, transactionIds }),
|
|
]);
|
|
|
|
const budgetPoolIds = Array.from(new Set([
|
|
...requestBudgetEntries.map((item) => item.budget_pool_id).filter(Boolean),
|
|
...corporateBudgetEntries.map((item) => item.budget_pool_id).filter(Boolean),
|
|
...request.card_authorizations.map((item) => item.budget_pool_id).filter(Boolean),
|
|
]));
|
|
|
|
const budgetPools = budgetPoolIds.length
|
|
? await prisma.budgetPool.findMany({
|
|
where: { id: { in: budgetPoolIds } },
|
|
select: { id: true, code: true, name: true },
|
|
})
|
|
: [];
|
|
const budgetPoolMap = new Map(budgetPools.map((item) => [item.id, item]));
|
|
|
|
const orderInvoicesMap = new Map();
|
|
for (const invoice of boundInvoices) {
|
|
if (!invoice.travel_order_id) {
|
|
continue;
|
|
}
|
|
const items = orderInvoicesMap.get(invoice.travel_order_id) ?? [];
|
|
items.push(mapInvoice(invoice));
|
|
orderInvoicesMap.set(invoice.travel_order_id, items);
|
|
}
|
|
|
|
const ordersById = new Map(request.travel_orders.map((item) => [item.id, item]));
|
|
const transactionsByOrderId = new Map();
|
|
for (const transaction of request.card_transactions) {
|
|
if (!transaction.travel_order_id) {
|
|
continue;
|
|
}
|
|
const items = transactionsByOrderId.get(transaction.travel_order_id) ?? [];
|
|
items.push(transaction);
|
|
transactionsByOrderId.set(transaction.travel_order_id, items);
|
|
}
|
|
|
|
const orderStages = await Promise.all(
|
|
request.travel_orders.map(async (order) => {
|
|
const preview = await buildTravelOrderPaymentJournalPreview(prisma, order);
|
|
return [order.id, preview];
|
|
}),
|
|
);
|
|
const orderStageMap = new Map(orderStages);
|
|
|
|
const anomalies = request.card_transactions.flatMap((transaction) =>
|
|
buildTransactionAnomalies(transaction, transaction.travel_order_id ? ordersById.get(transaction.travel_order_id) : null),
|
|
);
|
|
|
|
return {
|
|
travelRequestId: request.id,
|
|
requestNo: request.request_no ?? null,
|
|
requestBudgetTrack: {
|
|
items: mapBudgetPoolSummary(summarizeBudgetLedgerByPool(requestBudgetEntries), budgetPoolMap, "request"),
|
|
},
|
|
corporateBudgetTrack: {
|
|
items: mapBudgetPoolSummary(summarizeCorporateBudgetEntries(corporateBudgetEntries), budgetPoolMap, "corporate"),
|
|
},
|
|
authorizations: request.card_authorizations.map(mapAuthorization),
|
|
transactions: request.card_transactions.map((item) => mapTransaction(item, ordersById)),
|
|
travelOrders: request.travel_orders.map((order) => {
|
|
const matchedTransactions = (transactionsByOrderId.get(order.id) ?? []).map((item) => mapTransaction(item, ordersById));
|
|
const stage = orderStageMap.get(order.id) ?? {
|
|
journal: null,
|
|
exportState: {
|
|
externalId: null,
|
|
lastOutboxId: null,
|
|
lastOutboxStatus: null,
|
|
lastError: null,
|
|
exportedAt: null,
|
|
},
|
|
binding: null,
|
|
outbox: null,
|
|
};
|
|
|
|
return {
|
|
id: order.id,
|
|
externalOrderNo: order.external_order_no,
|
|
source: order.source,
|
|
amount: toMoneyFixed(order.amount),
|
|
currency: order.currency,
|
|
status: order.status,
|
|
paymentVoucherNo: order.payment_voucher_no ?? null,
|
|
paymentDate: order.payment_date?.toISOString?.()?.slice(0, 10) ?? null,
|
|
paymentChannel: order.payment_channel ?? null,
|
|
paidAt: order.paid_at?.toISOString?.() ?? null,
|
|
paidBy: order.paid_by ?? null,
|
|
boundInvoices: orderInvoicesMap.get(order.id) ?? [],
|
|
matchedTransactions,
|
|
erpStage: {
|
|
binding: stage.binding,
|
|
outbox: stage.outbox,
|
|
exportState: stage.exportState,
|
|
},
|
|
actions: {
|
|
canMarkPaid: canManagePayments && order.status === "confirmed",
|
|
canBindInvoice: canManagePayments && ["confirmed", "paid"].includes(order.status),
|
|
},
|
|
};
|
|
}),
|
|
anomalies,
|
|
permissions: {
|
|
canManagePayments,
|
|
},
|
|
};
|
|
}
|
|
|
|
module.exports = {
|
|
authorizeBudgetForEntity,
|
|
settleBudgetForEntity,
|
|
reverseBudgetForEntity,
|
|
summarizeCorporateBudgetEntries,
|
|
buildTravelPaymentTrack,
|
|
buildTravelPaymentValidationError,
|
|
isOrderAmountAndCurrencyMatched,
|
|
};
|