430 行
11 KiB
TypeScript
430 行
11 KiB
TypeScript
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<string, unknown>;
|
|
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<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 fetchPurchaseRequests(query: PurchaseRequestListQuery = {}) {
|
|
return apiRequestData<PurchaseRequestListResponse>(
|
|
`/api/purchase-requests${toQueryString(query as Record<string, string | number | boolean | undefined>)}`,
|
|
);
|
|
}
|
|
|
|
export async function createPurchaseRequest() {
|
|
return apiRequestData<{ purchaseRequest: PurchaseRequestDetail }>("/api/purchase-requests", {
|
|
method: "POST",
|
|
body: JSON.stringify({}),
|
|
});
|
|
}
|
|
|
|
export async function fetchPurchaseRequestDetail(purchaseRequestId: string) {
|
|
return apiRequestData<PurchaseRequestDetail>(`/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<PrecheckResponse>(`/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<PurchaseRequestErpTrack>(
|
|
`/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),
|
|
},
|
|
);
|
|
}
|