import { apiRequestBlob, apiRequestData } from "./http"; import type { ExpenseStandardCheck, PrecheckResponse, RiskFlag } from "./feeControl"; import { buildAttachmentFileUrl, type AttachmentGovernanceFields } from "../lib/attachmentGovernance"; import type { ProcessingProgress, ProcessingStage } from "../types/processingProgress"; export type ReimbursementStatus = | "draft" | "reviewing" | "approved" | "paid" | "rejected" | "cancelled"; export type ReimbursementItem = { id: string; expenseTypeId: string | null; expenseDate: string | null; amount: string; description: string | null; assetPriorApprovalBasis?: string | null; expenseType: { id: string; code: string; name: string; category: string; enabled: boolean; requiresInvoice?: boolean; } | null; }; export type LinkedTravelRequestSummary = { id: string; requestNo: string | null; purpose: string | null; startDate: string | null; endDate: string | null; estimatedAmount: string; }; export type ReimbursementListItem = { id: string; formNumber: string | null; status: ReimbursementStatus; reimbursementDate: string | null; totalAmount: string | null; description: string | null; payment: { paymentVoucherNo: string | null; paymentDate: string | null; paymentChannel: string | null; paidAt: string | null; paidBy: string | null; }; owner: { id: string; employeeNo: string; name: string; } | null; submitter: { id: string; employeeNo: string; name: string; } | null; project: { id: string; projectNo: string; name: string; } | null; pendingWorkflow: { instanceId: string; stepId: string; allowedActions: Array<"approve" | "reject" | "return" | "transfer">; task: { id: string; stepName: string; assigneeId: string | null; assigneeRole: string | null; } | null; } | null; createdAt: string | null; updatedAt: string | null; }; export type ReimbursementListResponse = { page: number; pageSize: number; total: number; scope: "own" | "all"; items: ReimbursementListItem[]; }; export type ReimbursementBudgetHint = { budgetId: string; budgetCode: string; budgetName: string; controlMode: "warn" | "block"; warningThresholdPercent: number; allocatedAmount: string; usedAmount: string; remainingAmount: string; currentAmount: string; nextUsedAmount: string; nextUsagePercent: number; exceedsThreshold: boolean; exceedsBudget: boolean; }; export type ReimbursementWorkflowProgress = { currentStepIndex: number; totalSteps: number; steps: Array<{ id: string; name: string; status: "completed" | "current" | "pending"; }>; }; export type ReimbursementProcessingStage = ProcessingStage; export type ReimbursementProcessingProgress = ProcessingProgress; export type ReimbursementApprovalHistoryItem = { id: string; stepName: string | null; action: string; actorName: string; comment: string | null; createdAt: string | null; }; export type ReimbursementDetail = { deletedAt?: string | null; id: string; formNumber: string | null; submitterId: string; ownerId: string; departmentId: string; department: { id: string; name: string; } | null; status: ReimbursementStatus; createdAt: string | null; submittedAt: string | null; reimbursementDate: string | null; totalAmount: string | null; description: string | null; travelSummary: { isTravel: boolean; purpose: string | null; location: string | null; startDate: string | null; endDate: string | null; }; payment: { paymentVoucherNo: string | null; paymentDate: string | null; paymentChannel: string | null; paidAt: string | null; paidBy: { id: string; employeeNo: string; name: string; } | null; }; projectId: string | null; linkedTravelRequestId: string | null; linkedTravelRequest: LinkedTravelRequestSummary | null; project: { id: string; projectNo: string; name: string; } | null; owner: { id: string; employee_no: string; name: string; } | null; submitter: { id: string; employee_no: string; name: string; } | null; invoices: Array<{ id: string; status: string; invoiceCode: string | null; invoiceNumber: string | null; invoiceDate: string | null; sellerName: string | null; buyerName: string | null; documentCategory: string | null; documentSubtype: string | null; totalAmount: string | null; originalName: string | null; mimeType: string | null; fileSize: number | null; linkedItemId: string | null; amountEvidenceStatus: "verified" | "manual_review" | "ocr_failed"; }>; attachments: Array<{ id: string; originalName: string; mimeType: string; fileSize: number; fileSha256: string; } & AttachmentGovernanceFields & { uploadedBy: { id: string; name: string; employeeNo: string; } | null; createdAt: string | null; }>; budgetHint: ReimbursementBudgetHint | null; pendingWorkflow: { instanceId: string; stepId: string; task: { id: string; step_name: string; assignee_id: string | null; assignee_role: string | null; } | null; } | null; workflowProgress: ReimbursementWorkflowProgress | null; processingProgress: ReimbursementProcessingProgress; approvalHistory: ReimbursementApprovalHistoryItem[]; riskFlags: RiskFlag[]; invoiceGuardEnabled?: boolean; invoiceAmountManualReviewRequired?: boolean; items: ReimbursementItem[]; }; export type ReimbursementListQuery = { page?: number; pageSize?: number; status?: ReimbursementStatus; scope?: "own" | "all"; }; export type ReimbursementStatsQuery = { dateFrom?: string; dateTo?: string; departmentId?: string; }; export type ReimbursementStatsGranularity = "month" | "week"; export type ReimbursementStatsSummary = { totalCount: number; totalAmount: string; byStatus: Record< ReimbursementStatus, { count: number; totalAmount: string; } >; avgProcessingDays: number | null; }; export type ReimbursementStatsTrendItem = { period: string | null; count: number; amount: string; paidCount: number; paidAmount: string; }; export type ReimbursementTopExpenseTypeItem = { expenseTypeId: string; expenseTypeName: string; count: number; totalAmount: string; }; export type ReimbursementApprovalBottleneckItem = { assigneeId: string | null; assigneeName: string; pendingCount: number; oldestPendingDays: number; }; export type ReimbursementPayPayload = { paymentVoucherNo?: string; paymentDate?: string; paymentChannel?: "bank" | "alipay" | "wechat" | "cash" | "other"; paymentNote?: string; }; export type ReimbursementCreatePayload = { ownerEmployeeId?: string; reimbursementDate?: string; description?: string; templateId?: string; linkedTravelRequestId?: string; travelPurpose?: string; travelLocation?: string; travelStartDate?: string; travelEndDate?: string; }; export type ReimbursementTravelSummaryPayload = { travelPurpose: string | null; travelLocation: string | null; travelStartDate: string | null; travelEndDate: string | null; }; export type ReimbursementUpdatePayload = Partial & { reimbursementDate?: string; description?: string | null; }; export type ReimbursementCreateResponse = { id: string; formNumber: string | null; submitterId: string; ownerId: string; reimbursementDate: string | null; status: ReimbursementStatus; description: string | null; projectId: string | null; project: { id: string; projectNo: string; name: string; } | null; totalAmount: string; linkedTravelRequestId: string | null; linkedTravelRequest: LinkedTravelRequestSummary | null; items: ReimbursementItem[]; }; export type LinkableTravelRequestSummary = { id: string; requestNo: string | null; purpose: string; startDate: string | null; endDate: string | null; estimatedAmount: string; }; export type LinkableTravelRequestListResponse = { ownerEmployeeId: string; items: LinkableTravelRequestSummary[]; }; export type ReimbursementItemPayload = { expenseTypeId: string | null; expenseDate: string; amount: string; description?: string; assetPriorApprovalBasis?: string | null; }; export type ReimbursementItemMutationResponse = { item: { id: string; expenseTypeId: string | null; expenseDate: string; amount: string; description: string | null; assetPriorApprovalBasis?: string | null; }; totalAmount: string; }; export type ReimbursementItemDeleteResponse = { outcome: "deleted"; totalAmount: string; }; export type ReimbursementDeleteResponse = { reimbursementId: string; outcome: "deleted"; unboundInvoiceCount: number; }; export type ReimbursementSubmitResponse = { reimbursementId: string; formNumber: string; workflowInstanceId: string; currentTaskId: string; status: "reviewing"; budget: { budgetId: string; budgetCode: string; controlMode: "warn" | "block" | "manual_review"; usagePercent: number; exceededThreshold: boolean; exceededBudget: boolean; } | null; precheckDecision: "pass" | "warn" | "manual_review"; standardWarnings: ExpenseStandardCheck[]; openedRiskFlagCount: number; invoiceAmountManualReviewRequired: boolean; manualReviewItemCount: number; }; function toQueryString(query: Record) { const params = new URLSearchParams(); for (const [key, value] of Object.entries(query)) { if (value == null || value === "") { continue; } params.set(key, String(value)); } const encoded = params.toString(); return encoded ? `?${encoded}` : ""; } export async function fetchReimbursements(query: ReimbursementListQuery = {}) { return apiRequestData( `/api/reimbursements${toQueryString(query as Record)}`, ); } export async function fetchReimbursementStatsSummary(query: ReimbursementStatsQuery = {}) { return apiRequestData( `/api/reimbursements/stats/summary${toQueryString(query as Record)}`, ); } export async function fetchReimbursementStatsTrend( query: ReimbursementStatsQuery & { granularity?: ReimbursementStatsGranularity } = {}, ) { return apiRequestData<{ items: ReimbursementStatsTrendItem[] }>( `/api/reimbursements/stats/trend${toQueryString(query as Record)}`, ); } export async function fetchReimbursementTopExpenseTypes( query: ReimbursementStatsQuery & { limit?: number } = {}, ) { return apiRequestData<{ items: ReimbursementTopExpenseTypeItem[] }>( `/api/reimbursements/stats/top-expense-types${toQueryString(query as Record)}`, ); } export async function fetchReimbursementApprovalBottleneck(query: ReimbursementStatsQuery = {}) { return apiRequestData<{ items: ReimbursementApprovalBottleneckItem[] }>( `/api/reimbursements/stats/approval-bottleneck${toQueryString(query as Record)}`, ); } export async function fetchReimbursementDetail(reimbursementId: string) { return apiRequestData(`/api/reimbursements/${reimbursementId}`); } export async function createReimbursement(payload: ReimbursementCreatePayload = {}) { return apiRequestData(`/api/reimbursements`, { method: "POST", body: JSON.stringify(payload), }); } export async function fetchLinkableTravelRequests(query: { ownerEmployeeId?: string } = {}) { return apiRequestData( `/api/reimbursements/linkable-travel-requests${toQueryString(query as Record)}`, ); } export async function precheckReimbursement(reimbursementId: string) { return apiRequestData(`/api/reimbursements/${reimbursementId}/precheck`, { method: "POST", body: JSON.stringify({}), }); } export async function submitReimbursement(reimbursementId: string) { return apiRequestData(`/api/reimbursements/${reimbursementId}/submit`, { method: "POST", body: JSON.stringify({}), }); } export async function linkReimbursementInvoiceItem( reimbursementId: string, invoiceId: string, linkedItemId: string, ) { return apiRequestData<{ reimbursementId: string; invoiceId: string; linkedItemId: string; }>(`/api/reimbursements/${reimbursementId}/invoices/${invoiceId}/item-link`, { method: "PATCH", body: JSON.stringify({ linkedItemId }), }); } export async function withdrawReimbursement(reimbursementId: string, comment?: string) { return apiRequestData>(`/api/reimbursements/${reimbursementId}/withdraw`, { method: "POST", body: JSON.stringify({ comment }), }); } export async function payReimbursement(reimbursementId: string, payload: ReimbursementPayPayload = {}) { return apiRequestData>(`/api/reimbursements/${reimbursementId}/pay`, { method: "POST", body: JSON.stringify(payload), }); } export async function deleteReimbursement(reimbursementId: string) { return apiRequestData(`/api/reimbursements/${reimbursementId}`, { method: "DELETE", }); } export async function updateReimbursementTravelSummary( reimbursementId: string, payload: ReimbursementTravelSummaryPayload, ) { return updateReimbursement(reimbursementId, payload); } export async function updateReimbursement( reimbursementId: string, payload: ReimbursementUpdatePayload, ) { return apiRequestData<{ id: string; status: ReimbursementStatus; reimbursementDate: string; description: string | null; totalAmount: string; travelSummary: Omit; }>(`/api/reimbursements/${reimbursementId}`, { method: "PATCH", body: JSON.stringify(payload), }); } export async function createReimbursementItem( reimbursementId: string, payload: ReimbursementItemPayload, ) { return apiRequestData( `/api/reimbursements/${reimbursementId}/items`, { method: "POST", body: JSON.stringify(payload), }, ); } export async function updateReimbursementItem( reimbursementId: string, itemId: string, payload: Partial, ) { return apiRequestData( `/api/reimbursements/${reimbursementId}/items/${itemId}`, { method: "PATCH", body: JSON.stringify(payload), }, ); } export async function deleteReimbursementItem(reimbursementId: string, itemId: string) { return apiRequestData( `/api/reimbursements/${reimbursementId}/items/${itemId}`, { method: "DELETE", }, ); } export const MAX_REIMBURSEMENT_ATTACHMENT_FILES = 5; export async function uploadReimbursementAttachments(reimbursementId: string, files: File[]) { const formData = new FormData(); for (const file of files) { formData.append("files", file); } return apiRequestData<{ items: ReimbursementDetail["attachments"] }>( `/api/reimbursements/${reimbursementId}/attachments`, { method: "POST", body: formData, }, ); } export async function deleteReimbursementAttachment(reimbursementId: string, attachmentId: string, reason: string) { return apiRequestData<{ attachmentId: string; outcome: string }>( `/api/reimbursements/${reimbursementId}/attachments/${attachmentId}`, { method: "DELETE", body: JSON.stringify({ reason }), }, ); } export function reimbursementAttachmentFileUrl(reimbursementId: string, attachmentId: string) { return `/api/reimbursements/${reimbursementId}/attachments/${attachmentId}/file`; } export async function downloadReimbursementAttachment(reimbursementId: string, attachmentId: string, reason?: string) { return apiRequestBlob(buildAttachmentFileUrl(reimbursementAttachmentFileUrl(reimbursementId, attachmentId), reason)); } export async function fetchReimbursementAttachmentPreviewUrl( reimbursementId: string, attachmentId: string, reason?: string, ) { return apiRequestData<{ signedUrl: string; expiresAt: string; originalName: string; mimeType: string; fileSize: number; }>(`/api/reimbursements/${reimbursementId}/attachments/${attachmentId}/preview-url`, { method: "POST", body: JSON.stringify(reason ? { reason } : {}), }); } export type ReimbursementMaterialPreviewBundle = { expiresAt: string; signedUrl: string; originalName: string; mimeType: "application/pdf"; fileSize: number; pageCount: number; }; export async function fetchReimbursementMaterialPreviewBundle( reimbursementId: string, reason?: string, ) { return apiRequestData( `/api/reimbursements/${reimbursementId}/materials/preview-bundle`, { method: "POST", body: JSON.stringify(reason ? { reason } : {}), }, ); } export async function linkReimbursementProject(reimbursementId: string, projectId: string | null) { return apiRequestData<{ projectId: string | null; project: { id: string; projectNo: string; name: string } | null }>(`/api/reimbursements/${reimbursementId}/link-project`, { method: "POST", body: JSON.stringify({ projectId }), }); }