import { apiRequestBlob, apiRequestData, type ApiBlobResponse } from "./http"; import { buildAttachmentFileUrl, type AttachmentGovernanceFields } from "../lib/attachmentGovernance"; import type { RiskFlag } from "./feeControl"; export type PaymentBatchEntityType = "reimbursement" | "travel_order"; export type PaymentBatchStatus = "draft" | "reviewed" | "executed" | "failed" | "cancelled"; export type PaymentChannel = "bank" | "alipay" | "wechat" | "cash" | "other"; export type PaymentBatchItem = { id: string; entityId: string; entityCode: string; entityTitle: string; amount: string; currencyCode: string; paymentVoucherNoOverride: string | null; paymentDateOverride: string | null; paymentChannelOverride: PaymentChannel | null; payeeName: string | null; bankName: string | null; bankAccountMasked: string | null; status: string; failureCode: string | null; failureMessage: string | null; createdAt: string | null; openRiskFlags: RiskFlag[]; }; export type PaymentBatch = { id: string; batchNo: string; entityType: PaymentBatchEntityType; name: string; status: PaymentBatchStatus; defaultPaymentDate: string | null; defaultPaymentChannel: PaymentChannel | null; voucherPrefix: string | null; totalAmount: string; highRisk: boolean; reviewRequirement: { thresholdAmount: string; requiredReviewCount: number; completedReviewCount: number; nextStep: "first_review" | "ready_for_execute"; makerCheckerRequired: boolean; reviewerPermission: string; }; reviewComment: string | null; secondaryReviewComment: string | null; executionSummaryJson: Record | null; receiptAttachmentCount?: number; paymentAccount: { accountName: string; bankName: string; bankInstitutionCode: string; accountNumberMasked: string; } | null; paymentOrderExportCount: number; paymentOrderLastExportedAt: string | null; paymentOrderLastExportedBy: string | null; canExportPaymentOrder: boolean; paymentOrderExportBlockReason: string | null; paymentOrderExportBlockers: Array<{ code: string; entityCode?: string }>; createdAt: string | null; createdBy: string | null; reviewedAt: string | null; reviewedBy: string | null; secondaryReviewedAt: string | null; secondaryReviewedBy: string | null; executedAt: string | null; executedBy: string | null; cancelledAt: string | null; cancelledBy: string | null; cancelledReason: string | null; items: PaymentBatchItem[]; }; export type PaymentBatchCandidate = { entityId: string; entityType: "reimbursement"; businessNo: string; employee: { id: string; employeeNo: string; name: string; }; amount: string; currencyCode: "CNY"; businessDate: string | null; summary: string | null; }; export type PaymentBatchCandidatesResponse = { items: PaymentBatchCandidate[]; page: number; pageSize: number; total: number; }; export type PaymentBatchReceiptAttachment = { id: string; originalName: string; mimeType: string; fileSize: number; fileSha256: string; } & AttachmentGovernanceFields & { uploadedBy: { id: string; employeeNo: string; name: string; } | null; createdAt: string | null; }; export type PaymentBatchListResponse = { items: PaymentBatch[] }; export async function fetchPaymentBatches(query: { entityType?: PaymentBatchEntityType; status?: PaymentBatchStatus } = {}) { const params = new URLSearchParams(); if (query.entityType) params.set("entityType", query.entityType); if (query.status) params.set("status", query.status); const suffix = params.toString() ? `?${params.toString()}` : ""; return apiRequestData(`/api/payment-batches${suffix}`); } export async function fetchPaymentBatchDetail(batchId: string) { return apiRequestData<{ item: PaymentBatch }>(`/api/payment-batches/${batchId}`); } export async function fetchPaymentBatchCandidates(query: { q?: string; page?: number; pageSize?: number } = {}) { const params = new URLSearchParams(); if (query.q?.trim()) params.set("q", query.q.trim()); if (query.page) params.set("page", String(query.page)); if (query.pageSize) params.set("pageSize", String(query.pageSize)); const suffix = params.toString() ? `?${params.toString()}` : ""; return apiRequestData(`/api/payment-batches/candidates${suffix}`); } export async function createPaymentBatch(payload: { entityType: PaymentBatchEntityType; name?: string; entityIds: string[]; defaultPaymentDate?: string; defaultPaymentChannel?: PaymentChannel; voucherPrefix?: string; }) { return apiRequestData<{ item: PaymentBatch }>("/api/payment-batches", { method: "POST", body: JSON.stringify(payload), }); } export async function updatePaymentBatchItem(batchId: string, itemId: string, payload: { paymentVoucherNoOverride?: string | null; paymentDateOverride?: string | null; }) { return apiRequestData<{ item: Pick }>( `/api/payment-batches/${batchId}/items/${itemId}`, { method: "PATCH", body: JSON.stringify(payload), }, ); } export async function reviewPaymentBatch(batchId: string, payload: { comment?: string; resolveRiskFlagIds?: string[] } = {}) { return apiRequestData<{ item: PaymentBatch }>(`/api/payment-batches/${batchId}/review`, { method: "POST", body: JSON.stringify(payload), }); } export async function executePaymentBatch(batchId: string) { return apiRequestData<{ item: PaymentBatch }>(`/api/payment-batches/${batchId}/execute`, { method: "POST", body: JSON.stringify({}), }); } export async function cancelPaymentBatch(batchId: string, reason: string) { return apiRequestData<{ item: PaymentBatch }>(`/api/payment-batches/${batchId}/cancel`, { method: "POST", body: JSON.stringify({ reason }), }); } export async function downloadPaymentOrder(batchId: string, reason: string): Promise { return apiRequestBlob(`/api/payment-batches/${batchId}/payment-order`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ reason }), }); } export async function revealPaymentBatchPayeeBank(batchId: string, itemId: string, reason: string) { return apiRequestData<{ bankAccount: string }>( `/api/payment-batches/${batchId}/items/${itemId}/payee-bank/reveal`, { method: "POST", body: JSON.stringify({ reason }) }, ); } export async function fetchPaymentBatchReceiptAttachments(batchId: string) { return apiRequestData<{ items: PaymentBatchReceiptAttachment[] }>( `/api/payment-batches/${batchId}/receipt-attachments`, ); } export async function uploadPaymentBatchReceiptAttachments(batchId: string, files: File[]) { const formData = new FormData(); for (const file of files) { formData.append("files", file); } return apiRequestData<{ items: PaymentBatchReceiptAttachment[] }>( `/api/payment-batches/${batchId}/receipt-attachments`, { method: "POST", body: formData, }, ); } export function paymentBatchReceiptAttachmentFileUrl(batchId: string, attachmentId: string) { return `/api/payment-batches/${batchId}/receipt-attachments/${attachmentId}/file`; } export async function downloadPaymentBatchReceiptAttachment( batchId: string, attachmentId: string, reason?: string, ): Promise { return apiRequestBlob(buildAttachmentFileUrl(paymentBatchReceiptAttachmentFileUrl(batchId, attachmentId), reason)); } export async function voidPaymentBatchReceiptAttachment(batchId: string, attachmentId: string, reason: string) { return apiRequestData<{ attachmentId: string; outcome: "voided"; voidedAt: string }>( `/api/payment-batches/${batchId}/receipt-attachments/${attachmentId}/void`, { method: "POST", body: JSON.stringify({ reason }) }, ); }