feat(P20T2-1,P20T2-2,P20T2-3): 卡授权/交易/支付标记/发票绑定/ERP出站后端
这个提交包含在:
@@ -0,0 +1,666 @@
|
||||
const { z } = require("zod");
|
||||
|
||||
const { AppError } = require("../utils/AppError");
|
||||
const { sendData } = require("../utils/reply");
|
||||
const { buildAuditLogData } = require("../utils/audit");
|
||||
const { normalizeTravelTransactionRows } = require("../services/integrations/travel");
|
||||
const {
|
||||
settleBudgetForEntity,
|
||||
reverseBudgetForEntity,
|
||||
isOrderAmountAndCurrencyMatched,
|
||||
} = require("../services/travelPayments");
|
||||
|
||||
const UUID_SCHEMA = z.string().uuid();
|
||||
const LIST_QUERY_SCHEMA = z.object({
|
||||
travelRequestId: UUID_SCHEMA.optional(),
|
||||
travelOrderId: UUID_SCHEMA.optional(),
|
||||
status: z.string().trim().min(1).max(32).optional(),
|
||||
txnType: z.string().trim().min(1).max(32).optional(),
|
||||
page: z.coerce.number().int().min(1).optional(),
|
||||
pageSize: z.coerce.number().int().min(1).max(100).optional(),
|
||||
});
|
||||
const IMPORT_SCHEMA = z.object({
|
||||
transactions: z.array(z.record(z.string(), z.unknown())).min(1).max(100),
|
||||
});
|
||||
const FREEZE_SUGGESTION_SCHEMA = z.object({
|
||||
reasonCode: z.enum([
|
||||
"amount_mismatch",
|
||||
"currency_mismatch",
|
||||
"missing_order",
|
||||
"duplicate",
|
||||
"manual_review",
|
||||
]),
|
||||
note: z.string().trim().max(500).nullable().optional(),
|
||||
});
|
||||
|
||||
const TRANSACTION_STATUS_LIST = new Set(["pending", "settled", "reversed", "failed"]);
|
||||
const TRANSACTION_TYPE_LIST = new Set(["charge", "refund", "reversal"]);
|
||||
|
||||
function buildValidationError(details, statusCode = 400) {
|
||||
return new AppError({
|
||||
code: "VALID_001",
|
||||
message: "参数校验失败",
|
||||
statusCode,
|
||||
details,
|
||||
});
|
||||
}
|
||||
|
||||
function normalizeTransactionStatus(value) {
|
||||
const normalized = String(value ?? "pending").trim().toLowerCase();
|
||||
return TRANSACTION_STATUS_LIST.has(normalized) ? normalized : "pending";
|
||||
}
|
||||
|
||||
function normalizeTransactionType(value) {
|
||||
const normalized = String(value ?? "charge").trim().toLowerCase();
|
||||
return TRANSACTION_TYPE_LIST.has(normalized) ? normalized : "charge";
|
||||
}
|
||||
|
||||
function parseListQuery(query) {
|
||||
const parsed = LIST_QUERY_SCHEMA.safeParse(query ?? {});
|
||||
if (!parsed.success) {
|
||||
throw buildValidationError(parsed.error.flatten());
|
||||
}
|
||||
|
||||
return {
|
||||
travelRequestId: parsed.data.travelRequestId ?? null,
|
||||
travelOrderId: parsed.data.travelOrderId ?? null,
|
||||
status: parsed.data.status ? normalizeTransactionStatus(parsed.data.status) : null,
|
||||
txnType: parsed.data.txnType ? normalizeTransactionType(parsed.data.txnType) : null,
|
||||
page: parsed.data.page ?? 1,
|
||||
pageSize: parsed.data.pageSize ?? 20,
|
||||
};
|
||||
}
|
||||
|
||||
function toMoneyFixed(value) {
|
||||
const numeric = Number(value ?? 0);
|
||||
if (!Number.isFinite(numeric)) {
|
||||
return "0.00";
|
||||
}
|
||||
return numeric.toFixed(2);
|
||||
}
|
||||
|
||||
function mapMatchedOrder(order, transaction) {
|
||||
if (!order) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
id: order.id,
|
||||
externalOrderNo: order.external_order_no,
|
||||
status: order.status,
|
||||
amount: toMoneyFixed(order.amount),
|
||||
currency: order.currency,
|
||||
exactMatch: isOrderAmountAndCurrencyMatched(order, transaction),
|
||||
};
|
||||
}
|
||||
|
||||
function mapTransaction(item) {
|
||||
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: mapMatchedOrder(item.travel_order ?? null, item),
|
||||
createdAt: item.created_at?.toISOString?.() ?? null,
|
||||
updatedAt: item.updated_at?.toISOString?.() ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
function hasTransactionConflict(existing, row) {
|
||||
if (
|
||||
existing.employee_id !== row.employeeId ||
|
||||
existing.travel_request_id !== row.travelRequestId ||
|
||||
String(existing.amount ?? "0.00") !== String(row.amount ?? "0.00") ||
|
||||
String(existing.currency ?? "CNY") !== String(row.currency ?? "CNY") ||
|
||||
String(existing.txn_type ?? "charge") !== String(row.txnType ?? "charge")
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
row.hasCardAuthorizationId &&
|
||||
(existing.card_authorization_id ?? null) !== (row.cardAuthorizationId ?? null)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (row.hasTravelOrderId && (existing.travel_order_id ?? null) !== (row.travelOrderId ?? null)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
async function syncAuthorizationSnapshot(tx, authorizationId, actorId) {
|
||||
const authorization = await tx.cardAuthorization.findUnique({
|
||||
where: { id: authorizationId },
|
||||
select: {
|
||||
id: true,
|
||||
authorized_amount: true,
|
||||
card_transactions: {
|
||||
select: {
|
||||
amount: true,
|
||||
txn_type: true,
|
||||
status: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!authorization) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const settledAmount = authorization.card_transactions.reduce((total, item) => {
|
||||
if (item.txn_type === "charge" && item.status === "settled") {
|
||||
return total + Number(item.amount ?? 0);
|
||||
}
|
||||
return total;
|
||||
}, 0);
|
||||
|
||||
const reversedAmount = authorization.card_transactions.reduce((total, item) => {
|
||||
if (["refund", "reversal"].includes(item.txn_type) || item.status === "reversed") {
|
||||
return total + Number(item.amount ?? 0);
|
||||
}
|
||||
return total;
|
||||
}, 0);
|
||||
|
||||
const authorizedAmount = Number(authorization.authorized_amount ?? 0);
|
||||
let nextStatus = "authorized";
|
||||
if (reversedAmount > 0 && reversedAmount >= authorizedAmount && authorizedAmount > 0) {
|
||||
nextStatus = "reversed";
|
||||
} else if (settledAmount > 0 && settledAmount >= authorizedAmount && authorizedAmount > 0) {
|
||||
nextStatus = "settled";
|
||||
} else if (settledAmount > 0) {
|
||||
nextStatus = "partially_settled";
|
||||
}
|
||||
|
||||
return tx.cardAuthorization.update({
|
||||
where: { id: authorizationId },
|
||||
data: {
|
||||
settled_amount: settledAmount.toFixed(2),
|
||||
reversed_amount: reversedAmount.toFixed(2),
|
||||
status: nextStatus,
|
||||
updated_by: actorId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function loadOrderById(tx, orderId) {
|
||||
return tx.travelOrder.findUnique({
|
||||
where: { id: orderId },
|
||||
select: {
|
||||
id: true,
|
||||
external_order_no: true,
|
||||
employee_id: true,
|
||||
travel_request_id: true,
|
||||
amount: true,
|
||||
currency: true,
|
||||
status: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function travelTransactionsRoutes(fastify) {
|
||||
fastify.get(
|
||||
"/travel/transactions",
|
||||
{
|
||||
preHandler: fastify.requirePermission("travel.payment.manage"),
|
||||
},
|
||||
async (request, reply) => {
|
||||
const query = parseListQuery(request.query);
|
||||
const skip = (query.page - 1) * query.pageSize;
|
||||
const where = {
|
||||
...(query.travelRequestId ? { travel_request_id: query.travelRequestId } : {}),
|
||||
...(query.travelOrderId ? { travel_order_id: query.travelOrderId } : {}),
|
||||
...(query.status ? { status: query.status } : {}),
|
||||
...(query.txnType ? { txn_type: query.txnType } : {}),
|
||||
};
|
||||
|
||||
const [items, total] = await Promise.all([
|
||||
fastify.prisma.cardTransaction.findMany({
|
||||
where,
|
||||
orderBy: [{ occurred_at: "desc" }, { created_at: "desc" }],
|
||||
skip,
|
||||
take: query.pageSize,
|
||||
include: {
|
||||
travel_order: {
|
||||
select: {
|
||||
id: true,
|
||||
external_order_no: true,
|
||||
status: true,
|
||||
amount: true,
|
||||
currency: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
fastify.prisma.cardTransaction.count({ where }),
|
||||
]);
|
||||
|
||||
return sendData(reply, request, {
|
||||
page: query.page,
|
||||
pageSize: query.pageSize,
|
||||
total,
|
||||
items: items.map(mapTransaction),
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
fastify.post(
|
||||
"/travel/transactions/import",
|
||||
{
|
||||
preHandler: fastify.requirePermission("integration.manage"),
|
||||
},
|
||||
async (request, reply) => {
|
||||
if (!fastify.config.travelSyncEnabled) {
|
||||
throw buildValidationError({ integration: "travel_sync_disabled" }, 409);
|
||||
}
|
||||
|
||||
const parsed = IMPORT_SCHEMA.safeParse(request.body ?? {});
|
||||
if (!parsed.success) {
|
||||
throw buildValidationError(parsed.error.flatten());
|
||||
}
|
||||
|
||||
const rows = normalizeTravelTransactionRows(parsed.data.transactions);
|
||||
if (!rows.length) {
|
||||
throw buildValidationError({ transactions: "empty_after_normalization" });
|
||||
}
|
||||
|
||||
const employeeIds = Array.from(new Set(rows.map((item) => item.employeeId)));
|
||||
const travelRequestIds = Array.from(new Set(rows.map((item) => item.travelRequestId)));
|
||||
const authorizationIds = Array.from(
|
||||
new Set(rows.map((item) => item.cardAuthorizationId).filter(Boolean)),
|
||||
);
|
||||
const travelOrderIds = Array.from(new Set(rows.map((item) => item.travelOrderId).filter(Boolean)));
|
||||
|
||||
const [employees, travelRequests, authorizations, travelOrders] = await Promise.all([
|
||||
fastify.prisma.employee.findMany({
|
||||
where: { id: { in: employeeIds }, status: "active" },
|
||||
select: { id: true, department_id: true },
|
||||
}),
|
||||
fastify.prisma.travelRequest.findMany({
|
||||
where: { id: { in: travelRequestIds } },
|
||||
select: {
|
||||
id: true,
|
||||
applicant_employee_id: true,
|
||||
budget_pool_id: true,
|
||||
},
|
||||
}),
|
||||
authorizationIds.length
|
||||
? fastify.prisma.cardAuthorization.findMany({
|
||||
where: { id: { in: authorizationIds } },
|
||||
select: {
|
||||
id: true,
|
||||
employee_id: true,
|
||||
travel_request_id: true,
|
||||
budget_pool_id: true,
|
||||
},
|
||||
})
|
||||
: Promise.resolve([]),
|
||||
travelOrderIds.length
|
||||
? fastify.prisma.travelOrder.findMany({
|
||||
where: { id: { in: travelOrderIds } },
|
||||
select: {
|
||||
id: true,
|
||||
employee_id: true,
|
||||
travel_request_id: true,
|
||||
external_order_no: true,
|
||||
amount: true,
|
||||
currency: true,
|
||||
status: true,
|
||||
},
|
||||
})
|
||||
: Promise.resolve([]),
|
||||
]);
|
||||
|
||||
const employeeMap = new Map(employees.map((item) => [item.id, item]));
|
||||
const travelRequestMap = new Map(travelRequests.map((item) => [item.id, item]));
|
||||
const authorizationMap = new Map(authorizations.map((item) => [item.id, item]));
|
||||
const orderMap = new Map(travelOrders.map((item) => [item.id, item]));
|
||||
|
||||
const imported = [];
|
||||
const skipped = [];
|
||||
let confirmedCount = 0;
|
||||
let reversedCount = 0;
|
||||
|
||||
for (const row of rows) {
|
||||
const employee = employeeMap.get(row.employeeId);
|
||||
if (!employee) {
|
||||
skipped.push({
|
||||
source: row.source,
|
||||
externalTxnId: row.externalTxnId,
|
||||
reason: "employee_not_found",
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
const travelRequest = travelRequestMap.get(row.travelRequestId);
|
||||
if (!travelRequest) {
|
||||
skipped.push({
|
||||
source: row.source,
|
||||
externalTxnId: row.externalTxnId,
|
||||
reason: "travel_request_not_found",
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
if (travelRequest.applicant_employee_id !== row.employeeId) {
|
||||
skipped.push({
|
||||
source: row.source,
|
||||
externalTxnId: row.externalTxnId,
|
||||
reason: "travel_request_employee_mismatch",
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
const authorization = row.cardAuthorizationId ? authorizationMap.get(row.cardAuthorizationId) ?? null : null;
|
||||
if (row.cardAuthorizationId && !authorization) {
|
||||
skipped.push({
|
||||
source: row.source,
|
||||
externalTxnId: row.externalTxnId,
|
||||
reason: "card_authorization_not_found",
|
||||
});
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
authorization &&
|
||||
(authorization.employee_id !== row.employeeId || authorization.travel_request_id !== row.travelRequestId)
|
||||
) {
|
||||
skipped.push({
|
||||
source: row.source,
|
||||
externalTxnId: row.externalTxnId,
|
||||
reason: "card_authorization_mismatch",
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
const travelOrder = row.travelOrderId ? orderMap.get(row.travelOrderId) ?? null : null;
|
||||
if (row.travelOrderId && !travelOrder) {
|
||||
skipped.push({
|
||||
source: row.source,
|
||||
externalTxnId: row.externalTxnId,
|
||||
reason: "travel_order_not_found",
|
||||
});
|
||||
continue;
|
||||
}
|
||||
if (travelOrder && travelOrder.employee_id !== row.employeeId) {
|
||||
skipped.push({
|
||||
source: row.source,
|
||||
externalTxnId: row.externalTxnId,
|
||||
reason: "travel_order_employee_mismatch",
|
||||
});
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
travelOrder &&
|
||||
travelOrder.travel_request_id &&
|
||||
travelOrder.travel_request_id !== row.travelRequestId
|
||||
) {
|
||||
skipped.push({
|
||||
source: row.source,
|
||||
externalTxnId: row.externalTxnId,
|
||||
reason: "travel_order_request_mismatch",
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
const existing = await fastify.prisma.cardTransaction.findUnique({
|
||||
where: {
|
||||
source_external_txn_id: {
|
||||
source: row.source,
|
||||
external_txn_id: row.externalTxnId,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (existing && hasTransactionConflict(existing, row)) {
|
||||
skipped.push({
|
||||
source: row.source,
|
||||
externalTxnId: row.externalTxnId,
|
||||
reason: "transaction_conflict",
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
const txnType = normalizeTransactionType(row.txnType);
|
||||
const status = normalizeTransactionStatus(row.status);
|
||||
|
||||
const record = await fastify.prisma.$transaction(async (tx) => {
|
||||
const nextCardAuthorizationId = row.hasCardAuthorizationId
|
||||
? row.cardAuthorizationId ?? null
|
||||
: existing?.card_authorization_id;
|
||||
const nextTravelOrderId = row.hasTravelOrderId
|
||||
? row.travelOrderId ?? null
|
||||
: existing?.travel_order_id;
|
||||
|
||||
const upserted = await tx.cardTransaction.upsert({
|
||||
where: {
|
||||
source_external_txn_id: {
|
||||
source: row.source,
|
||||
external_txn_id: row.externalTxnId,
|
||||
},
|
||||
},
|
||||
update: {
|
||||
employee_id: row.employeeId,
|
||||
department_id: employee.department_id,
|
||||
travel_request_id: row.travelRequestId,
|
||||
...(row.hasCardAuthorizationId ? { card_authorization_id: nextCardAuthorizationId } : {}),
|
||||
...(row.hasTravelOrderId ? { travel_order_id: nextTravelOrderId } : {}),
|
||||
amount: row.amount,
|
||||
currency: row.currency,
|
||||
txn_type: txnType,
|
||||
status,
|
||||
occurred_at: row.occurredAt,
|
||||
raw_payload: row.rawPayload,
|
||||
updated_by: request.employee.id,
|
||||
},
|
||||
create: {
|
||||
source: row.source,
|
||||
external_txn_id: row.externalTxnId,
|
||||
card_authorization_id: nextCardAuthorizationId ?? null,
|
||||
employee_id: row.employeeId,
|
||||
department_id: employee.department_id,
|
||||
travel_request_id: row.travelRequestId,
|
||||
travel_order_id: nextTravelOrderId ?? null,
|
||||
amount: row.amount,
|
||||
currency: row.currency,
|
||||
txn_type: txnType,
|
||||
status,
|
||||
occurred_at: row.occurredAt,
|
||||
raw_payload: row.rawPayload,
|
||||
created_by: request.employee.id,
|
||||
updated_by: request.employee.id,
|
||||
},
|
||||
});
|
||||
|
||||
const budgetPoolId = authorization?.budget_pool_id ?? travelRequest.budget_pool_id ?? null;
|
||||
|
||||
if (txnType === "charge" && status === "settled") {
|
||||
await settleBudgetForEntity(tx, {
|
||||
budgetPoolId,
|
||||
entityId: upserted.id,
|
||||
amount: row.amount,
|
||||
actorId: request.employee.id,
|
||||
note: "travel_transaction_import_settle",
|
||||
});
|
||||
}
|
||||
|
||||
if (["refund", "reversal"].includes(txnType) || status === "reversed") {
|
||||
const reverseBudget = await reverseBudgetForEntity(tx, {
|
||||
budgetPoolId,
|
||||
entityId: upserted.id,
|
||||
amount: row.amount,
|
||||
actorId: request.employee.id,
|
||||
note: "travel_transaction_import_reverse",
|
||||
});
|
||||
if (reverseBudget.applied) {
|
||||
reversedCount += 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (nextCardAuthorizationId) {
|
||||
await syncAuthorizationSnapshot(tx, nextCardAuthorizationId, request.employee.id);
|
||||
}
|
||||
|
||||
const nextOrder = nextTravelOrderId ? await loadOrderById(tx, nextTravelOrderId) : null;
|
||||
if (
|
||||
nextOrder &&
|
||||
txnType === "charge" &&
|
||||
status === "settled" &&
|
||||
nextOrder.status === "imported" &&
|
||||
isOrderAmountAndCurrencyMatched(nextOrder, upserted)
|
||||
) {
|
||||
await tx.travelOrder.update({
|
||||
where: { id: nextOrder.id },
|
||||
data: {
|
||||
status: "confirmed",
|
||||
updated_by: request.employee.id,
|
||||
},
|
||||
});
|
||||
confirmedCount += 1;
|
||||
}
|
||||
|
||||
return tx.cardTransaction.findUnique({
|
||||
where: { id: upserted.id },
|
||||
include: {
|
||||
travel_order: {
|
||||
select: {
|
||||
id: true,
|
||||
external_order_no: true,
|
||||
status: true,
|
||||
amount: true,
|
||||
currency: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
imported.push(record);
|
||||
}
|
||||
|
||||
await fastify.prisma.auditLog.create({
|
||||
data: buildAuditLogData(request, {
|
||||
actorId: request.employee.id,
|
||||
action: "TRAVEL_TRANSACTIONS_IMPORTED",
|
||||
targetType: "card_transaction",
|
||||
extra: {
|
||||
requested: rows.length,
|
||||
createdOrUpdated: imported.length,
|
||||
confirmedCount,
|
||||
reversedCount,
|
||||
skipped,
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
return sendData(reply, request, {
|
||||
requested: rows.length,
|
||||
imported: imported.length,
|
||||
confirmedCount,
|
||||
reversedCount,
|
||||
skipped,
|
||||
items: imported.filter(Boolean).map(mapTransaction),
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
fastify.post(
|
||||
"/travel/transactions/:id/freeze-suggestion",
|
||||
{
|
||||
preHandler: fastify.requirePermission("travel.payment.manage"),
|
||||
},
|
||||
async (request, reply) => {
|
||||
const parsedId = UUID_SCHEMA.safeParse(request.params?.id);
|
||||
if (!parsedId.success) {
|
||||
throw buildValidationError({ id: "invalid_uuid" });
|
||||
}
|
||||
|
||||
const parsedBody = FREEZE_SUGGESTION_SCHEMA.safeParse(request.body ?? {});
|
||||
if (!parsedBody.success) {
|
||||
throw buildValidationError(parsedBody.error.flatten());
|
||||
}
|
||||
|
||||
const transaction = await fastify.prisma.$transaction(async (tx) => {
|
||||
const current = await tx.cardTransaction.findUnique({
|
||||
where: { id: parsedId.data },
|
||||
include: {
|
||||
travel_order: {
|
||||
select: {
|
||||
id: true,
|
||||
external_order_no: true,
|
||||
status: true,
|
||||
amount: true,
|
||||
currency: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!current) {
|
||||
throw new AppError({
|
||||
code: "SYS_404",
|
||||
message: "Not Found",
|
||||
statusCode: 404,
|
||||
});
|
||||
}
|
||||
|
||||
const updated = await tx.cardTransaction.update({
|
||||
where: { id: parsedId.data },
|
||||
data: {
|
||||
freeze_suggested_at: new Date(),
|
||||
freeze_suggested_by: request.employee.id,
|
||||
freeze_reason_code: parsedBody.data.reasonCode,
|
||||
freeze_reason_note: parsedBody.data.note ?? null,
|
||||
updated_by: request.employee.id,
|
||||
},
|
||||
include: {
|
||||
travel_order: {
|
||||
select: {
|
||||
id: true,
|
||||
external_order_no: true,
|
||||
status: true,
|
||||
amount: true,
|
||||
currency: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await tx.auditLog.create({
|
||||
data: buildAuditLogData(request, {
|
||||
actorId: request.employee.id,
|
||||
action: "TRAVEL_TRANSACTION_FREEZE_SUGGESTED",
|
||||
targetType: "card_transaction",
|
||||
targetId: updated.id,
|
||||
extra: {
|
||||
reasonCode: parsedBody.data.reasonCode,
|
||||
note: parsedBody.data.note ?? null,
|
||||
travelOrderId: updated.travel_order_id ?? null,
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
return updated;
|
||||
});
|
||||
|
||||
return sendData(reply, request, mapTransaction(transaction));
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = travelTransactionsRoutes;
|
||||
在新工单中引用
屏蔽一个用户