- ErpIntegrationPage Tab 化:主数据同步 / Outbox 队列管理 / ID 映射查看器 - Outbox 分页查询、详情、重试与统计端点 - IntegrationBinding 分页查询、routePath 推导与采购/资产深链 - 报销/采购/资产详情区接入 ERP 同步状态卡片 - 单测覆盖 Outbox 与 Binding 端点
291 行
7.5 KiB
TypeScript
291 行
7.5 KiB
TypeScript
import { apiRequestData } from "./http";
|
|
|
|
export type ErpMasterDataResourceType = "legal_entity" | "cost_center" | "supplier";
|
|
|
|
export type ErpGateStatus = {
|
|
envEnabled: boolean;
|
|
effectiveEnabled: boolean;
|
|
effectiveReason: string;
|
|
provider: string;
|
|
masterDataEndpointConfigured: boolean;
|
|
journalEndpointConfigured: boolean;
|
|
masterDataSyncCron: string | null;
|
|
};
|
|
|
|
export type ErpMasterDataSyncSummary = {
|
|
requested: number;
|
|
created: number;
|
|
updated: number;
|
|
archived: number;
|
|
skipped: number;
|
|
sampleSkipped: Array<{ key: string; reason: string }>;
|
|
};
|
|
|
|
export type ErpMasterDataSyncItem = {
|
|
resourceType: ErpMasterDataResourceType;
|
|
status: string;
|
|
lastStartedAt: string | null;
|
|
lastFinishedAt: string | null;
|
|
lastSuccessAt: string | null;
|
|
lastError: string | null;
|
|
lastResult: ErpMasterDataSyncSummary | null;
|
|
};
|
|
|
|
export type ErpMasterDataStatusResponse = {
|
|
gate: ErpGateStatus;
|
|
items: ErpMasterDataSyncItem[];
|
|
triggerMode?: string;
|
|
};
|
|
|
|
export type ReimbursementJournalPreview = {
|
|
journal: {
|
|
journalType: string;
|
|
sourceEventName: string;
|
|
entityType: string;
|
|
entityId: string;
|
|
documentNo: string | null;
|
|
currencyCode: string;
|
|
totalAmount: string | null;
|
|
occurredAt: string | null;
|
|
paymentVoucherNo: string | null;
|
|
paymentDate: string | null;
|
|
dimensions: {
|
|
departmentId: string | null;
|
|
ownerEmployeeId: string | null;
|
|
submitterEmployeeId: string | null;
|
|
legalEntityCode: string | null;
|
|
costCenterCode: string | null;
|
|
};
|
|
};
|
|
exportState: {
|
|
externalId: string | null;
|
|
lastOutboxId: string | null;
|
|
lastOutboxStatus: string | null;
|
|
lastError: string | null;
|
|
exportedAt: string | null;
|
|
};
|
|
};
|
|
|
|
export type IntegrationOutboxStatus =
|
|
| "pending"
|
|
| "processing"
|
|
| "succeeded"
|
|
| "failed"
|
|
| "dead_letter";
|
|
|
|
export type IntegrationOutboxPayloadSummary = {
|
|
entityType: string | null;
|
|
entityId: string | null;
|
|
sourceEventName: string | null;
|
|
journalType: string | null;
|
|
exportType: string | null;
|
|
identityCode: string | null;
|
|
};
|
|
|
|
export type IntegrationOutboxItem = {
|
|
id: string;
|
|
eventType: string;
|
|
status: IntegrationOutboxStatus | string;
|
|
payloadSummary: IntegrationOutboxPayloadSummary;
|
|
attempts: number;
|
|
maxAttempts: number;
|
|
nextRetryAt: string | null;
|
|
createdAt: string | null;
|
|
processedAt: string | null;
|
|
deadLetterAt: string | null;
|
|
};
|
|
|
|
export type IntegrationOutboxDetail = IntegrationOutboxItem & {
|
|
payload: Record<string, unknown> | null;
|
|
lastError: string | null;
|
|
};
|
|
|
|
export type IntegrationOutboxListResponse = {
|
|
page: number;
|
|
pageSize: number;
|
|
total: number;
|
|
items: IntegrationOutboxItem[];
|
|
};
|
|
|
|
export type IntegrationOutboxStats = {
|
|
pending: number;
|
|
processing: number;
|
|
succeeded: number;
|
|
failed: number;
|
|
deadLetter: number;
|
|
};
|
|
|
|
export type IntegrationOutboxQuery = {
|
|
page?: number;
|
|
pageSize?: number;
|
|
status?: IntegrationOutboxStatus;
|
|
eventType?: string;
|
|
};
|
|
|
|
export type IntegrationBindingItem = {
|
|
id: string;
|
|
provider: string;
|
|
entityType: string;
|
|
localId: string;
|
|
externalId: string | null;
|
|
externalCode: string | null;
|
|
lastSyncedAt: string | null;
|
|
routePath: string | null;
|
|
};
|
|
|
|
export type IntegrationBindingListResponse = {
|
|
page: number;
|
|
pageSize: number;
|
|
total: number;
|
|
items: IntegrationBindingItem[];
|
|
};
|
|
|
|
export type IntegrationBindingQuery = {
|
|
page?: number;
|
|
pageSize?: number;
|
|
provider?: string;
|
|
entityType?: string;
|
|
};
|
|
|
|
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 triggerErpSync(reimbursementId: string, runNow = true) {
|
|
return apiRequestData<{
|
|
outboxId: string;
|
|
status: string;
|
|
duplicated: boolean;
|
|
}>(`/api/integrations/erp/sync/${reimbursementId}?runNow=${runNow ? "1" : "0"}`, {
|
|
method: "POST",
|
|
body: JSON.stringify({}),
|
|
});
|
|
}
|
|
|
|
export async function fetchErpMasterDataStatus() {
|
|
return apiRequestData<ErpMasterDataStatusResponse>("/api/integrations/erp/master-data/status");
|
|
}
|
|
|
|
export async function syncErpMasterData(payload: { resources?: ErpMasterDataResourceType[] }) {
|
|
return apiRequestData<ErpMasterDataStatusResponse>("/api/integrations/erp/master-data/sync", {
|
|
method: "POST",
|
|
body: JSON.stringify(payload),
|
|
});
|
|
}
|
|
|
|
export async function previewReimbursementJournal(reimbursementId: string) {
|
|
return apiRequestData<ReimbursementJournalPreview>(
|
|
`/api/integrations/erp/journals/reimbursements/${reimbursementId}/preview`,
|
|
);
|
|
}
|
|
|
|
export async function exportReimbursementJournal(reimbursementId: string, runNow = true) {
|
|
return apiRequestData<{
|
|
outboxId: string;
|
|
status: string;
|
|
duplicated: boolean;
|
|
requeued: boolean;
|
|
eventType: string;
|
|
}>(`/api/integrations/erp/journals/reimbursements/${reimbursementId}/export?runNow=${runNow ? "1" : "0"}`, {
|
|
method: "POST",
|
|
body: JSON.stringify({}),
|
|
});
|
|
}
|
|
|
|
export async function fetchIntegrationOutbox(query: IntegrationOutboxQuery = {}) {
|
|
return apiRequestData<IntegrationOutboxListResponse>(
|
|
`/api/integrations/outbox${toQueryString({
|
|
page: query.page,
|
|
pageSize: query.pageSize,
|
|
status: query.status,
|
|
event_type: query.eventType,
|
|
})}`,
|
|
);
|
|
}
|
|
|
|
export async function fetchIntegrationOutboxDetail(outboxId: string) {
|
|
const response = await apiRequestData<{ item: IntegrationOutboxDetail }>(`/api/integrations/outbox/${outboxId}`);
|
|
return response.item;
|
|
}
|
|
|
|
export async function retryIntegrationOutbox(outboxId: string) {
|
|
const response = await apiRequestData<{ item: IntegrationOutboxDetail }>(`/api/integrations/outbox/${outboxId}/retry`, {
|
|
method: "POST",
|
|
body: JSON.stringify({}),
|
|
});
|
|
return response.item;
|
|
}
|
|
|
|
export async function fetchIntegrationOutboxStats() {
|
|
return apiRequestData<IntegrationOutboxStats>("/api/integrations/outbox/stats");
|
|
}
|
|
|
|
export async function fetchIntegrationBindings(query: IntegrationBindingQuery = {}) {
|
|
return apiRequestData<IntegrationBindingListResponse>(
|
|
`/api/integrations/bindings${toQueryString({
|
|
page: query.page,
|
|
pageSize: query.pageSize,
|
|
provider: query.provider,
|
|
entity_type: query.entityType,
|
|
})}`,
|
|
);
|
|
}
|
|
|
|
export async function importTravelOrders(payload: {
|
|
orders: Array<{
|
|
source: string;
|
|
externalOrderNo: string;
|
|
employeeId: string;
|
|
linkedReimbursementId?: string;
|
|
occurredAt?: string;
|
|
amount?: number;
|
|
currency?: string;
|
|
}>;
|
|
}) {
|
|
return apiRequestData<{
|
|
requested: number;
|
|
imported: number;
|
|
skipped: Array<{ source: string; externalOrderNo: string; reason: string }>;
|
|
items: Array<{
|
|
id: string;
|
|
source: string;
|
|
externalOrderNo: string;
|
|
employeeId: string;
|
|
linkedReimbursementId: string | null;
|
|
}>;
|
|
}>(`/api/travel/orders/import`, {
|
|
method: "POST",
|
|
body: JSON.stringify(payload),
|
|
});
|
|
}
|
|
|
|
export async function testNotificationChannel(payload: {
|
|
channelType: string;
|
|
name: string;
|
|
}) {
|
|
return apiRequestData<{
|
|
channel: {
|
|
id: string;
|
|
channelType: string;
|
|
name: string;
|
|
enabled: boolean;
|
|
};
|
|
testResult: {
|
|
ok: boolean;
|
|
code: string;
|
|
response?: Record<string, unknown>;
|
|
};
|
|
}>(`/api/notifications/channels/test`, {
|
|
method: "POST",
|
|
body: JSON.stringify(payload),
|
|
});
|
|
}
|