P25L-1~P25L-5 实现预算控制与AP最小可用运营
合同 CSV 已回填 reviewer/QA 结果;运行时回归受当前环境缺少 node/npm 阻塞。
这个提交包含在:
@@ -3,15 +3,16 @@ import type { RiskFlag } from "./feeControl";
|
||||
|
||||
export type ApOperationsSummary = {
|
||||
summary: {
|
||||
openPaymentBatchCount: number;
|
||||
failedPaymentBatchItemCount: number;
|
||||
openRiskFlagCount: number;
|
||||
reconciliationAnomalyCount: number;
|
||||
pendingPaymentCount: number;
|
||||
partialMatchCount: number;
|
||||
exceptionCount: number;
|
||||
budgetOverspendCount: number;
|
||||
};
|
||||
openPaymentBatches: Array<{ id: string; batchNo: string; entityType: string; name: string; status: string; createdAt: string | null }>;
|
||||
pendingPaymentBatches: Array<{ id: string; batchNo: string; entityType: string; name: string; status: string; totalAmount: string; highRisk: boolean; createdAt: string | null }>;
|
||||
failedPaymentBatchItems: Array<{ id: string; paymentBatchId: string; entityId: string; entityCode: string; entityTitle: string; amount: string; currencyCode: string; failureCode: string | null; failureMessage: string | null; createdAt: string | null }>;
|
||||
openRiskFlags: RiskFlag[];
|
||||
reconciliationAnomalies: Array<{ id: string; travelReconciliationBatchId: string; travelOrderId: string | null; travelRequestId: string | null; matchStatus: string; resolutionStatus: string; anomalyCode: string | null; orderAmount: string | null; transactionAmount: string | null; currency: string; createdAt: string | null }>;
|
||||
exceptionRiskFlags: RiskFlag[];
|
||||
budgetOverspendRisks: RiskFlag[];
|
||||
partialMatchItems: Array<{ id: string; travelReconciliationBatchId: string; travelOrderId: string | null; travelRequestId: string | null; matchStatus: string; resolutionStatus: string; anomalyCode: string | null; orderAmount: string | null; transactionAmount: string | null; currency: string; createdAt: string | null }>;
|
||||
};
|
||||
|
||||
export async function fetchApOperations() {
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
import { apiRequestData } from "./http";
|
||||
|
||||
export type BudgetControlRuleEntityType = "all" | "reimbursement" | "purchase_request" | "travel_request";
|
||||
export type BudgetControlMode = "warn" | "block" | "manual_review";
|
||||
|
||||
export type BudgetControlRule = {
|
||||
id: string;
|
||||
name: string;
|
||||
entityType: BudgetControlRuleEntityType;
|
||||
departmentId: string | null;
|
||||
legalEntityId: string | null;
|
||||
costCenterId: string | null;
|
||||
expenseTypeId: string | null;
|
||||
controlMode: BudgetControlMode;
|
||||
priority: number;
|
||||
enabled: boolean;
|
||||
notes: string | null;
|
||||
archivedAt: string | null;
|
||||
createdAt: string | null;
|
||||
updatedAt: string | null;
|
||||
};
|
||||
|
||||
export type BudgetControlRuleListResponse = {
|
||||
items: BudgetControlRule[];
|
||||
};
|
||||
|
||||
function toQueryString(query: Record<string, string | 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" ? String(value) : value);
|
||||
}
|
||||
const encoded = params.toString();
|
||||
return encoded ? `?${encoded}` : "";
|
||||
}
|
||||
|
||||
export async function fetchBudgetControlRules(query: {
|
||||
entityType?: BudgetControlRuleEntityType;
|
||||
enabled?: boolean;
|
||||
} = {}) {
|
||||
return apiRequestData<BudgetControlRuleListResponse>(
|
||||
`/api/budget-control-rules${toQueryString(query as Record<string, string | boolean | undefined>)}`,
|
||||
);
|
||||
}
|
||||
|
||||
export async function createBudgetControlRule(payload: {
|
||||
name: string;
|
||||
entityType: BudgetControlRuleEntityType;
|
||||
departmentId?: string | null;
|
||||
legalEntityId?: string | null;
|
||||
expenseTypeId?: string | null;
|
||||
controlMode: BudgetControlMode;
|
||||
priority?: number;
|
||||
enabled?: boolean;
|
||||
notes?: string | null;
|
||||
}) {
|
||||
return apiRequestData<{ item: BudgetControlRule }>("/api/budget-control-rules", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
}
|
||||
|
||||
export async function patchBudgetControlRule(
|
||||
ruleId: string,
|
||||
payload: Partial<{
|
||||
name: string;
|
||||
entityType: BudgetControlRuleEntityType;
|
||||
departmentId: string | null;
|
||||
legalEntityId: string | null;
|
||||
expenseTypeId: string | null;
|
||||
controlMode: BudgetControlMode;
|
||||
priority: number;
|
||||
enabled: boolean;
|
||||
notes: string | null;
|
||||
}>,
|
||||
) {
|
||||
return apiRequestData<{ item: BudgetControlRule }>(`/api/budget-control-rules/${ruleId}`, {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
}
|
||||
|
||||
export async function archiveBudgetControlRule(ruleId: string) {
|
||||
return apiRequestData<{ item: BudgetControlRule }>(`/api/budget-control-rules/${ruleId}/archive`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({}),
|
||||
});
|
||||
}
|
||||
@@ -132,6 +132,26 @@ export function mapApiErrorMessage(
|
||||
return "当前状态不允许执行付款。";
|
||||
}
|
||||
|
||||
if (details?.review === "maker_checker_conflict") {
|
||||
return "复核人与批次创建人不能是同一人。";
|
||||
}
|
||||
|
||||
if (details?.review === "second_reviewer_conflict") {
|
||||
return "二次复核人不能与首个复核人相同。";
|
||||
}
|
||||
|
||||
if (details?.review === "high_risk_second_review_required") {
|
||||
return "当前为高风险付款批次,完成二次复核后才能执行。";
|
||||
}
|
||||
|
||||
if (details?.review === "batch_review_required") {
|
||||
return "当前批次尚未完成复核,不能执行。";
|
||||
}
|
||||
|
||||
if (details?.review === "batch_not_reviewable") {
|
||||
return "当前批次状态不允许继续复核。";
|
||||
}
|
||||
|
||||
if (details?.transfer === "chain_limit_exceeded") {
|
||||
return "当前任务转审次数已达上限。";
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { apiRequestData } from "./http";
|
||||
|
||||
export type RiskFlag = {
|
||||
id: string;
|
||||
entityType: string;
|
||||
@@ -32,3 +34,13 @@ export type PrecheckResponse = {
|
||||
riskFlags: Array<{ code: string; level: "low" | "medium" | "high"; title: string; detail: string; willOpenOnSubmit: boolean }>;
|
||||
nextActions: string[];
|
||||
};
|
||||
|
||||
export async function precheckEntity(payload: {
|
||||
entityType: "reimbursement" | "purchase_request" | "travel_request";
|
||||
entityId: string;
|
||||
}) {
|
||||
return apiRequestData<PrecheckResponse>("/api/fee-control/precheck", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -32,12 +32,25 @@ export type PaymentBatch = {
|
||||
defaultPaymentDate: string | null;
|
||||
defaultPaymentChannel: PaymentChannel | null;
|
||||
voucherPrefix: string | null;
|
||||
totalAmount: string;
|
||||
highRisk: boolean;
|
||||
reviewRequirement: {
|
||||
thresholdAmount: string;
|
||||
requiredReviewCount: number;
|
||||
completedReviewCount: number;
|
||||
nextStep: "first_review" | "second_review" | "ready_for_execute";
|
||||
makerCheckerRequired: boolean;
|
||||
reviewerPermission: string;
|
||||
};
|
||||
reviewComment: string | null;
|
||||
secondaryReviewComment: string | null;
|
||||
executionSummaryJson: Record<string, unknown> | null;
|
||||
createdAt: string | null;
|
||||
createdBy: string | null;
|
||||
reviewedAt: string | null;
|
||||
reviewedBy: string | null;
|
||||
secondaryReviewedAt: string | null;
|
||||
secondaryReviewedBy: string | null;
|
||||
executedAt: string | null;
|
||||
executedBy: string | null;
|
||||
cancelledAt: string | null;
|
||||
|
||||
在新工单中引用
屏蔽一个用户