feat(P20T2-4): 差旅支付轨道卡片与发票绑定前端交互
这个提交包含在:
@@ -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`);
|
||||
}
|
||||
在新工单中引用
屏蔽一个用户