import { apiRequestBlob, apiRequestData } from "./http"; import type { AttachmentGovernanceFields } from "../lib/attachmentGovernance"; import type { PrecheckResponse, RiskFlag } from "./feeControl"; export type PurchaseRequestStatus = "draft" | "reviewing" | "approved" | "rejected" | "cancelled"; export type PurchaseRequestSummary = { id: string; requestNo: string | null; applicantId: string; submitterId: string | null; departmentId: string; legalEntityId: string | null; costCenterId: string | null; supplierId: string | null; documentCategoryId: string | null; budgetPoolId: string | null; projectId: string | null; title: string; reason: string | null; currencyCode: string; totalAmount: string; expectedDeliveryDate: string | null; status: PurchaseRequestStatus; createdAt: string | null; updatedAt: string | null; applicant: { id: string; employeeNo: string; name: string; } | null; submitter: { id: string; employeeNo: string; name: string; } | null; department: { id: string; name: string; } | null; legalEntity: { id: string; code: string; name: string; } | null; costCenter: { id: string; code: string; name: string; legalEntityId: string; } | null; supplier: { id: string; code: string; name: string; } | null; documentCategory: { id: string; code: string; name: string; entityType: string; } | null; budgetPool: { id: string; code: string; name: string; } | null; project: { id: string; projectNo: string; name: string; status: string; } | null; }; export type PurchaseRequestItem = { id: string; description: string; specification: string | null; quantity: string; unitPrice: string; lineAmount: string; isAssetCandidate: boolean; createdAt: string | null; updatedAt: string | null; }; export type PurchaseAttachment = { id: string; originalName: string; mimeType: string; fileSize: number; fileSha256: string | null; } & AttachmentGovernanceFields & { uploadedBy: string; createdAt: string | null; uploader: { id: string; employeeNo: string; name: string; } | null; }; export type PurchaseArchivedFile = { storagePath: string | null; originalName: string | null; mimeType: string | null; fileSize: number; }; export type PurchaseOrderArchive = { id: string; poNo: string; contractNo: string | null; archivedAt: string | null; archivedBy: string; archivedByEmployee: { id: string; employeeNo: string; name: string; } | null; archivedFiles: PurchaseArchivedFile[]; }; export type PurchaseErpTrackStatus = | "not_triggered" | "pending" | "processing" | "succeeded" | "failed" | "dead_letter" | "skipped"; export type PurchaseErpBinding = { id: string; provider: string; entityType: string; localId: string; externalId: string | null; externalCode: string | null; lastSyncedAt: string | null; }; export type PurchaseErpOutbox = { id: string; eventType: string; status: string; attempts: number; lastError: string | null; processedAt: string | null; updatedAt: string | null; }; export type PurchaseErpTrackStage = { eligible: boolean; reason: string | null; status: PurchaseErpTrackStatus | string; preview: Record; outbox: PurchaseErpOutbox | null; binding?: PurchaseErpBinding | null; bindings?: { po: PurchaseErpBinding | null; contract: PurchaseErpBinding | null; }; }; export type PurchaseRequestErpTrack = { purchaseRequest: { id: string; requestNo: string | null; status: string; totalAmount: string; supplier: { id: string; code: string | null; name: string | null; } | null; archiveOrder: { id: string; poNo: string; contractNo: string | null; archivedAt: string | null; } | null; }; stages: { commitmentJournal: PurchaseErpTrackStage; accrualJournal: PurchaseErpTrackStage; archiveSummary: PurchaseErpTrackStage; }; permissions: { canRetry: boolean; }; }; export type PurchaseRequestErpRequeueTarget = "commitment_journal" | "accrual_journal" | "archive_summary"; export type PurchaseRequestDetail = PurchaseRequestSummary & { items: PurchaseRequestItem[]; attachments: PurchaseAttachment[]; pendingWorkflow: { instanceId: string; currentStatus: string; currentStepId: string; task: { id: string; stepId: string; stepName: string; assigneeId: string | null; assigneeRole: string | null; } | null; } | null; approvalHistory: Array<{ id: string; taskId: string | null; actorId: string; actorName: string | null; actorEmployeeNo: string | null; action: string; comment: string | null; createdAt: string | null; }>; archiveOrder: PurchaseOrderArchive | null; permissions: { canEditDraft: boolean; canSubmit: boolean; canWithdraw: boolean; canArchiveOrder: boolean; }; riskFlags: RiskFlag[]; }; export type PurchaseRequestListResponse = { page: number; pageSize: number; total: number; mine: boolean; items: PurchaseRequestSummary[]; }; export type PurchaseRequestListQuery = { status?: PurchaseRequestStatus; page?: number; pageSize?: number; mine?: boolean; supplierId?: string; legalEntityId?: string; costCenterId?: string; projectId?: string; }; export type PurchaseRequestItemPayload = { description: string; specification?: string | null; quantity: string; unitPrice: string; isAssetCandidate?: boolean; }; function toQueryString(query: Record) { 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 fetchPurchaseRequests(query: PurchaseRequestListQuery = {}) { return apiRequestData( `/api/purchase-requests${toQueryString(query as Record)}`, ); } export async function createPurchaseRequest() { return apiRequestData<{ purchaseRequest: PurchaseRequestDetail }>("/api/purchase-requests", { method: "POST", body: JSON.stringify({}), }); } export async function fetchPurchaseRequestDetail(purchaseRequestId: string) { return apiRequestData(`/api/purchase-requests/${purchaseRequestId}`); } export async function updatePurchaseRequest( purchaseRequestId: string, payload: Partial<{ title: string; reason: string | null; currencyCode: string; legalEntityId: string | null; costCenterId: string | null; supplierId: string | null; documentCategoryId: string | null; budgetPoolId: string | null; projectId: string | null; expectedDeliveryDate: string | null; items: PurchaseRequestItemPayload[]; }>, ) { return apiRequestData<{ purchaseRequest: PurchaseRequestDetail }>(`/api/purchase-requests/${purchaseRequestId}`, { method: "PATCH", body: JSON.stringify(payload), }); } export async function precheckPurchaseRequest(purchaseRequestId: string) { return apiRequestData(`/api/purchase-requests/${purchaseRequestId}/precheck`, { method: "POST", body: JSON.stringify({}), }); } export async function submitPurchaseRequest(purchaseRequestId: string) { return apiRequestData<{ purchaseRequestId: string; requestNo: string; status: "reviewing"; workflowInstanceId: string; currentTaskId: string; budgetPoolId: string | null; warnings: string[]; budgetReservation: { applied: boolean; reservedAmount: string; }; precheckDecision: "pass" | "warn" | "manual_review"; openedRiskFlagCount: number; }>(`/api/purchase-requests/${purchaseRequestId}/submit`, { method: "POST", body: JSON.stringify({}), }); } export async function withdrawPurchaseRequest(purchaseRequestId: string) { return apiRequestData<{ purchaseRequestId: string; status: "draft"; workflowInstanceId: string | null; budgetRelease: { applied: boolean; items: Array<{ budgetPoolId: string; amount: string }>; }; }>(`/api/purchase-requests/${purchaseRequestId}/withdraw`, { method: "POST", body: JSON.stringify({}), }); } export async function fetchPurchaseOrderArchive(purchaseRequestId: string) { return apiRequestData<{ archiveOrder: PurchaseOrderArchive | null }>(`/api/purchase-requests/${purchaseRequestId}/archive-order`); } export async function archivePurchaseOrder( purchaseRequestId: string, payload: { poNo: string; contractNo?: string | null; archivedFiles?: PurchaseArchivedFile[]; }, ) { return apiRequestData<{ archiveOrder: PurchaseOrderArchive }>(`/api/purchase-requests/${purchaseRequestId}/archive-order`, { method: "POST", body: JSON.stringify(payload), }); } export async function uploadPurchaseAttachment(purchaseRequestId: string, file: File) { const formData = new FormData(); formData.append("file", file); return apiRequestData<{ attachment: PurchaseAttachment }>(`/api/purchase-requests/${purchaseRequestId}/attachments`, { method: "POST", body: formData, }); } export async function deletePurchaseAttachment(purchaseRequestId: string, attachmentId: string) { return apiRequestData<{ outcome: "deleted"; attachmentId: string }>( `/api/purchase-requests/${purchaseRequestId}/attachments/${attachmentId}`, { method: "DELETE", }, ); } export async function downloadPurchaseAttachment(purchaseRequestId: string, attachmentId: string, reason?: string) { return apiRequestBlob( `/api/purchase-requests/${purchaseRequestId}/attachments/${attachmentId}/download`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(reason ? { reason } : {}), }, ); } export async function fetchPurchaseRequestErpTrack(purchaseRequestId: string) { return apiRequestData( `/api/integrations/erp/procurement/purchase-requests/${purchaseRequestId}/track`, ); } export async function requeuePurchaseRequestErpSync( purchaseRequestId: string, payload: { targets: PurchaseRequestErpRequeueTarget[]; }, runNow = true, ) { return apiRequestData<{ items: Array<{ target: PurchaseRequestErpRequeueTarget; outboxId: string; status: string; duplicated: boolean; requeued: boolean; eventType: string; }>; }>( `/api/integrations/erp/procurement/purchase-requests/${purchaseRequestId}/requeue${toQueryString({ runNow })}`, { method: 'POST', body: JSON.stringify(payload), }, ); }