fix: keep submission previews user-readable

This commit is contained in:
2026-08-20 01:13:36 +00:00
parent ecb2f570e0
commit 7fbd13bf20
12 changed files with 485 additions and 23 deletions
+158
View File
@@ -1108,6 +1108,164 @@ test("Xft application templates support API submission with live post-write stat
assert.equal(travelPrepared.status, "confirmation_required");
});
test("Xft preparation preview resolves every selection to user-readable business names", async () => {
const template: XftSubmissionTemplate = {
eventSeqId: "event-travel", eventNumber: "XFT-TRAVEL", eventName: "差旅报销单",
eventType: "TRAVEL_REIM", kind: "travel_reimburse",
};
const fields: XftSubmissionField[] = [
{
fieldNumber: "C-TOPIC", name: "申请事项", type: "SELECT", required: true,
options: [{ code: "PROCUREMENT", label: "采购事项" }],
selectionCapability: { status: "supported", userInput: "label", submitValue: "code", exposeInternalValue: false },
},
{
fieldNumber: "C-TABLE", name: "补充明细", type: "TABLE", required: true,
itemFields: [{
titleCode: "T-TYPE", label: "明细类型", type: "SELECT", required: true,
options: [{ code: "MARKETING", label: "市场活动" }],
selectionCapability: { status: "supported", userInput: "label", submitValue: "code", exposeInternalValue: false },
}],
},
];
const standardFields = [
{ key: "reimAmount", label: "报销总金额", type: "decimal" },
{ key: "reimAmountCurrencyCode", label: "报销币种", type: "string", enumLabels: { CNY: "人民币" } },
{
key: "expenseList", label: "费用明细", type: "array", itemFields: [
{ key: "expTypeCode", label: "费用类型", type: "string", optionSource: { kind: "expense_type" } },
{ key: "currencyCode", label: "费用币种", type: "string", enumLabels: { CNY: "人民币" } },
{ key: "originalCurrencyReimAmt", label: "费用金额", type: "decimal" },
{ key: "payCityCode", label: "消费城市", type: "string", optionSource: { kind: "city" } },
],
},
{
key: "businessTrip", label: "差旅行程", type: "object", fields: [{
key: "businessTripDetails", label: "行程明细", type: "array", itemFields: [
{ key: "departCityCode", label: "出发城市", type: "string", optionSource: { kind: "city" } },
{ key: "destinationCityCode", label: "目的城市", type: "string", optionSource: { kind: "city" } },
{ key: "beginTime", label: "出发时间", type: "datetime" },
{ key: "endTime", label: "结束时间", type: "datetime" },
{ key: "travelMode", label: "出行方式", type: "comma_separated_enum", multiple: true, enumLabels: { AirPlane: "飞机", Train: "火车" } },
{ key: "roundTrip", label: "是否往返", type: "string", enumLabels: { Y: "往返", N: "单程" } },
],
}],
},
{ key: "customFieldValues", userVisible: false, type: "array" },
];
const optionCalls: Array<{ kind: string; query: string }> = [];
const xft = {
async listSubmissionTemplates() { return [template]; },
async getSubmissionSchema() {
return { template, fields, inputContract: { values: { standardFields } }, diagnostics: {} };
},
async getSubmissionOptions(_user: unknown, _template: unknown, kind: string, query: string) {
optionCalls.push({ kind, query });
const labels: Record<string, string> = {
"440300": "深圳市", "310100": "上海市", MEAL: "餐饮费",
};
return {
kind,
items: labels[query] ? [{ code: query, label: labels[query] }] : [],
diagnostics: {},
};
},
async validateSubmissionIdentity() { return { validated: true, diagnostics: {} }; },
};
const service = new UnifiedApprovalService(config(), {} as ApprovalService, xft as never);
const user = { userId: "user-1" };
const templateId = String((await service.listSubmissionTemplates(user, "xft", 10)).items[0]?.templateId);
const prepared = await service.prepareSubmission(user, {
templateId,
values: {
reimAmount: "100.00",
reimAmountCurrencyCode: "CNY",
expenseList: [{
expTypeCode: "MEAL", currencyCode: "CNY", originalCurrencyReimAmt: "100.00", payCityCode: "440300",
}],
businessTrip: { businessTripDetails: [{
departCityCode: "440300", destinationCityCode: "310100",
beginTime: "2026-08-20 09:00", endTime: "2026-08-21 18:00",
travelMode: "AirPlane,Train", roundTrip: "Y",
}] },
customFieldValues: [
{ fieldNumber: "C-TOPIC", fieldValue: "PROCUREMENT" },
{ fieldNumber: "C-TABLE", fieldValue: JSON.stringify([{ sequence: 1, columnValues: [{ titleCode: "T-TYPE", value: "MARKETING" }] }]) },
],
},
});
assert.equal(prepared.status, "confirmation_required");
const summary = JSON.stringify(prepared.preview.formSummary);
for (const label of ["深圳市", "上海市", "餐饮费", "人民币", "飞机", "火车", "往返", "采购事项", "市场活动"]) {
assert.match(summary, new RegExp(label));
}
for (const internal of ["440300", "310100", "MEAL", "C-TOPIC", "PROCUREMENT", "CNY", "T-TYPE", "MARKETING"]) {
assert.doesNotMatch(summary, new RegExp(internal));
}
assert.deepEqual(optionCalls, [
{ kind: "expense_type", query: "MEAL" },
{ kind: "city", query: "440300" },
{ kind: "city", query: "310100" },
]);
});
test("Xft preparation fails before confirmation when a selected code has no readable mapping", async () => {
const template: XftSubmissionTemplate = {
eventSeqId: "event-expense", eventNumber: "XFT-EXPENSE", eventName: "费用申请单",
eventType: "EXPENSE_APPLY", kind: "expense_apply",
};
const xft = {
async listSubmissionTemplates() { return [template]; },
async getSubmissionSchema() {
return {
template,
fields: [],
inputContract: { values: { standardFields: [{
key: "applyExpenses", label: "事前申请费用列表", type: "array", itemFields: [
{ key: "expenseNumber", label: "费用类型", type: "string", optionSource: { kind: "expense_type" } },
{ key: "amount", label: "申请金额", type: "decimal" },
],
}] } },
diagnostics: {},
};
},
async getSubmissionOptions() { return { kind: "expense_type", items: [], diagnostics: {} }; },
async validateSubmissionIdentity() { return { validated: true, diagnostics: {} }; },
};
const service = new UnifiedApprovalService(config(), {} as ApprovalService, xft as never);
const user = { userId: "user-1" };
const templateId = String((await service.listSubmissionTemplates(user, "xft", 10)).items[0]?.templateId);
await assert.rejects(
service.prepareSubmission(user, {
templateId, values: { applyExpenses: [{ expenseNumber: "UNKNOWN", amount: "10.00" }] },
}),
/无法映射为业务名称.*重新查询可用名称/,
);
});
test("Xft use-car application is explicitly client-only and cannot be prepared", async () => {
const template: XftSubmissionTemplate = {
eventSeqId: "event-car", eventNumber: "XFT-CAR", eventName: "用车申请单",
eventType: "USE_CAR_APPLY", kind: "use_car_apply",
};
const xft = {
async listSubmissionTemplates() { return [template]; },
async getSubmissionSchema() {
return { template, fields: [], inputContract: { values: { standardFields: [] } }, diagnostics: {} };
},
};
const service = new UnifiedApprovalService(config(), {} as ApprovalService, xft as never);
const user = { userId: "user-1" };
const templateId = String((await service.listSubmissionTemplates(user, "xft", 10)).items[0]?.templateId);
const schema = await service.getSubmissionSchema(user, templateId);
assert.equal(schema.submissionCapability.status, "client_only");
assert.match(String(schema.submissionCapability.reason), /用车信息对象.*未提供可验证/);
await assert.rejects(
service.prepareSubmission(user, { templateId, values: { useCarInfo: { purpose: "客户拜访" } } }),
/用车申请.*薪福通客户端/,
);
});
test("Xft preparation reports standard and live custom required fields together", async () => {
const template: XftSubmissionTemplate = {
eventSeqId: "1", eventNumber: "XFT-E", eventName: "招待费用申请单",