CI / Client installer (macos-latest) (push) Canceled after 0s
CI / Client installer (ubuntu-latest) (push) Canceled after 0s
CI / Client installer (windows-latest) (push) Canceled after 0s
CI / test (22) (push) Canceled after 0s
CI / test (24) (push) Canceled after 0s
CI / release-smoke (push) Canceled after 0s
309 lines
14 KiB
TypeScript
309 lines
14 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import test from "node:test";
|
|
import { browseRawDetail, compactApprovalDetail, extractExpenseItems } from "../src/approval-detail.js";
|
|
|
|
test("extracts DingTalk TableField expense rows into stable agent fields", () => {
|
|
const raw = {
|
|
title: "测试用户甲提交的通用费用报销_北极大",
|
|
createTime: 1_786_179_600_000,
|
|
formComponentValues: [
|
|
{ name: "报销金额", value: "600" },
|
|
{
|
|
name: "报销明细",
|
|
componentType: "TableField",
|
|
value: JSON.stringify([{
|
|
rowValue: [
|
|
{ label: "费用发生日期", value: "2026-08-08" },
|
|
{ label: "费用类型", value: "员工团队保障经费/误餐费" },
|
|
{ label: "费用说明", value: "7月份误餐费" },
|
|
{ label: "实际报销金额", value: "¥600.00" },
|
|
{ label: "消费地点", value: "深圳" },
|
|
{ label: "接待对象", value: "张三、李四" },
|
|
{ label: "发票日期", value: "2026-06-19、2026-08-08" },
|
|
],
|
|
}]),
|
|
},
|
|
],
|
|
};
|
|
assert.deepEqual(extractExpenseItems(raw), [{
|
|
occurredOn: "2026-08-08",
|
|
expenseType: "员工团队保障经费/误餐费",
|
|
amount: 600,
|
|
currency: "CNY",
|
|
description: "7月份误餐费",
|
|
merchant: null,
|
|
location: "深圳",
|
|
customer: null,
|
|
contacts: ["张三", "李四"],
|
|
relatedTripId: null,
|
|
invoiceDates: ["2026-06-19", "2026-08-08"],
|
|
projectId: null,
|
|
crmRecordId: null,
|
|
}]);
|
|
const compact = compactApprovalDetail(raw, "summary");
|
|
assert.equal((compact.detail.expenseItems as unknown[]).length, 1);
|
|
assert.equal(JSON.stringify(compact).length < 20_000, true);
|
|
});
|
|
|
|
test("compact detail omits container placeholders and keeps invoice context in expense view", () => {
|
|
const raw = {
|
|
formComponentValues: [
|
|
{ name: "[]", value: [] },
|
|
{ name: "报销明细", value: [{ rowValue: [
|
|
{ label: "费用发生日期", value: "2026-08-10" },
|
|
{ label: "费用类型", value: "交通费" },
|
|
{ label: "实际报销金额", value: 88 },
|
|
] }] },
|
|
{ name: "发票号码", value: "INV-100" },
|
|
{ name: "付款主体", value: "深圳公司" },
|
|
],
|
|
};
|
|
const summary = compactApprovalDetail(raw, "summary");
|
|
assert.doesNotMatch(JSON.stringify(summary.detail.formFields), /\[0 items\]|\{\d+ fields\}|"\[\]"/);
|
|
const expense = compactApprovalDetail(raw, "expense");
|
|
assert.match(JSON.stringify(expense.detail.supportingFields), /发票号码|付款主体|INV-100|深圳公司/);
|
|
assert.match(JSON.stringify(expense.detail.formFields), /实际报销金额|88/);
|
|
assert.deepEqual(expense.detail.amount, { value: 88, currency: "CNY" });
|
|
assert.equal(expense.detail.amountSource, "expense_items_sum");
|
|
});
|
|
|
|
test("expense view safely sums multiple complete detail rows", () => {
|
|
const raw = { formComponentValues: [{ name: "报销明细", value: [{ rowValue: [
|
|
{ label: "费用类型", value: "交通费" }, { label: "实际报销金额", value: 100.11 },
|
|
] }, { rowValue: [
|
|
{ label: "费用类型", value: "住宿费" }, { label: "实际报销金额", value: 2504.38 },
|
|
] }] }] };
|
|
const expense = compactApprovalDetail(raw, "expense");
|
|
assert.deepEqual(expense.detail.amount, { value: 2604.49, currency: "CNY" });
|
|
assert.equal(expense.detail.amountSource, "expense_items_sum");
|
|
});
|
|
|
|
test("extracts expense fields from source-native XFT business data", () => {
|
|
const raw = {
|
|
form: {
|
|
values: { amount: 4812 },
|
|
businessData: {
|
|
expenseRows: [{
|
|
"费用发生日期": "2026/08/07",
|
|
"费用项目": "客户招待费",
|
|
"费用金额": 4812,
|
|
"消费地点": "上海",
|
|
"接待客户": "示例客户",
|
|
"项目编号": "P-001",
|
|
"CRM记录ID": "CRM-99",
|
|
}],
|
|
},
|
|
},
|
|
};
|
|
const items = extractExpenseItems(raw);
|
|
assert.equal(items.length, 1);
|
|
assert.equal(items[0]!.occurredOn, "2026-08-07");
|
|
assert.equal(items[0]!.expenseType, "客户招待费");
|
|
assert.equal(items[0]!.projectId, "P-001");
|
|
assert.equal(items[0]!.crmRecordId, "CRM-99");
|
|
});
|
|
|
|
test("epoch expense dates use Asia/Shanghai calendar boundaries", () => {
|
|
const items = extractExpenseItems({ form: { businessData: { expenseRows: [{
|
|
"费用发生日期": Date.parse("2026-08-13T16:30:00.000Z"),
|
|
"费用类型": "交通费",
|
|
"费用金额": 10,
|
|
}] } } });
|
|
assert.equal(items[0]?.occurredOn, "2026-08-14");
|
|
});
|
|
|
|
test("joins real XFT expense schema keys with values without exposing its schema", () => {
|
|
const raw = {
|
|
todo: { applicantName: "李冬梅", applicantDepartment: "奋进队", applyTime: "2026-08-08", amount: 820 },
|
|
form: {
|
|
values: {
|
|
TripReimburseExpense: {
|
|
value: {
|
|
expenseList: [{
|
|
values: {
|
|
TripExpReimAmount: { value: { currencyCode: "CNY", reimAmt: 820 } },
|
|
dateKey: { value: "2026-08-07" },
|
|
descriptionKey: { value: "客户招待" },
|
|
attachmentKey: { value: [{ fileNam: "secret.pdf" }] },
|
|
},
|
|
businessConfigDTO: { expenseSchema: { body: [
|
|
{ key: "TripExpReimAmount", name: "报销金额" },
|
|
{ key: "dateKey", name: "消费日期" },
|
|
{ key: "descriptionKey", name: "费用说明" },
|
|
{ key: "attachmentKey", name: "附件(支付记录、截图、小票)" },
|
|
] } },
|
|
businessDataDTO: { expBodyInsDto: {
|
|
expNam: "餐饮费",
|
|
wrTim: 1_786_032_000_000,
|
|
reimAmt: 820,
|
|
currencyCode: "CNY",
|
|
remark: "客户招待",
|
|
certificate: { invoiceCloudCertificates: [
|
|
{ invoiceDate: "2026-08-07" },
|
|
{ invoiceDate: "2026-08-06" },
|
|
] },
|
|
} },
|
|
}],
|
|
},
|
|
},
|
|
},
|
|
},
|
|
};
|
|
const compact = compactApprovalDetail(raw, "summary");
|
|
const items = compact.detail.expenseItems as Array<Record<string, unknown>>;
|
|
assert.equal(items.length, 1);
|
|
assert.equal(items[0]!.occurredOn, "2026-08-07");
|
|
assert.equal(items[0]!.expenseType, "餐饮费");
|
|
assert.equal(items[0]!.amount, 820);
|
|
assert.equal(items[0]!.description, "客户招待");
|
|
assert.deepEqual(items[0]!.invoiceDates, ["2026-08-07", "2026-08-06"]);
|
|
assert.equal((compact.detail.applicant as Record<string, unknown>).name, "李冬梅");
|
|
assert.doesNotMatch(JSON.stringify(compact), /secret\.pdf|expenseSchema|businessConfigDTO/);
|
|
});
|
|
|
|
test("extracts official XFT OpenAPI totals and expense fields", () => {
|
|
const raw = {
|
|
billId: "2099010200000001",
|
|
form: {
|
|
eventType: "COMMON_REIM",
|
|
eventName: "费用报销单",
|
|
applyTim: 1_786_083_489_000,
|
|
aplName: "李林峰",
|
|
aplDepName: "奋进队",
|
|
expTotalCnt: 2336,
|
|
billRemark: "客户招待",
|
|
billCustomFieldList: [
|
|
{ fieldName: "成本中心", showValue: "销售一部", type: "SELECT" },
|
|
{ fieldName: "附件(支付记录、截图、小票)", showValue: "secret.pdf", type: "ACCESSORIES" },
|
|
],
|
|
expenseList: [{
|
|
expTypeCode: "00",
|
|
expTypeName: "餐饮费",
|
|
expAttribute: "MEAL",
|
|
remark: "8月6日宴请联合电子张家万",
|
|
reimAmount: 2336,
|
|
wrTim: 1_785_974_400_000,
|
|
currencyCode: "CNY",
|
|
merchantName: null,
|
|
payCity: "98",
|
|
invoiceInfoList: [
|
|
{ invoiceDate: "2026-08-06", sellerName: "上海靓小余餐饮管理有限公司", invoiceAmount: 970 },
|
|
{ invoiceDate: "2026-08-05", sellerName: "另一商户", invoiceAmount: 1366 },
|
|
],
|
|
expCustomFieldList: [
|
|
{ fieldName: "接待客户", showValue: "联合电子" },
|
|
{ fieldName: "CRM记录ID", showValue: "CRM-100" },
|
|
{ fieldName: "附件", showValue: "private-image-data" },
|
|
],
|
|
}],
|
|
},
|
|
};
|
|
const compact = compactApprovalDetail(raw, "summary");
|
|
assert.deepEqual(compact.detail.amount, { value: 2336, currency: "CNY" });
|
|
assert.equal((compact.detail.applicant as Record<string, unknown>).name, "李林峰");
|
|
assert.equal((compact.detail.applicant as Record<string, unknown>).department, "奋进队");
|
|
assert.equal(compact.detail.summary, "客户招待");
|
|
const items = compact.detail.expenseItems as Array<Record<string, unknown>>;
|
|
assert.equal(items.length, 1);
|
|
assert.equal(items[0]!.occurredOn, "2026-08-06");
|
|
assert.equal(items[0]!.expenseType, "餐饮费");
|
|
assert.equal(items[0]!.amount, 2336);
|
|
assert.equal(items[0]!.currency, "CNY");
|
|
assert.equal(items[0]!.description, "8月6日宴请联合电子张家万");
|
|
assert.equal(items[0]!.merchant, "上海靓小余餐饮管理有限公司");
|
|
assert.equal(items[0]!.location, "98");
|
|
assert.equal(items[0]!.customer, "联合电子");
|
|
assert.equal(items[0]!.crmRecordId, "CRM-100");
|
|
assert.deepEqual(items[0]!.invoiceDates, ["2026-08-06", "2026-08-05"]);
|
|
assert.match(JSON.stringify(compact.detail.formFields), /单据类型|报销总额|成本中心/);
|
|
assert.doesNotMatch(JSON.stringify(compact), /secret\.pdf|private-image-data/);
|
|
});
|
|
|
|
test("extracts XFT generic detail expList aliases used by expense applications", () => {
|
|
const raw = {
|
|
todo: { applicantName: "测试用户丁", applicantDepartment: "测试部门", amount: 3000 },
|
|
form: {
|
|
billInfo: {
|
|
eventType: "EXPENSE_APPLY", applyTotalAmount: 3000,
|
|
customFieldValues: [
|
|
{ customFieldName: "招待地址", customFieldValue: "无锡", customFieldConfig: { name: "内部配置", value: "不应展示" } },
|
|
{ customFieldName: "招待日期", customFieldValue: "2026-08-12" },
|
|
{ customFieldName: "招待事由", customFieldValue: "客户招待" },
|
|
{ customFieldName: "家庭住址", customFieldValue: "不应展示的私人地址" },
|
|
],
|
|
},
|
|
expList: [{
|
|
expNam: "招待客户烟、酒、水果等", expId: "000", wrTim: "2026-08-13 00:00",
|
|
reimAmt: 3000, currencyCode: "CNY", remark: "买酒及招待费用", merchantNam: "公司", payCity: "深圳市",
|
|
}],
|
|
applyExpenseDtoList: [{ expTypeName: "招待客户烟、酒、水果等", applyAmount: 3000, remark: "买酒及招待费用" }],
|
|
},
|
|
};
|
|
const compact = compactApprovalDetail(raw, "expense");
|
|
const items = compact.detail.expenseItems as Array<Record<string, unknown>>;
|
|
assert.equal(items.length, 1);
|
|
assert.deepEqual(items[0], {
|
|
occurredOn: "2026-08-13", expenseType: "招待客户烟、酒、水果等", amount: 3000, currency: "CNY",
|
|
description: "买酒及招待费用", merchant: "公司", location: "深圳市", customer: null, contacts: [],
|
|
relatedTripId: null, invoiceDates: [], projectId: null, crmRecordId: null,
|
|
});
|
|
assert.match(JSON.stringify(compact.detail.formFields), /招待地址.*无锡.*招待日期.*2026-08-12.*招待事由.*客户招待/);
|
|
assert.match(JSON.stringify(compact.detail.supportingFields), /招待地址.*无锡.*招待日期.*2026-08-12.*招待事由.*客户招待/);
|
|
assert.doesNotMatch(JSON.stringify(compact), /内部配置|不应展示/);
|
|
});
|
|
|
|
test("official XFT expense date falls back to hotel check-in date", () => {
|
|
const raw = { form: { eventType: "TRAVEL_REIM", expTotalCnt: 843, expenseList: [{
|
|
expTypeName: "住宿费",
|
|
reimAmount: 843,
|
|
currencyCode: "CNY",
|
|
remark: "7/21-8/8出差住宿费用",
|
|
wrTim: null,
|
|
bgnTim: null,
|
|
checkInDate: 1_784_592_000_000,
|
|
checkOutDate: 1_786_147_200_000,
|
|
invoiceInfoList: [],
|
|
}] } };
|
|
const items = extractExpenseItems(raw);
|
|
assert.equal(items.length, 1);
|
|
assert.equal(items[0]!.occurredOn, "2026-07-21");
|
|
assert.equal(items[0]!.expenseType, "住宿费");
|
|
assert.equal(items[0]!.amount, 843);
|
|
});
|
|
|
|
test("raw detail is filtered, paginated, bounded, and traverses embedded JSON", () => {
|
|
const raw = {
|
|
businessKey: "B-1",
|
|
businessConfig: { hugeSchema: "must-not-be-visible" },
|
|
mobile: "13800138000",
|
|
formComponentValues: [{
|
|
name: "报销明细",
|
|
value: JSON.stringify([{ rowValue: [{ label: "费用说明", value: "x".repeat(10_000) }] }]),
|
|
}],
|
|
};
|
|
const root = browseRawDetail(raw, "", 0, 50);
|
|
assert.doesNotMatch(JSON.stringify(root), /businessConfig|must-not-be-visible|13800138000/);
|
|
const embedded = browseRawDetail(raw, "/formComponentValues/0/value/0/rowValue/0", 0, 20) as Record<string, unknown>;
|
|
assert.equal(embedded.kind, "container");
|
|
assert.equal(embedded.encodedAsJsonString, true);
|
|
assert.equal(JSON.stringify(embedded).length < 10_000, true);
|
|
const scalar = browseRawDetail(raw, "/formComponentValues/0/value/0/rowValue/0/value", 0, 20) as Record<string, unknown>;
|
|
assert.equal(scalar.kind, "string");
|
|
assert.equal(scalar.valueTruncated, true);
|
|
assert.equal(String(scalar.value).length, 4_000);
|
|
assert.throws(() => browseRawDetail(raw, "/businessConfig"), /不存在/);
|
|
assert.throws(() => browseRawDetail(raw, "/mobile"), /不存在/);
|
|
});
|
|
|
|
test("raw detail supports bounded field search and hides internal user and department IDs", () => {
|
|
const raw = { originatorUserid: "private-user", originatorDeptId: 123, formValueVOS: [
|
|
{ name: "普通字段", value: "无关" },
|
|
{ name: "关联申请单", value: "TRIP-20260811" },
|
|
] };
|
|
const result = browseRawDetail(raw, "/formValueVOS", 0, 20, "关联申请单") as Record<string, unknown>;
|
|
assert.equal(result.kind, "search");
|
|
assert.equal(result.total, 1);
|
|
assert.match(JSON.stringify(result), /关联申请单|TRIP-20260811/);
|
|
assert.doesNotMatch(JSON.stringify(browseRawDetail(raw)), /private-user|originatorUserid|originatorDeptId/);
|
|
});
|