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>; 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).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).name, "李林峰"); assert.equal((compact.detail.applicant as Record).department, "奋进队"); assert.equal(compact.detail.summary, "客户招待"); const items = compact.detail.expenseItems as Array>; 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>; 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; 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; 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; 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/); });