const { createRequire } = require('node:module'); const path = require('node:path'); const req = createRequire(path.resolve(__dirname, '../../server/package.json')); function syntheticReceiptText({ status = 'success', final = true, direction = 'debit', amount = '10.00', currency = '人民币', payerAccount = '000011112222', payeeAccount = '000033334444', reference = 'SYNTHETIC-REFERENCE', bookedOn = '2026-10-01', extraLines = [] } = {}) { const words = { success: '交易成功', failed: '交易失败', pending: '处理中', booked: '' }; return [final ? '中国建设银行单位客户专用回单' : '中国建设银行网上银行电子回执', '合成公司测试回单 SYNTHETIC', `记账日期 ${bookedOn}`, direction === 'credit' ? '贷' : '借', `金额 ${amount}`, `币别 ${currency}`, `付款人账号 ${payerAccount}`, `收款人账号 ${payeeAccount}`, `凭证号码 ${reference}`, ...(words[status] ? [`单据状态 ${words[status]}`] : []), ...(!final ? ['本回执不作为收、付款方交易的最终依据,正式回单请在交易成功后另打印。'] : []), ...extraLines].join('\n'); } async function syntheticBankReceiptPdf({ pages = 1, tag = 'SYNTHETIC', ...options } = {}) { const { PDFDocument, PDFHexString, beginText, endText, setFontAndSize, moveText, showText } = req('pdf-lib'); const doc = await PDFDocument.create(); doc.setTitle(tag); const text = syntheticReceiptText(options); const hex = (s) => Buffer.from(s, 'utf16le').swap16().toString('hex').toUpperCase(); const codes = [...new Set([...text.replace(/\n/g, '')])].map((c) => `<${hex(c)}> <${hex(c)}>`); const cmap = `/CIDInit /ProcSet findresource begin 12 dict begin begincmap /CIDSystemInfo << /Registry (Adobe) /Ordering (UCS) /Supplement 0 >> def /CMapName /Synthetic-UCS def /CMapType 2 def 1 begincodespacerange <0000> endcodespacerange ${codes.length} beginbfchar\n${codes.join('\n')}\nendbfchar endcmap CMapName currentdict /CMap defineresource pop end end`; const toUnicode = doc.context.register(doc.context.flateStream(cmap)); const cid = doc.context.register(doc.context.obj({ Type: 'Font', Subtype: 'CIDFontType0', BaseFont: 'STSong-Light', CIDSystemInfo: { Registry: PDFHexString.fromText('Adobe'), Ordering: PDFHexString.fromText('GB1'), Supplement: 4 }, DW: 1000 })); const font = doc.context.register(doc.context.obj({ Type: 'Font', Subtype: 'Type0', BaseFont: 'STSong-Light', Encoding: 'Identity-H', DescendantFonts: [cid], ToUnicode: toUnicode })); for (let n = 0; n < pages; n++) { const page = doc.addPage([595, 842]); const name = page.node.newFontDictionary('SYN', font); text.split('\n').forEach((line, index) => page.pushOperators(beginText(), setFontAndSize(name, 10), moveText(30, 790 - index * 22), showText(PDFHexString.of(hex(line))), endText())); } return Buffer.from(await doc.save()); } // Synthetic replica of the CCB "单位客户专用回单" table layout (all values fake): // out-of-order text items, stacked 付款人/收款人 column headers, a vertical copy // note that also contains 收款人, an unlabelled header date and 大写/小写 amounts. async function syntheticCcbIncomingPdf({ tag = 'SYNTHETIC-CCB', amount = '50,000.00', capital = '伍万元整', date = ['2025', '03', '14'], serial = '123456789AB12CDEFG3', payerName = '合成甲', payerAccount = '6222000011112222', payeeName = '合成公司', payeeAccount = '33050161000000000001', title = '单位客户专用回单', direction = '贷', copyNote = '(第一联)(收款人回单)', headers = ['付款人', '收款人'], dateDy = 0, extraItems = [] } = {}) { const { PDFDocument, PDFHexString, beginText, endText, setFontAndSize, moveText, showText } = req('pdf-lib'); const items = [ ...(serial ? [['流水号:' + serial, 388, 785]] : []), // dateDy models the small baseline jitter of real PDFs within one row; date: null omits it. ...(date ? [['年', 272, 785], ['月', 292, 785], ['日', 312, 785], [date[0], 252, 785 + dateDy], [date[1], 282, 785 + dateDy], [date[2], 302, 785 + dateDy]] : []), ['币别:人民币', 40, 785], ['中国建设银行' + title, 172, 806], ...[...headers[0]].map((c, i) => [c, 29, 769 - i * 10]), ...[...headers[1]].map((c, i) => [c, 284, 769 - i * 10]), ['全', 52, 772], ['称', 72, 772], [payerName, 92, 772], ['全', 307, 772], ['称', 327, 772], [payeeName, 347, 772], ['账', 52, 759], ['号', 72, 759], [payerAccount, 92, 759], ['账', 307, 759], ['号', 327, 759], [payeeAccount, 347, 759], ['开户行', 52, 745], ['中国建设银行合成支行', 92, 745], ['开户行', 307, 745], ['中国建设银行合成支行', 347, 745], ...[...copyNote].map((c, i) => [c, 540, 751 - i * 10]), ['金额', 47, 731], ['(大写)人民币' + capital, 92, 731], ['(小写)¥' + amount, 347, 731], ['凭证种类', 37, 716], ['凭证号码', 293, 716], ['摘要', 37, 702], ['借款', 92, 702], [direction, 500, 690], ['打印时间:2026-10-08 10:00:00', 25, 596], ['本回单为合成测试回单,仅用于验证版式解析,已记账。', 25, 585], ...extraItems, ]; const doc = await PDFDocument.create(); doc.setTitle(tag); const hex = (t) => Buffer.from(t, 'utf16le').swap16().toString('hex').toUpperCase(); const chars = [...new Set(items.map(([t]) => t).join(''))]; const cmap = `/CIDInit /ProcSet findresource begin 12 dict begin begincmap /CIDSystemInfo << /Registry (Adobe) /Ordering (UCS) /Supplement 0 >> def /CMapName /Synthetic-UCS def /CMapType 2 def 1 begincodespacerange <0000> endcodespacerange ${Array.from({ length: Math.ceil(chars.length / 100) }, (_, i) => chars.slice(i * 100, i * 100 + 100)) .map((chunk) => `${chunk.length} beginbfchar\n${chunk.map((c) => `<${hex(c)}> <${hex(c)}>`).join('\n')}\nendbfchar`).join('\n')} endcmap CMapName currentdict /CMap defineresource pop end end`; const toUnicode = doc.context.register(doc.context.flateStream(cmap)); const cid = doc.context.register(doc.context.obj({ Type: 'Font', Subtype: 'CIDFontType0', BaseFont: 'STSong-Light', CIDSystemInfo: { Registry: PDFHexString.fromText('Adobe'), Ordering: PDFHexString.fromText('GB1'), Supplement: 4 }, DW: 1000 })); const font = doc.context.register(doc.context.obj({ Type: 'Font', Subtype: 'Type0', BaseFont: 'STSong-Light', Encoding: 'Identity-H', DescendantFonts: [cid], ToUnicode: toUnicode })); const page = doc.addPage([595, 842]); const name = page.node.newFontDictionary('SYN', font); // Real receipts use roughly 5pt glyphs; larger text would run off the page. for (const [text, x, y] of items) page.pushOperators(beginText(), setFontAndSize(name, 5), moveText(x, y), showText(PDFHexString.of(hex(text))), endText()); return Buffer.from(await doc.save()); } function syntheticDocumentEvidence(options) { return req('./src/services/paymentReceiptDocument').classifyReceiptText(syntheticReceiptText(options)); } module.exports = { syntheticReceiptText, syntheticBankReceiptPdf, syntheticCcbIncomingPdf, syntheticDocumentEvidence };