taxdrop-core 1.0.5 → 1.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "taxdrop-core",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.6",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
32
|
"eslint": "^9.39.2",
|
|
33
|
+
"handlebars": "^4.7.8",
|
|
33
34
|
"prettier": "^3.8.1",
|
|
34
35
|
"typescript": "^5.9.3",
|
|
35
36
|
"vitest": "^4.0.17"
|
|
@@ -40,4 +41,4 @@
|
|
|
40
41
|
"eslint-config-prettier": "^10.1.8",
|
|
41
42
|
"eslint-plugin-prettier": "^5.5.5"
|
|
42
43
|
}
|
|
43
|
-
}
|
|
44
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { renderInvoice } from './render-invoice.js'
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import Handlebars from 'handlebars'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Renders an invoice HTML template with provided parameters.
|
|
5
|
+
*
|
|
6
|
+
* This is a pure function that compiles a Handlebars template and merges it
|
|
7
|
+
* with invoice data to produce final HTML. The same function is used for both
|
|
8
|
+
* frontend preview and backend PDF generation to ensure consistency.
|
|
9
|
+
*
|
|
10
|
+
* The core rendering engine is minimal and does not include opinionated helpers
|
|
11
|
+
* for formatting, locale, or business logic. Consumers can register their own
|
|
12
|
+
* Handlebars helpers as needed.
|
|
13
|
+
*
|
|
14
|
+
* @param {Object} options - Rendering options
|
|
15
|
+
* @param {string} options.templateHtml - Handlebars template HTML string
|
|
16
|
+
* @param {InvoiceRenderParams} options.params - Invoice data parameters
|
|
17
|
+
* @returns {{html: string}} Object containing the rendered HTML string
|
|
18
|
+
* @throws {Error} If template compilation fails or rendering errors occur
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```javascript
|
|
22
|
+
* const template = '<h1>Invoice {{invoiceNumber}}</h1>'
|
|
23
|
+
* const params = { invoiceNumber: 'INV-001' }
|
|
24
|
+
* const { html } = renderInvoice({ templateHtml: template, params })
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
export function renderInvoice({ templateHtml, params }) {
|
|
28
|
+
// Validate inputs
|
|
29
|
+
if (typeof templateHtml !== 'string') {
|
|
30
|
+
throw new Error(
|
|
31
|
+
'renderInvoice: templateHtml must be a string, got ' +
|
|
32
|
+
typeof templateHtml,
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (params === null || params === undefined) {
|
|
37
|
+
throw new Error('renderInvoice: params is required')
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (typeof params !== 'object') {
|
|
41
|
+
throw new Error(
|
|
42
|
+
'renderInvoice: params must be an object, got ' + typeof params,
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
// Compile Handlebars template
|
|
48
|
+
const template = Handlebars.compile(templateHtml)
|
|
49
|
+
|
|
50
|
+
// Prepare context - params is already validated to be an object above
|
|
51
|
+
const context = params
|
|
52
|
+
|
|
53
|
+
// Render template with context
|
|
54
|
+
const html = template(context)
|
|
55
|
+
|
|
56
|
+
return { html }
|
|
57
|
+
} catch (error) {
|
|
58
|
+
// Provide clearer error messages
|
|
59
|
+
if (error instanceof Error) {
|
|
60
|
+
throw new Error(
|
|
61
|
+
`renderInvoice: Template rendering failed: ${error.message}`,
|
|
62
|
+
{ cause: error },
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
throw error
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest'
|
|
2
|
+
import { renderInvoice } from '../../../src/invoices/rendering/render-invoice.js'
|
|
3
|
+
import Handlebars from 'handlebars'
|
|
4
|
+
|
|
5
|
+
describe('renderInvoice', () => {
|
|
6
|
+
describe('basic rendering', () => {
|
|
7
|
+
it('should render a simple template with basic params', () => {
|
|
8
|
+
const template = '<h1>Invoice {{invoiceNumber}}</h1>'
|
|
9
|
+
const params = { invoiceNumber: 'INV-001' }
|
|
10
|
+
|
|
11
|
+
const result = renderInvoice({ templateHtml: template, params })
|
|
12
|
+
|
|
13
|
+
expect(result).toHaveProperty('html')
|
|
14
|
+
expect(result.html).toBe('<h1>Invoice INV-001</h1>')
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it('should render multiple variables', () => {
|
|
18
|
+
const template =
|
|
19
|
+
'<div>Invoice {{invoiceNumber}} dated {{invoiceDate}}</div>'
|
|
20
|
+
const params = {
|
|
21
|
+
invoiceNumber: 'INV-002',
|
|
22
|
+
invoiceDate: '2026-01-28',
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const result = renderInvoice({ templateHtml: template, params })
|
|
26
|
+
|
|
27
|
+
expect(result.html).toBe('<div>Invoice INV-002 dated 2026-01-28</div>')
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('should return HTML string in result object', () => {
|
|
31
|
+
const template = 'test'
|
|
32
|
+
const params = {}
|
|
33
|
+
|
|
34
|
+
const result = renderInvoice({ templateHtml: template, params })
|
|
35
|
+
|
|
36
|
+
expect(result).toEqual({ html: 'test' })
|
|
37
|
+
expect(typeof result.html).toBe('string')
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('should handle falsy params by using empty object', () => {
|
|
41
|
+
// This tests the params || {} branch (line 52)
|
|
42
|
+
// Note: We validate params earlier, so this branch is defensive
|
|
43
|
+
// Since params is validated to be an object, this branch may be unreachable
|
|
44
|
+
// But it exists as defensive code. We test with empty object which is truthy
|
|
45
|
+
// To truly test the || {} branch, params would need to be falsy, but validation prevents that
|
|
46
|
+
const template = 'test'
|
|
47
|
+
const params = {}
|
|
48
|
+
|
|
49
|
+
const result = renderInvoice({ templateHtml: template, params })
|
|
50
|
+
|
|
51
|
+
expect(result.html).toBe('test')
|
|
52
|
+
})
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
describe('missing optional fields', () => {
|
|
56
|
+
it('should handle missing optional fields gracefully', () => {
|
|
57
|
+
const template = 'Invoice {{invoiceNumber}} - Due: {{dueDate}}'
|
|
58
|
+
const params = { invoiceNumber: 'INV-001' }
|
|
59
|
+
|
|
60
|
+
const result = renderInvoice({ templateHtml: template, params })
|
|
61
|
+
|
|
62
|
+
expect(result.html).toBe('Invoice INV-001 - Due: ')
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('should handle empty params object', () => {
|
|
66
|
+
const template = 'Invoice {{invoiceNumber}}'
|
|
67
|
+
const params = {}
|
|
68
|
+
|
|
69
|
+
const result = renderInvoice({ templateHtml: template, params })
|
|
70
|
+
|
|
71
|
+
expect(result.html).toBe('Invoice ')
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('should handle null values', () => {
|
|
75
|
+
const template = 'Value: {{value}}'
|
|
76
|
+
const params = { value: null }
|
|
77
|
+
|
|
78
|
+
const result = renderInvoice({ templateHtml: template, params })
|
|
79
|
+
|
|
80
|
+
expect(result.html).toBe('Value: ')
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it('should handle undefined values', () => {
|
|
84
|
+
const template = 'Value: {{value}}'
|
|
85
|
+
const params = { value: undefined }
|
|
86
|
+
|
|
87
|
+
const result = renderInvoice({ templateHtml: template, params })
|
|
88
|
+
|
|
89
|
+
expect(result.html).toBe('Value: ')
|
|
90
|
+
})
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
describe('arrays and line items', () => {
|
|
94
|
+
it('should render line items array', () => {
|
|
95
|
+
const template = `
|
|
96
|
+
<ul>
|
|
97
|
+
{{#each lineItems}}
|
|
98
|
+
<li>{{description}}: {{price}}</li>
|
|
99
|
+
{{/each}}
|
|
100
|
+
</ul>
|
|
101
|
+
`
|
|
102
|
+
const params = {
|
|
103
|
+
lineItems: [
|
|
104
|
+
{ description: 'Service A', price: '$100' },
|
|
105
|
+
{ description: 'Service B', price: '$200' },
|
|
106
|
+
],
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const result = renderInvoice({ templateHtml: template, params })
|
|
110
|
+
|
|
111
|
+
expect(result.html).toContain('Service A')
|
|
112
|
+
expect(result.html).toContain('$100')
|
|
113
|
+
expect(result.html).toContain('Service B')
|
|
114
|
+
expect(result.html).toContain('$200')
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
it('should handle empty arrays', () => {
|
|
118
|
+
const template = `
|
|
119
|
+
<ul>
|
|
120
|
+
{{#each lineItems}}
|
|
121
|
+
<li>{{description}}</li>
|
|
122
|
+
{{else}}
|
|
123
|
+
<li>No items</li>
|
|
124
|
+
{{/each}}
|
|
125
|
+
</ul>
|
|
126
|
+
`
|
|
127
|
+
const params = { lineItems: [] }
|
|
128
|
+
|
|
129
|
+
const result = renderInvoice({ templateHtml: template, params })
|
|
130
|
+
|
|
131
|
+
expect(result.html).toContain('No items')
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
it('should handle missing lineItems array', () => {
|
|
135
|
+
const template = `
|
|
136
|
+
<ul>
|
|
137
|
+
{{#each lineItems}}
|
|
138
|
+
<li>{{description}}</li>
|
|
139
|
+
{{else}}
|
|
140
|
+
<li>No items</li>
|
|
141
|
+
{{/each}}
|
|
142
|
+
</ul>
|
|
143
|
+
`
|
|
144
|
+
const params = {}
|
|
145
|
+
|
|
146
|
+
const result = renderInvoice({ templateHtml: template, params })
|
|
147
|
+
|
|
148
|
+
expect(result.html).toContain('No items')
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
it('should render nested properties in line items', () => {
|
|
152
|
+
const template = `
|
|
153
|
+
<table>
|
|
154
|
+
{{#each lineItems}}
|
|
155
|
+
<tr>
|
|
156
|
+
<td>{{description}}</td>
|
|
157
|
+
<td>{{quantity}}</td>
|
|
158
|
+
<td>{{price}}</td>
|
|
159
|
+
<td>{{total}}</td>
|
|
160
|
+
</tr>
|
|
161
|
+
{{/each}}
|
|
162
|
+
</table>
|
|
163
|
+
`
|
|
164
|
+
const params = {
|
|
165
|
+
lineItems: [
|
|
166
|
+
{
|
|
167
|
+
description: 'Product A',
|
|
168
|
+
quantity: 2,
|
|
169
|
+
price: 50,
|
|
170
|
+
total: 100,
|
|
171
|
+
},
|
|
172
|
+
],
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const result = renderInvoice({ templateHtml: template, params })
|
|
176
|
+
|
|
177
|
+
expect(result.html).toContain('Product A')
|
|
178
|
+
expect(result.html).toContain('2')
|
|
179
|
+
expect(result.html).toContain('50')
|
|
180
|
+
expect(result.html).toContain('100')
|
|
181
|
+
})
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
describe('nested objects', () => {
|
|
185
|
+
it('should render nested issuer object', () => {
|
|
186
|
+
const template = `
|
|
187
|
+
<div>
|
|
188
|
+
<h2>From: {{issuer.name}}</h2>
|
|
189
|
+
<p>{{issuer.address}}</p>
|
|
190
|
+
<p>{{issuer.email}}</p>
|
|
191
|
+
</div>
|
|
192
|
+
`
|
|
193
|
+
const params = {
|
|
194
|
+
issuer: {
|
|
195
|
+
name: 'Acme Corp',
|
|
196
|
+
address: '123 Main St',
|
|
197
|
+
email: 'contact@acme.com',
|
|
198
|
+
},
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const result = renderInvoice({ templateHtml: template, params })
|
|
202
|
+
|
|
203
|
+
expect(result.html).toContain('Acme Corp')
|
|
204
|
+
expect(result.html).toContain('123 Main St')
|
|
205
|
+
expect(result.html).toContain('contact@acme.com')
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
it('should render nested recipient object', () => {
|
|
209
|
+
const template = `
|
|
210
|
+
<div>
|
|
211
|
+
<h2>To: {{recipient.name}}</h2>
|
|
212
|
+
<p>{{recipient.address}}</p>
|
|
213
|
+
</div>
|
|
214
|
+
`
|
|
215
|
+
const params = {
|
|
216
|
+
recipient: {
|
|
217
|
+
name: 'John Doe',
|
|
218
|
+
address: '456 Oak Ave',
|
|
219
|
+
},
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const result = renderInvoice({ templateHtml: template, params })
|
|
223
|
+
|
|
224
|
+
expect(result.html).toContain('John Doe')
|
|
225
|
+
expect(result.html).toContain('456 Oak Ave')
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
it('should handle missing nested properties', () => {
|
|
229
|
+
const template = 'Name: {{issuer.name}}, Email: {{issuer.email}}'
|
|
230
|
+
const params = {
|
|
231
|
+
issuer: {
|
|
232
|
+
name: 'Acme Corp',
|
|
233
|
+
},
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const result = renderInvoice({ templateHtml: template, params })
|
|
237
|
+
|
|
238
|
+
expect(result.html).toBe('Name: Acme Corp, Email: ')
|
|
239
|
+
})
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
describe('deterministic output', () => {
|
|
243
|
+
it('should produce identical output for same input', () => {
|
|
244
|
+
const template = 'Invoice {{invoiceNumber}} - {{invoiceDate}}'
|
|
245
|
+
const params = {
|
|
246
|
+
invoiceNumber: 'INV-001',
|
|
247
|
+
invoiceDate: '2026-01-28',
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const result1 = renderInvoice({ templateHtml: template, params })
|
|
251
|
+
const result2 = renderInvoice({ templateHtml: template, params })
|
|
252
|
+
const result3 = renderInvoice({ templateHtml: template, params })
|
|
253
|
+
|
|
254
|
+
expect(result1.html).toBe(result2.html)
|
|
255
|
+
expect(result2.html).toBe(result3.html)
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
it('should produce consistent output with complex data', () => {
|
|
259
|
+
const template = `
|
|
260
|
+
<div>
|
|
261
|
+
<h1>{{invoiceNumber}}</h1>
|
|
262
|
+
{{#each lineItems}}
|
|
263
|
+
<p>{{description}}: {{price}}</p>
|
|
264
|
+
{{/each}}
|
|
265
|
+
</div>
|
|
266
|
+
`
|
|
267
|
+
const params = {
|
|
268
|
+
invoiceNumber: 'INV-001',
|
|
269
|
+
lineItems: [
|
|
270
|
+
{ description: 'Item 1', price: '$10' },
|
|
271
|
+
{ description: 'Item 2', price: '$20' },
|
|
272
|
+
],
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const results = Array.from({ length: 5 }, () =>
|
|
276
|
+
renderInvoice({ templateHtml: template, params }),
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
const firstHtml = results[0].html
|
|
280
|
+
results.forEach((result) => {
|
|
281
|
+
expect(result.html).toBe(firstHtml)
|
|
282
|
+
})
|
|
283
|
+
})
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
describe('error handling', () => {
|
|
287
|
+
it('should throw error if templateHtml is not a string', () => {
|
|
288
|
+
const params = {}
|
|
289
|
+
|
|
290
|
+
expect(() => {
|
|
291
|
+
renderInvoice({ templateHtml: null, params })
|
|
292
|
+
}).toThrow('templateHtml must be a string')
|
|
293
|
+
|
|
294
|
+
expect(() => {
|
|
295
|
+
renderInvoice({ templateHtml: undefined, params })
|
|
296
|
+
}).toThrow('templateHtml must be a string')
|
|
297
|
+
|
|
298
|
+
expect(() => {
|
|
299
|
+
renderInvoice({ templateHtml: 123, params })
|
|
300
|
+
}).toThrow('templateHtml must be a string')
|
|
301
|
+
|
|
302
|
+
expect(() => {
|
|
303
|
+
renderInvoice({ templateHtml: {}, params })
|
|
304
|
+
}).toThrow('templateHtml must be a string')
|
|
305
|
+
})
|
|
306
|
+
|
|
307
|
+
it('should throw error if params is missing', () => {
|
|
308
|
+
const template = 'test'
|
|
309
|
+
|
|
310
|
+
expect(() => {
|
|
311
|
+
renderInvoice({ templateHtml: template, params: null })
|
|
312
|
+
}).toThrow('params is required')
|
|
313
|
+
|
|
314
|
+
expect(() => {
|
|
315
|
+
renderInvoice({ templateHtml: template, params: undefined })
|
|
316
|
+
}).toThrow('params is required')
|
|
317
|
+
})
|
|
318
|
+
|
|
319
|
+
it('should throw error if params is not an object', () => {
|
|
320
|
+
const template = 'test'
|
|
321
|
+
|
|
322
|
+
expect(() => {
|
|
323
|
+
renderInvoice({ templateHtml: template, params: 'not an object' })
|
|
324
|
+
}).toThrow('params must be an object')
|
|
325
|
+
|
|
326
|
+
expect(() => {
|
|
327
|
+
renderInvoice({ templateHtml: template, params: 123 })
|
|
328
|
+
}).toThrow('params must be an object')
|
|
329
|
+
})
|
|
330
|
+
|
|
331
|
+
it('should throw error for invalid Handlebars syntax', () => {
|
|
332
|
+
const template = '{{#if}}unclosed block'
|
|
333
|
+
const params = {}
|
|
334
|
+
|
|
335
|
+
expect(() => {
|
|
336
|
+
renderInvoice({ templateHtml: template, params })
|
|
337
|
+
}).toThrow('Template rendering failed')
|
|
338
|
+
})
|
|
339
|
+
|
|
340
|
+
it('should provide clear error messages', () => {
|
|
341
|
+
// Use a template with unclosed block to trigger a Handlebars error
|
|
342
|
+
const template = '{{#if value}}unclosed'
|
|
343
|
+
const params = {}
|
|
344
|
+
|
|
345
|
+
expect(() => {
|
|
346
|
+
renderInvoice({ templateHtml: template, params })
|
|
347
|
+
}).toThrow('Template rendering failed')
|
|
348
|
+
})
|
|
349
|
+
|
|
350
|
+
it('should handle non-Error exceptions', () => {
|
|
351
|
+
// This test covers the case where something throws a non-Error object
|
|
352
|
+
// We'll mock Handlebars.compile to throw a non-Error object
|
|
353
|
+
const originalCompile = Handlebars.compile
|
|
354
|
+
const template = 'test'
|
|
355
|
+
const params = {}
|
|
356
|
+
|
|
357
|
+
// Mock compile to throw a non-Error object
|
|
358
|
+
Handlebars.compile = vi.fn(() => {
|
|
359
|
+
throw 'non-error string'
|
|
360
|
+
})
|
|
361
|
+
|
|
362
|
+
try {
|
|
363
|
+
expect(() => {
|
|
364
|
+
renderInvoice({ templateHtml: template, params })
|
|
365
|
+
}).toThrow('non-error string')
|
|
366
|
+
} finally {
|
|
367
|
+
// Restore original compile
|
|
368
|
+
Handlebars.compile = originalCompile
|
|
369
|
+
}
|
|
370
|
+
})
|
|
371
|
+
})
|
|
372
|
+
|
|
373
|
+
describe('edge cases', () => {
|
|
374
|
+
it('should handle empty strings', () => {
|
|
375
|
+
const template = 'Value: {{value}}'
|
|
376
|
+
const params = { value: '' }
|
|
377
|
+
|
|
378
|
+
const result = renderInvoice({ templateHtml: template, params })
|
|
379
|
+
|
|
380
|
+
expect(result.html).toBe('Value: ')
|
|
381
|
+
})
|
|
382
|
+
|
|
383
|
+
it('should handle zero values', () => {
|
|
384
|
+
const template = 'Total: {{total}}'
|
|
385
|
+
const params = { total: 0 }
|
|
386
|
+
|
|
387
|
+
const result = renderInvoice({ templateHtml: template, params })
|
|
388
|
+
|
|
389
|
+
expect(result.html).toBe('Total: 0')
|
|
390
|
+
})
|
|
391
|
+
|
|
392
|
+
it('should handle boolean values', () => {
|
|
393
|
+
const template = 'Paid: {{paid}}'
|
|
394
|
+
const params = { paid: true }
|
|
395
|
+
|
|
396
|
+
const result = renderInvoice({ templateHtml: template, params })
|
|
397
|
+
|
|
398
|
+
expect(result.html).toBe('Paid: true')
|
|
399
|
+
})
|
|
400
|
+
|
|
401
|
+
it('should handle numbers as strings', () => {
|
|
402
|
+
const template = 'Amount: {{amount}}'
|
|
403
|
+
const params = { amount: '123.45' }
|
|
404
|
+
|
|
405
|
+
const result = renderInvoice({ templateHtml: template, params })
|
|
406
|
+
|
|
407
|
+
expect(result.html).toBe('Amount: 123.45')
|
|
408
|
+
})
|
|
409
|
+
|
|
410
|
+
it('should handle complex nested structures', () => {
|
|
411
|
+
const template = `
|
|
412
|
+
<div>
|
|
413
|
+
<h1>{{invoiceNumber}}</h1>
|
|
414
|
+
<div>Issuer: {{issuer.name}} ({{issuer.taxId}})</div>
|
|
415
|
+
<div>Recipient: {{recipient.name}} ({{recipient.taxId}})</div>
|
|
416
|
+
<ul>
|
|
417
|
+
{{#each lineItems}}
|
|
418
|
+
<li>{{description}} - Qty: {{quantity}} @ {{price}} = {{total}}</li>
|
|
419
|
+
{{/each}}
|
|
420
|
+
</ul>
|
|
421
|
+
<div>Subtotal: {{totals.subtotal}}</div>
|
|
422
|
+
<div>Tax: {{totals.tax}}</div>
|
|
423
|
+
<div>Total: {{totals.total}}</div>
|
|
424
|
+
</div>
|
|
425
|
+
`
|
|
426
|
+
const params = {
|
|
427
|
+
invoiceNumber: 'INV-001',
|
|
428
|
+
issuer: {
|
|
429
|
+
name: 'Acme Corp',
|
|
430
|
+
taxId: 'TAX-123',
|
|
431
|
+
},
|
|
432
|
+
recipient: {
|
|
433
|
+
name: 'John Doe',
|
|
434
|
+
taxId: 'TAX-456',
|
|
435
|
+
},
|
|
436
|
+
lineItems: [
|
|
437
|
+
{
|
|
438
|
+
description: 'Service A',
|
|
439
|
+
quantity: 2,
|
|
440
|
+
price: 50,
|
|
441
|
+
total: 100,
|
|
442
|
+
},
|
|
443
|
+
{
|
|
444
|
+
description: 'Service B',
|
|
445
|
+
quantity: 1,
|
|
446
|
+
price: 75,
|
|
447
|
+
total: 75,
|
|
448
|
+
},
|
|
449
|
+
],
|
|
450
|
+
totals: {
|
|
451
|
+
subtotal: 175,
|
|
452
|
+
tax: 35,
|
|
453
|
+
total: 210,
|
|
454
|
+
},
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
const result = renderInvoice({ templateHtml: template, params })
|
|
458
|
+
|
|
459
|
+
expect(result.html).toContain('INV-001')
|
|
460
|
+
expect(result.html).toContain('Acme Corp')
|
|
461
|
+
expect(result.html).toContain('TAX-123')
|
|
462
|
+
expect(result.html).toContain('John Doe')
|
|
463
|
+
expect(result.html).toContain('Service A')
|
|
464
|
+
expect(result.html).toContain('Service B')
|
|
465
|
+
expect(result.html).toContain('175')
|
|
466
|
+
expect(result.html).toContain('35')
|
|
467
|
+
expect(result.html).toContain('210')
|
|
468
|
+
})
|
|
469
|
+
})
|
|
470
|
+
})
|