feat(P20T2-4): 差旅支付轨道卡片与发票绑定前端交互

这个提交包含在:
2026-03-11 06:41:11 +08:00
父节点 dc74d9e53f
当前提交 2553955f28
共修改 9 个文件,包含 977 行新增和 21 行删除
+259
查看文件
@@ -0,0 +1,259 @@
import { apiRequestData } from "./http";
export type TravelPaymentChannel =
| "bank_transfer"
| "corporate_card"
| "virtual_card"
| "manual"
| "other";
export type TravelPaymentBudgetSummaryItem = {
budgetPoolId: string;
budgetPoolCode: string | null;
budgetPoolName: string | null;
outstanding: string;
reserve?: string;
confirm?: string;
release?: string;
confirmPending?: string;
authorize?: string;
settle?: string;
reverse?: string;
settlePending?: string;
};
export type TravelPaymentAuthorization = {
id: string;
source: string;
externalAuthNo: string;
employeeId: string;
departmentId: string | null;
travelRequestId: string;
budgetPoolId: string | null;
legalEntityId: string;
costCenterId: string;
authorizedAmount: string;
settledAmount: string;
reversedAmount: string;
currency: string;
occurredAt: string | null;
expiresAt: string | null;
status: string;
createdAt: string | null;
updatedAt: string | null;
};
export type TravelPaymentMatchedOrder = {
id: string;
externalOrderNo: string;
status: string;
amount: string;
currency: string;
exactMatch: boolean;
};
export type TravelPaymentTransaction = {
id: string;
source: string;
externalTxnId: string;
cardAuthorizationId: string | null;
employeeId: string;
departmentId: string | null;
travelRequestId: string;
travelOrderId: string | null;
amount: string;
currency: string;
txnType: string;
status: string;
occurredAt: string | null;
freezeSuggestedAt: string | null;
freezeSuggestedBy: string | null;
freezeReasonCode: string | null;
freezeReasonNote: string | null;
matchedOrder: TravelPaymentMatchedOrder | null;
createdAt: string | null;
updatedAt: string | null;
};
export type TravelPaymentBoundInvoice = {
id: string;
status: string;
originalName: string | null;
totalAmount: string | null;
invoiceCode: string | null;
invoiceNumber: string | null;
invoiceDate: string | null;
linkedReimbursementId: string | null;
linkedTravelOrderId: string | null;
};
export type TravelPaymentErpStage = {
binding: {
id: string;
provider: string;
entityType: string;
localId: string;
externalId: string | null;
externalCode: string | null;
lastSyncedAt: string | null;
} | null;
outbox: {
id: string;
eventType: string;
status: string;
lastError: string | null;
processedAt: string | null;
updatedAt: string | null;
} | null;
exportState: {
externalId: string | null;
lastOutboxId: string | null;
lastOutboxStatus: string | null;
lastError: string | null;
exportedAt: string | null;
};
};
export type TravelPaymentOrder = {
id: string;
externalOrderNo: string;
source: string;
amount: string;
currency: string;
status: string;
paymentVoucherNo: string | null;
paymentDate: string | null;
paymentChannel: TravelPaymentChannel | null;
paidAt: string | null;
paidBy: string | null;
boundInvoices: TravelPaymentBoundInvoice[];
matchedTransactions: TravelPaymentTransaction[];
erpStage: TravelPaymentErpStage;
actions: {
canMarkPaid: boolean;
canBindInvoice: boolean;
};
};
export type TravelPaymentAnomaly = {
code: string;
level: "low" | "medium" | "high";
title: string;
detail: string;
transactionId: string;
travelOrderId: string | null;
};
export type TravelPaymentTrack = {
travelRequestId: string;
requestNo: string | null;
requestBudgetTrack: {
items: TravelPaymentBudgetSummaryItem[];
};
corporateBudgetTrack: {
items: TravelPaymentBudgetSummaryItem[];
};
authorizations: TravelPaymentAuthorization[];
transactions: TravelPaymentTransaction[];
travelOrders: TravelPaymentOrder[];
anomalies: TravelPaymentAnomaly[];
permissions: {
canManagePayments: boolean;
};
};
export type TravelAuthorizationListResponse = {
page: number;
pageSize: number;
total: number;
items: TravelPaymentAuthorization[];
};
export type TravelTransactionListResponse = {
page: number;
pageSize: number;
total: number;
items: TravelPaymentTransaction[];
};
function toQueryString(query: Record<string, string | number | boolean | undefined>) {
const params = new URLSearchParams();
for (const [key, value] of Object.entries(query)) {
if (value == null || value === "") {
continue;
}
params.set(key, typeof value === "boolean" ? (value ? "1" : "0") : String(value));
}
const encoded = params.toString();
return encoded ? `?${encoded}` : "";
}
export async function fetchTravelAuthorizations(query: Record<string, string | number | boolean | undefined> = {}) {
return apiRequestData<TravelAuthorizationListResponse>(
`/api/travel/authorizations${toQueryString(query)}`,
);
}
export async function importTravelAuthorizations(payload: { authorizations: Array<Record<string, unknown>> }) {
return apiRequestData<Record<string, unknown>>("/api/travel/authorizations/import", {
method: "POST",
body: JSON.stringify(payload),
});
}
export async function fetchTravelTransactions(query: Record<string, string | number | boolean | undefined> = {}) {
return apiRequestData<TravelTransactionListResponse>(
`/api/travel/transactions${toQueryString(query)}`,
);
}
export async function importTravelTransactions(payload: { transactions: Array<Record<string, unknown>> }) {
return apiRequestData<Record<string, unknown>>("/api/travel/transactions/import", {
method: "POST",
body: JSON.stringify(payload),
});
}
export async function suggestFreezeTravelTransaction(
transactionId: string,
payload: {
reasonCode: "amount_mismatch" | "currency_mismatch" | "missing_order" | "duplicate" | "manual_review";
note?: string | null;
},
) {
return apiRequestData<TravelPaymentTransaction>(`/api/travel/transactions/${transactionId}/freeze-suggestion`, {
method: "POST",
body: JSON.stringify(payload),
});
}
export async function markTravelOrderPaid(
travelOrderId: string,
payload: {
paymentVoucherNo?: string | null;
paymentDate?: string | null;
paymentChannel?: TravelPaymentChannel | null;
},
) {
return apiRequestData<{
travelOrderId: string;
travelRequestId: string | null;
status: string;
paymentVoucherNo: string | null;
paymentDate: string | null;
paymentChannel: TravelPaymentChannel | null;
erpJournal: {
eventType: string;
outboxId: string | null;
dedupeKey: string;
enqueued: boolean;
};
}>(`/api/travel-orders/${travelOrderId}/mark-paid`, {
method: "POST",
body: JSON.stringify(payload),
});
}
export async function fetchTravelPaymentTrack(travelRequestId: string) {
return apiRequestData<TravelPaymentTrack>(`/api/travel-requests/${travelRequestId}/payment-track`);
}