文件
hyb-oa/client/src/api/invoicePool.ts
T
wangsli e5685f18b2
ci-e2e / e2e (push) Canceled after 0s
ci-minimal / check-minimal (push) Canceled after 0s
feat: 选票自动生成报销明细并支持集中分类(08-29-reimbursement-expense-policy-invoice-guard)
2026-09-09 23:19:02 +08:00

356 行
9.4 KiB
TypeScript

import { apiRequestData } from "./http";
export type InvoicePoolStatus = "pending_ocr" | "ocr_failed" | "available" | "bound" | "archived";
export type InvoiceVerificationRecord = {
id: string;
provider: string;
status: string;
resultCode: string | null;
resultMessage: string | null;
errorCode: string | null;
errorMessage: string | null;
durationMs: number | null;
requestedBy: string | null;
requestedAt: string | null;
completedAt: string | null;
retryOfId: string | null;
attempts: number;
};
export type InvoicePoolListItem = {
id: string;
status: InvoicePoolStatus;
availabilityReason: string | null;
availabilityMessage: string | null;
ocrType: string;
ocrProvider: string;
ocrAttempts: number;
ocrLastError: string | null;
ocrCompletedAt: string | null;
documentCategory: string | null;
documentSubtype: string | null;
invoiceCode: string | null;
invoiceNumber: string | null;
invoiceDate: string | null;
amount: string | null;
taxAmount: string | null;
totalAmount: string | null;
linkedReimbursementId: string | null;
linkedItemId: string | null;
linkedTravelOrderId: string | null;
originalName: string;
mimeType: string;
fileSize: number;
verification: InvoiceVerificationRecord | null;
owner: {
id: string;
employeeNo: string;
name: string;
} | null;
uploadedBy: {
id: string;
employeeNo: string;
name: string;
} | null;
createdAt: string | null;
updatedAt: string | null;
};
export type InvoicePoolListResponse = {
page: number;
pageSize: number;
total: number;
ownerScope: "own" | "all";
items: InvoicePoolListItem[];
};
export type UploadInvoiceResponse = {
invoicePoolId: string | null;
status: InvoicePoolStatus;
fileSha256: string;
uploadAction?: "create" | "reuse_existing" | "blocked";
unavailableReason?: string | null;
unavailableMessage?: string | null;
availabilityReason?: string | null;
availabilityMessage?: string | null;
};
export type InvoicePoolDetail = {
id: string;
status: InvoicePoolStatus;
availabilityReason: string | null;
availabilityMessage: string | null;
archivedReason: string | null;
archivedAt: string | null;
ownerEmployeeId: string;
uploadedByEmployeeId: string;
originalName: string | null;
mimeType: string | null;
fileSize: number | null;
fileSha256: string | null;
remarks: string | null;
ocrType: string;
ocrProvider: string;
ocrAttempts: number;
ocrLastRequestedAt: string | null;
ocrLastError: string | null;
ocrCompletedAt: string | null;
documentCategory: string | null;
documentSubtype: string | null;
invoiceCode: string | null;
invoiceNumber: string | null;
invoiceDate: string | null;
sellerName: string | null;
buyerName: string | null;
amount: string | null;
taxAmount: string | null;
totalAmount: string | null;
linkedReimbursementId: string | null;
linkedItemId: string | null;
linkedTravelOrderId: string | null;
isDuplicateOverride: boolean;
overrideRefFormNumber: string | null;
overrideReason: string | null;
overriddenBy: string | null;
verification: InvoiceVerificationRecord | null;
createdAt: string | null;
updatedAt: string | null;
};
export type InvoicePoolListQuery = {
page?: number;
pageSize?: number;
status?: InvoicePoolStatus;
ownerScope?: "own" | "all";
};
export type InvoiceBindPayload =
| {
reimbursementId: string;
linkedItemId?: string;
createItem?: true;
}
| {
travelOrderId: string;
};
export type BindableInvoiceSummary = {
id: 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;
createdAt: string | null;
};
export type BindableInvoiceListResponse = {
items: BindableInvoiceSummary[];
};
export type InvoiceStatusItem = {
id: string;
status: InvoicePoolStatus;
reason: string;
message: string;
availabilityReason: string | null;
availabilityMessage: string | null;
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;
linkedReimbursementId: string | null;
linkedItemId: string | null;
linkedTravelOrderId: string | null;
ocrAttempts: number;
ocrLastError: string | null;
ocrCompletedAt: string | null;
updatedAt: string | null;
};
export type InvoiceStatusResponse = {
items: InvoiceStatusItem[];
missingIds: string[];
};
function toQueryString(query: Record<string, string | number | undefined>) {
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 fetchInvoicePoolList(query: InvoicePoolListQuery = {}) {
return apiRequestData<InvoicePoolListResponse>(
`/api/invoice-pool${toQueryString(query as Record<string, string | number | undefined>)}`,
);
}
export async function fetchInvoicePoolDetail(invoiceId: string) {
return apiRequestData<InvoicePoolDetail>(`/api/invoice-pool/${invoiceId}`);
}
export async function fetchBindableInvoices(query: {
reimbursementId: string;
search?: string;
limit?: number;
}) {
return apiRequestData<BindableInvoiceListResponse>(
`/api/invoice-pool/bindable${toQueryString(query as Record<string, string | number | undefined>)}`,
);
}
export async function fetchInvoiceStatuses(ids: string[]) {
return apiRequestData<InvoiceStatusResponse>(
`/api/invoice-pool/status${toQueryString({ ids: ids.join(",") })}`,
);
}
export async function uploadInvoice(
file: File,
ocrType?: string,
ownerEmployeeId?: string,
options: { signal?: AbortSignal } = {},
) {
const formData = new FormData();
formData.append("file", file);
if (ocrType) {
formData.append("ocr_type", ocrType);
}
if (ownerEmployeeId) {
formData.append("owner_employee_id", ownerEmployeeId);
}
return apiRequestData<UploadInvoiceResponse>(
"/api/invoice-pool/upload",
{
method: "POST",
body: formData,
signal: options.signal,
},
);
}
export async function bindInvoice(
invoiceId: string,
reimbursementIdOrPayload: string | InvoiceBindPayload,
linkedItemId?: string,
) {
const payload =
typeof reimbursementIdOrPayload === "string"
? { reimbursementId: reimbursementIdOrPayload, linkedItemId }
: reimbursementIdOrPayload;
return apiRequestData<Record<string, unknown>>(`/api/invoice-pool/${invoiceId}/bind`, {
method: "POST",
body: JSON.stringify(payload),
});
}
export async function fetchInvoiceSignedUrl(invoiceId: string) {
return apiRequestData<{
signedUrl: string;
expiresAt: string;
originalName: string | null;
mimeType: string | null;
fileSize: number | null;
}>(
`/api/invoice-pool/${invoiceId}/signed-url`,
{
method: "POST",
body: JSON.stringify({}),
},
);
}
export type InvoicePatchPayload = Partial<{
status: "available";
remarks: string | null;
invoiceCode: string | null;
invoiceNumber: string | null;
invoiceDate: string | null;
sellerName: string | null;
buyerName: string | null;
amount: string | null;
taxAmount: string | null;
totalAmount: string | null;
}>;
export async function patchInvoice(invoiceId: string, payload: InvoicePatchPayload) {
return apiRequestData<InvoicePoolDetail>(`/api/invoice-pool/${invoiceId}`, {
method: "PATCH",
body: JSON.stringify(payload),
});
}
export async function archiveInvoice(invoiceId: string) {
return apiRequestData<InvoicePoolDetail>(`/api/invoice-pool/${invoiceId}/archive`, {
method: "POST",
body: JSON.stringify({}),
});
}
export async function unbindInvoice(invoiceId: string) {
return apiRequestData<InvoicePoolDetail>(`/api/invoice-pool/${invoiceId}/unbind`, {
method: "POST",
body: JSON.stringify({}),
});
}
export async function retryInvoiceOcr(invoiceId: string) {
return apiRequestData<InvoicePoolDetail>(`/api/invoice-pool/${invoiceId}/retry-ocr`, {
method: "POST",
body: JSON.stringify({}),
});
}
export async function overrideInvoiceDuplicate(invoiceId: string, payload: { refFormNumber: string; reason: string }) {
return apiRequestData<InvoicePoolDetail>(`/api/invoice-pool/${invoiceId}/override-duplicate`, {
method: "POST",
body: JSON.stringify(payload),
});
}
export async function triggerInvoiceVerification(invoiceId: string) {
return apiRequestData<{ invoiceId: string; verificationId: string; provider: string; triggerMode: string }>(
`/api/invoice-pool/${invoiceId}/verify`,
{
method: "POST",
body: JSON.stringify({}),
},
);
}
export async function fetchInvoiceVerifications(invoiceId: string, limit = 20) {
return apiRequestData<{ invoiceId: string; items: InvoiceVerificationRecord[] }>(
`/api/invoice-pool/${invoiceId}/verification?limit=${limit}`,
);
}
export async function retryInvoiceVerification(invoiceId: string) {
return apiRequestData<{ invoiceId: string; verificationId: string; status: string }>(
`/api/invoice-pool/${invoiceId}/verification/retry`,
{
method: "POST",
body: JSON.stringify({}),
},
);
}