@teleporthq/teleport-plugin-next-workflows 0.44.4 → 0.44.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/__tests__/account-delete-owned-rows.test.ts +488 -0
- package/__tests__/invoice-paid-status.test.ts +382 -0
- package/__tests__/invoice-tender.test.ts +5 -3
- package/dist/cjs/account-delete-route-generator.d.ts +9 -6
- package/dist/cjs/account-delete-route-generator.d.ts.map +1 -1
- package/dist/cjs/account-delete-route-generator.js +131 -1
- package/dist/cjs/account-delete-route-generator.js.map +1 -1
- package/dist/cjs/invoice/invoice-assembly-code.d.ts.map +1 -1
- package/dist/cjs/invoice/invoice-assembly-code.js +19 -2
- package/dist/cjs/invoice/invoice-assembly-code.js.map +1 -1
- package/dist/cjs/invoice/invoice-html-code.d.ts.map +1 -1
- package/dist/cjs/invoice/invoice-html-code.js +100 -71
- package/dist/cjs/invoice/invoice-html-code.js.map +1 -1
- package/dist/cjs/tsconfig.tsbuildinfo +1 -1
- package/dist/esm/account-delete-route-generator.d.ts +9 -6
- package/dist/esm/account-delete-route-generator.d.ts.map +1 -1
- package/dist/esm/account-delete-route-generator.js +131 -1
- package/dist/esm/account-delete-route-generator.js.map +1 -1
- package/dist/esm/invoice/invoice-assembly-code.d.ts.map +1 -1
- package/dist/esm/invoice/invoice-assembly-code.js +19 -2
- package/dist/esm/invoice/invoice-assembly-code.js.map +1 -1
- package/dist/esm/invoice/invoice-html-code.d.ts.map +1 -1
- package/dist/esm/invoice/invoice-html-code.js +100 -71
- package/dist/esm/invoice/invoice-html-code.js.map +1 -1
- package/dist/esm/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -3
- package/src/account-delete-route-generator.ts +140 -7
- package/src/invoice/invoice-assembly-code.ts +19 -2
- package/src/invoice/invoice-html-code.ts +100 -71
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
/* tslint:disable:function-constructor */
|
|
2
|
+
import { generateInvoiceAssemblyCode } from '../src/invoice/invoice-assembly-code'
|
|
3
|
+
import { generatePdfGeneratorCode } from '../src/invoice/pdf-generator-code'
|
|
4
|
+
import type { UIDLInvoiceSettings } from '@teleporthq/teleport-types'
|
|
5
|
+
import { evaluateEmittedModule } from './_helpers/load-invoice-route'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* What the invoice says about being paid, and how it writes money.
|
|
9
|
+
*
|
|
10
|
+
* A store invoices an order the moment its payment webhook marks it paid, so
|
|
11
|
+
* that invoice — its email and its PDF — must not ask for a payment already
|
|
12
|
+
* made: no due date, no amount due. A cash-on-delivery order is invoiced
|
|
13
|
+
* unpaid when it is placed and keeps both. A summary line whose amount is zero
|
|
14
|
+
* (no delivery, no discount, no tax, no gift card) is left out. And an empty
|
|
15
|
+
* field on the real invoice prints nothing — never the design canvas's sample.
|
|
16
|
+
*
|
|
17
|
+
* Every piece below is the EMITTED code, run: the assembly the generate route
|
|
18
|
+
* builds the invoice with, then the renderer the email and the PDF go through
|
|
19
|
+
* (`email-sender-code.ts` fills the body with `buildDataContext` +
|
|
20
|
+
* `replacePlaceholders`).
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const SETTINGS = {
|
|
24
|
+
invoicePrefix: 'INV-',
|
|
25
|
+
defaultTaxRate: 0,
|
|
26
|
+
showDiscount: false,
|
|
27
|
+
taxIncludedInPrice: false,
|
|
28
|
+
companyDetails: {},
|
|
29
|
+
template: { document: null as unknown },
|
|
30
|
+
tables: { invoicesTable: 'teleport_invoices', invoiceItemsTable: 'teleport_invoice_items' },
|
|
31
|
+
emailDelivery: { enabled: false },
|
|
32
|
+
} as unknown as UIDLInvoiceSettings
|
|
33
|
+
|
|
34
|
+
type Row = Record<string, unknown>
|
|
35
|
+
|
|
36
|
+
interface AssemblyModule {
|
|
37
|
+
assembleInvoiceData: (body: Row, order: Row | null, items: Row[]) => Row
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface PdfGeneratorModule {
|
|
41
|
+
buildInvoiceDataScope: (invoiceData: Row) => {
|
|
42
|
+
invoiceData: { Invoice: Record<string, string>; Company: Record<string, string> }
|
|
43
|
+
}
|
|
44
|
+
buildInvoiceHtml: (invoiceData: Row) => string
|
|
45
|
+
replacePlaceholders: (template: string, data: Row) => string
|
|
46
|
+
buildDataContext: (invoiceData: Row) => Row
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function loadAssembly(): AssemblyModule {
|
|
50
|
+
const requireFn = ((id: string) => {
|
|
51
|
+
if (id.indexOf('pdf-generator') !== -1) {
|
|
52
|
+
return { COMPANY_DETAILS: {} }
|
|
53
|
+
}
|
|
54
|
+
if (id.indexOf('data-access') !== -1) {
|
|
55
|
+
return {}
|
|
56
|
+
}
|
|
57
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
58
|
+
return require(id)
|
|
59
|
+
}) as unknown as NodeRequire
|
|
60
|
+
return evaluateEmittedModule<AssemblyModule>(generateInvoiceAssemblyCode(SETTINGS), requireFn)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function loadPdfGenerator(templateUidl: unknown = null): PdfGeneratorModule {
|
|
64
|
+
return evaluateEmittedModule<PdfGeneratorModule>(
|
|
65
|
+
generatePdfGeneratorCode(SETTINGS, templateUidl, templateUidl ? {} : null),
|
|
66
|
+
require
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// The date the renderer prints for a stored date: it reads the calendar day in
|
|
71
|
+
// the server's own time zone, so the expectation does too.
|
|
72
|
+
const MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
|
|
73
|
+
const printedDate = (raw: unknown): string => {
|
|
74
|
+
const date = new Date(raw as string)
|
|
75
|
+
return MONTHS[date.getMonth()] + ' ' + date.getDate() + ', ' + date.getFullYear()
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// The editor's invoice email design (teleport-gui purposed-email-designs.ts) as
|
|
79
|
+
// it is published: each summary row is a <div> of <span>s, copied from a body a
|
|
80
|
+
// store sent on 2026-10-23 with its values put back to their tokens.
|
|
81
|
+
const SUMMARY_ROW_STYLE =
|
|
82
|
+
'color: #374151; margin: 0; padding: 10px 0; font-size: 14px; line-height: 1.5'
|
|
83
|
+
const summaryRow = (label: string, value: string, last = false) =>
|
|
84
|
+
`<div style="${SUMMARY_ROW_STYLE}${last ? '' : '; border-bottom: 1px solid #f0f1f3'}">` +
|
|
85
|
+
`<span>${label}: </span>${value}</div>`
|
|
86
|
+
const INVOICE_EMAIL_BODY =
|
|
87
|
+
'<div style="color: #1f2937; margin: 0 auto; padding: 8px; max-width: 600px; font-family: \'Segoe UI\',Roboto,Helvetica,Arial,sans-serif">' +
|
|
88
|
+
'<div style="border: 1px solid #e5e7eb; overflow: hidden; background: #ffffff; border-radius: 14px">' +
|
|
89
|
+
'<div style="padding: 24px 32px; background: #1e293b"><small style="color: #94a3b8; margin: 0; font-size: 12px; font-weight: 600; letter-spacing: 1px; text-transform: uppercase">Invoice</small>' +
|
|
90
|
+
'<p style="color: #ffffff; margin: 6px 0 0; font-size: 22px; font-weight: 700">Your invoice</p></div>' +
|
|
91
|
+
'<div style="padding: 28px 32px">' +
|
|
92
|
+
'<div style="color: #1f2937; margin: 0 0 16px; font-size: 15px; line-height: 1.6"><span>Dear </span><span>{{customerName}}</span><span>,</span></div>' +
|
|
93
|
+
'<p style="color: #4b5563; margin: 0 0 18px; font-size: 15px; line-height: 1.6">Thank you for your purchase. Your invoice is attached to this email. Here is a quick summary:</p>' +
|
|
94
|
+
'<div style="margin: 0 0 22px">' +
|
|
95
|
+
summaryRow('Invoice number', '<span>{{invoiceNumber}}</span>') +
|
|
96
|
+
summaryRow('Invoice date', '<span>{{invoiceDate}}</span>') +
|
|
97
|
+
summaryRow('Due date', '<span>{{dueDate}}</span>') +
|
|
98
|
+
summaryRow('Subtotal', '<span>{{subtotal}}</span>') +
|
|
99
|
+
summaryRow('Discount', '<span>−</span><span>{{discountAmount}}</span>') +
|
|
100
|
+
summaryRow('Tax', '<span>{{taxAmount}}</span>') +
|
|
101
|
+
summaryRow('Delivery', '<span>{{shippingAmount}}</span>') +
|
|
102
|
+
summaryRow('Total', '<span>{{totalAmount}}</span>') +
|
|
103
|
+
summaryRow('Paid by gift card', '<span>−</span><span>{{giftCardAmount}}</span>') +
|
|
104
|
+
summaryRow('Amount due', '<span>{{amountDue}}</span>', true) +
|
|
105
|
+
'</div>' +
|
|
106
|
+
'<div style="color: #4b5563; margin: 4px 0 0; font-size: 14px; line-height: 1.6"><span>If you have any questions about this invoice, contact us at </span><span>{{companyEmail}}</span><span>.</span></div>' +
|
|
107
|
+
'<p style="color: #4b5563; margin: 20px 0 2px; font-size: 14px; line-height: 1.6">Best regards,</p>' +
|
|
108
|
+
'<p style="color: #1f2937; margin: 0; font-size: 14px; font-weight: 600; line-height: 1.6">{{companyName}}</p>' +
|
|
109
|
+
'</div></div></div>'
|
|
110
|
+
|
|
111
|
+
// ORD-2 of the store this was found on: one 119.00 lei line paid by card, no
|
|
112
|
+
// delivery, discount, tax or gift card. The payment webhook marked it paid —
|
|
113
|
+
// `payment_status` and the `updated_at` that write stamps — then asked for the
|
|
114
|
+
// invoice with just the order id.
|
|
115
|
+
const PAID_CARD_ORDER: Row = {
|
|
116
|
+
id: 'order-2',
|
|
117
|
+
order_number: 'ORD-2',
|
|
118
|
+
currency: 'RON',
|
|
119
|
+
total_amount: 119,
|
|
120
|
+
shipping_amount: 0,
|
|
121
|
+
discount_amount: 0,
|
|
122
|
+
gift_card_amount: 0,
|
|
123
|
+
status: 'confirmed',
|
|
124
|
+
payment_status: 'paid',
|
|
125
|
+
payment_method: 'stripe',
|
|
126
|
+
payment_provider: 'stripe',
|
|
127
|
+
updated_at: '2026-10-23T09:15:00.000Z',
|
|
128
|
+
billing_name: 'Card Buyer',
|
|
129
|
+
}
|
|
130
|
+
const LINES: Row[] = [
|
|
131
|
+
{ product_id: 'p1', product_name: 'Bouquet', quantity: 1, unit_price: 119, total_price: 119 },
|
|
132
|
+
]
|
|
133
|
+
// The same basket, delivered and paid in cash at the door: invoiced when it is
|
|
134
|
+
// placed, before any money moved.
|
|
135
|
+
const CASH_ORDER: Row = {
|
|
136
|
+
...PAID_CARD_ORDER,
|
|
137
|
+
id: 'order-3',
|
|
138
|
+
order_number: 'ORD-3',
|
|
139
|
+
total_amount: 134,
|
|
140
|
+
shipping_amount: 15,
|
|
141
|
+
status: 'pending',
|
|
142
|
+
payment_status: 'unpaid',
|
|
143
|
+
payment_method: 'cash_on_delivery',
|
|
144
|
+
payment_provider: '',
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// The editor's default invoice template, reduced to the parts this covers: the
|
|
148
|
+
// <context> carrying the design canvas's sample (teleport-gui
|
|
149
|
+
// invoice-template-builder.ts), the dates under the number, the total, the
|
|
150
|
+
// notes and the seller's VAT number.
|
|
151
|
+
const SAMPLE_NOTES = 'Payment is due within 30 days. Thank you for your business.'
|
|
152
|
+
const boundText = (path: string) => ({
|
|
153
|
+
type: 'element',
|
|
154
|
+
content: {
|
|
155
|
+
elementType: 'text',
|
|
156
|
+
semanticType: 'p',
|
|
157
|
+
children: [{ type: 'expr', content: `invoiceData?.${path}` }],
|
|
158
|
+
},
|
|
159
|
+
})
|
|
160
|
+
const INVOICE_TEMPLATE = {
|
|
161
|
+
name: 'invoice-template',
|
|
162
|
+
node: {
|
|
163
|
+
type: 'element',
|
|
164
|
+
content: {
|
|
165
|
+
elementType: 'context',
|
|
166
|
+
attrs: {
|
|
167
|
+
Company: {
|
|
168
|
+
type: 'static',
|
|
169
|
+
content: { name: 'Acme Corporation', vat: 'US123456789', logoUrl: '' },
|
|
170
|
+
},
|
|
171
|
+
Invoice: {
|
|
172
|
+
type: 'static',
|
|
173
|
+
content: {
|
|
174
|
+
invoiceNumber: '#INV-001',
|
|
175
|
+
status: 'paid',
|
|
176
|
+
issueDate: 'Jan 15, 2025',
|
|
177
|
+
dueDate: 'Feb 14, 2025',
|
|
178
|
+
paidDate: 'Jan 20, 2025',
|
|
179
|
+
total: '371.49',
|
|
180
|
+
currency: 'USD',
|
|
181
|
+
notes: SAMPLE_NOTES,
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
},
|
|
185
|
+
children: [
|
|
186
|
+
boundText('Invoice?.invoiceNumber'),
|
|
187
|
+
boundText('Invoice?.issueDate'),
|
|
188
|
+
boundText('Invoice?.dueDate'),
|
|
189
|
+
boundText('Invoice?.total'),
|
|
190
|
+
boundText('Invoice?.notes'),
|
|
191
|
+
boundText('Invoice?.paidDate'),
|
|
192
|
+
boundText('Company?.vat'),
|
|
193
|
+
],
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const assembly = loadAssembly()
|
|
199
|
+
const pdfGenerator = loadPdfGenerator()
|
|
200
|
+
|
|
201
|
+
function invoiceFor(order: Row, body: Row = {}): Row {
|
|
202
|
+
const invoice = assembly.assembleInvoiceData(
|
|
203
|
+
{ orderId: order.id, issueDate: '2026-10-23', ...body },
|
|
204
|
+
order,
|
|
205
|
+
LINES
|
|
206
|
+
)
|
|
207
|
+
return { ...invoice, invoiceNumber: 'INV-0002' }
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function emailFor(invoice: Row, body = INVOICE_EMAIL_BODY): string {
|
|
211
|
+
return pdfGenerator.replacePlaceholders(body, pdfGenerator.buildDataContext(invoice))
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
describe('the invoice of a paid order', () => {
|
|
215
|
+
it('is issued paid, at the time the payment marked the order', () => {
|
|
216
|
+
const invoice = invoiceFor(PAID_CARD_ORDER)
|
|
217
|
+
expect(invoice.status).toBe('paid')
|
|
218
|
+
expect(invoice.paidAt).toBe('2026-10-23T09:15:00.000Z')
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
it('keeps the status and payment time a caller states', () => {
|
|
222
|
+
const invoice = invoiceFor(PAID_CARD_ORDER, { status: 'issued' })
|
|
223
|
+
expect(invoice.status).toBe('issued')
|
|
224
|
+
expect(invoice.paidAt).toBeNull()
|
|
225
|
+
const stated = invoiceFor(PAID_CARD_ORDER, { status: 'paid', paidAt: '2026-10-20T08:00:00Z' })
|
|
226
|
+
expect(stated.paidAt).toBe('2026-10-20T08:00:00Z')
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
it('emails no due date, no amount due and no zero lines', () => {
|
|
230
|
+
const email = emailFor(invoiceFor(PAID_CARD_ORDER))
|
|
231
|
+
expect(email).toContain('<span>Invoice number: </span><span>INV-0002</span>')
|
|
232
|
+
expect(email).toContain(
|
|
233
|
+
'<span>Invoice date: </span><span>' + printedDate('2026-10-23') + '</span>'
|
|
234
|
+
)
|
|
235
|
+
expect(email).toContain('<span>Subtotal: </span><span>lei 119.00</span>')
|
|
236
|
+
expect(email).toContain('<span>Total: </span><span>lei 119.00</span>')
|
|
237
|
+
for (const gone of ['Due date', 'Amount due', 'Discount', 'Tax', 'Delivery', 'gift card']) {
|
|
238
|
+
expect(email).not.toContain(gone)
|
|
239
|
+
}
|
|
240
|
+
// Only the rows went: the summary box and the rest of the message stay.
|
|
241
|
+
expect(email).toContain('<div style="margin: 0 0 22px">')
|
|
242
|
+
expect(email).toContain('Best regards,')
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
it('prints a paid line where the PDF printed the due date', () => {
|
|
246
|
+
const invoice = invoiceFor(PAID_CARD_ORDER)
|
|
247
|
+
const html = loadPdfGenerator(INVOICE_TEMPLATE).buildInvoiceHtml(invoice)
|
|
248
|
+
expect(html).toContain('<p>Paid on ' + printedDate('2026-10-23T09:15:00.000Z') + '</p>')
|
|
249
|
+
expect(html).not.toContain(printedDate(invoice.dueDate))
|
|
250
|
+
expect(html).toContain('<p>lei 119.00</p>')
|
|
251
|
+
// The built-in layout of a store without a template says so too.
|
|
252
|
+
expect(pdfGenerator.buildInvoiceHtml(invoice)).toContain(
|
|
253
|
+
'Paid on ' + printedDate('2026-10-23T09:15:00.000Z')
|
|
254
|
+
)
|
|
255
|
+
})
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
describe('the invoice of a cash-on-delivery order', () => {
|
|
259
|
+
it('is issued unpaid and due in 30 days', () => {
|
|
260
|
+
const invoice = invoiceFor(CASH_ORDER)
|
|
261
|
+
expect(invoice.status).toBe('issued')
|
|
262
|
+
expect(invoice.paidAt).toBeNull()
|
|
263
|
+
expect(invoice.dueDate).toBe('2026-11-22')
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
it('emails its due date and the delivery, and no amount due without a gift card', () => {
|
|
267
|
+
const email = emailFor(invoiceFor(CASH_ORDER))
|
|
268
|
+
expect(email).toContain(
|
|
269
|
+
'<span>Due date: </span><span>' + printedDate('2026-11-22') + '</span></div>'
|
|
270
|
+
)
|
|
271
|
+
expect(email).toContain('<span>Delivery: </span><span>lei 15.00</span>')
|
|
272
|
+
expect(email).toContain('<span>Total: </span><span>lei 134.00</span>')
|
|
273
|
+
for (const gone of ['Amount due', 'Discount', 'Tax', 'gift card']) {
|
|
274
|
+
expect(email).not.toContain(gone)
|
|
275
|
+
}
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
it('keeps the due date on its PDF', () => {
|
|
279
|
+
const invoice = invoiceFor(CASH_ORDER)
|
|
280
|
+
const html = loadPdfGenerator(INVOICE_TEMPLATE).buildInvoiceHtml(invoice)
|
|
281
|
+
expect(html).toContain('<p>' + printedDate('2026-11-22') + '</p>')
|
|
282
|
+
expect(html).not.toContain('Paid on')
|
|
283
|
+
expect(pdfGenerator.buildInvoiceHtml(invoice)).not.toContain('Paid on')
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
it('emails what a gift card paid and what is left to pay at the door', () => {
|
|
287
|
+
const email = emailFor(invoiceFor({ ...CASH_ORDER, gift_card_amount: 50 }))
|
|
288
|
+
expect(email).toContain(
|
|
289
|
+
'<span>Paid by gift card: </span><span>−</span><span>lei 50.00</span></div>'
|
|
290
|
+
)
|
|
291
|
+
expect(email).toContain('<span>Amount due: </span><span>lei 84.00</span></div>')
|
|
292
|
+
expect(email).toContain('<span>Due date: </span>')
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
it('emails no amount due once the rest was paid by card', () => {
|
|
296
|
+
const email = emailFor(invoiceFor({ ...PAID_CARD_ORDER, gift_card_amount: 50 }))
|
|
297
|
+
expect(email).toContain('<span>Paid by gift card: </span><span>−</span><span>lei 50.00</span>')
|
|
298
|
+
expect(email).not.toContain('Amount due')
|
|
299
|
+
})
|
|
300
|
+
})
|
|
301
|
+
|
|
302
|
+
describe('the invoice email drops a zero line, never what holds it', () => {
|
|
303
|
+
const zeroDeliveryInvoice = invoiceFor(CASH_ORDER, { shippingAmount: 0 })
|
|
304
|
+
|
|
305
|
+
it("keeps a merchant's own block around the line it drops", () => {
|
|
306
|
+
const body =
|
|
307
|
+
'<div class="merchant-summary">' +
|
|
308
|
+
'<div class="row"><span>Delivery: </span><span>{{shippingAmount}}</span></div>' +
|
|
309
|
+
'<div class="row"><span>Total: </span><span>{{totalAmount}}</span></div>' +
|
|
310
|
+
'<p>Thank you for shopping with us.</p>' +
|
|
311
|
+
'</div>'
|
|
312
|
+
expect(emailFor(zeroDeliveryInvoice, body)).toBe(
|
|
313
|
+
'<div class="merchant-summary">' +
|
|
314
|
+
'<div class="row"><span>Total: </span><span>lei 119.00</span></div>' +
|
|
315
|
+
'<p>Thank you for shopping with us.</p>' +
|
|
316
|
+
'</div>'
|
|
317
|
+
)
|
|
318
|
+
})
|
|
319
|
+
|
|
320
|
+
it('keeps a block that holds more than the line', () => {
|
|
321
|
+
const withBlocks =
|
|
322
|
+
'<div class="note"><span>Delivery {{shippingAmount}}</span><p>See you soon.</p></div>'
|
|
323
|
+
expect(emailFor(zeroDeliveryInvoice, withBlocks)).toBe(
|
|
324
|
+
'<div class="note"><span>Delivery lei 0.00</span><p>See you soon.</p></div>'
|
|
325
|
+
)
|
|
326
|
+
const withFigures = '<div>Delivery {{shippingAmount}}, total {{totalAmount}}.</div>'
|
|
327
|
+
expect(emailFor(zeroDeliveryInvoice, withFigures)).toBe(
|
|
328
|
+
'<div>Delivery lei 0.00, total lei 119.00.</div>'
|
|
329
|
+
)
|
|
330
|
+
})
|
|
331
|
+
})
|
|
332
|
+
|
|
333
|
+
describe('money on the invoice', () => {
|
|
334
|
+
it.each([
|
|
335
|
+
['RON', 'lei 119.00'],
|
|
336
|
+
['CHF', 'CHF 119.00'],
|
|
337
|
+
['USD', '$119.00'],
|
|
338
|
+
['EUR', '€119.00'],
|
|
339
|
+
])('writes %s as %s in the email and the PDF alike', (currency, printed) => {
|
|
340
|
+
const invoice = invoiceFor({ ...PAID_CARD_ORDER, currency })
|
|
341
|
+
expect(emailFor(invoice, '<span>{{totalAmount}}</span>')).toBe(`<span>${printed}</span>`)
|
|
342
|
+
expect(pdfGenerator.buildInvoiceDataScope(invoice).invoiceData.Invoice.total).toBe(printed)
|
|
343
|
+
})
|
|
344
|
+
})
|
|
345
|
+
|
|
346
|
+
describe("the invoice PDF never prints the template's sample", () => {
|
|
347
|
+
it('prints nothing for a field the real invoice leaves empty', () => {
|
|
348
|
+
const html = loadPdfGenerator(INVOICE_TEMPLATE).buildInvoiceHtml(
|
|
349
|
+
invoiceFor(CASH_ORDER, { notes: '' })
|
|
350
|
+
)
|
|
351
|
+
expect(html).not.toContain(SAMPLE_NOTES)
|
|
352
|
+
expect(html).not.toContain('Jan 20, 2025')
|
|
353
|
+
expect(html).not.toContain('US123456789')
|
|
354
|
+
expect(html).not.toContain('371.49')
|
|
355
|
+
expect(html).toContain('<p>#INV-0002</p>')
|
|
356
|
+
expect(html).toContain('<p>lei 134.00</p>')
|
|
357
|
+
})
|
|
358
|
+
|
|
359
|
+
it("prints the invoice's own notes", () => {
|
|
360
|
+
const html = loadPdfGenerator(INVOICE_TEMPLATE).buildInvoiceHtml(
|
|
361
|
+
invoiceFor(CASH_ORDER, { notes: 'Leave it with the neighbour.' })
|
|
362
|
+
)
|
|
363
|
+
expect(html).toContain('<p>Leave it with the neighbour.</p>')
|
|
364
|
+
})
|
|
365
|
+
|
|
366
|
+
it("keeps the company logo the editor wrote onto the template's context", () => {
|
|
367
|
+
const withLogo = {
|
|
368
|
+
...INVOICE_TEMPLATE,
|
|
369
|
+
node: {
|
|
370
|
+
...INVOICE_TEMPLATE.node,
|
|
371
|
+
content: {
|
|
372
|
+
...INVOICE_TEMPLATE.node.content,
|
|
373
|
+
attrs: {
|
|
374
|
+
Company: { type: 'static', content: { name: '', logoUrl: 'https://cdn/logo.png' } },
|
|
375
|
+
},
|
|
376
|
+
},
|
|
377
|
+
},
|
|
378
|
+
}
|
|
379
|
+
const scope = loadPdfGenerator(withLogo).buildInvoiceDataScope(invoiceFor(CASH_ORDER))
|
|
380
|
+
expect(scope.invoiceData.Company.logoUrl).toBe('https://cdn/logo.png')
|
|
381
|
+
})
|
|
382
|
+
})
|
|
@@ -420,7 +420,9 @@ describe('invoice PDF renderer — vouchers and the gift-card tender', () => {
|
|
|
420
420
|
expect(voucherEmail).not.toContain('Delivery')
|
|
421
421
|
expect(voucherEmail).not.toContain('gift card')
|
|
422
422
|
expect(voucherEmail).toContain('<tr><td>Discount</td><td>−$25.00</td></tr>')
|
|
423
|
-
|
|
423
|
+
// Without a gift card nothing is left beside the total, so, as on the PDF,
|
|
424
|
+
// there is no amount due.
|
|
425
|
+
expect(voucherEmail).not.toContain('Amount due')
|
|
424
426
|
|
|
425
427
|
const giftCardEmail = pdfGenerator.replacePlaceholders(
|
|
426
428
|
body,
|
|
@@ -434,10 +436,10 @@ describe('invoice PDF renderer — vouchers and the gift-card tender', () => {
|
|
|
434
436
|
// A rich-text editor turns table rows into paragraphs; the same rule holds.
|
|
435
437
|
expect(
|
|
436
438
|
pdfGenerator.replacePlaceholders(
|
|
437
|
-
'<p>Delivery {{shippingAmount}}</p><p>
|
|
439
|
+
'<p>Delivery {{shippingAmount}}</p><p>Total {{totalAmount}}</p>',
|
|
438
440
|
pdfGenerator.buildDataContext(voucherInvoice)
|
|
439
441
|
)
|
|
440
|
-
).toBe('<p>
|
|
442
|
+
).toBe('<p>Total $75.00</p>')
|
|
441
443
|
})
|
|
442
444
|
})
|
|
443
445
|
|
|
@@ -6,15 +6,18 @@
|
|
|
6
6
|
* 1. resolves the current user from the NextAuth session (never trusting a
|
|
7
7
|
* client-supplied id),
|
|
8
8
|
* 2. runs a single DB transaction that detaches + anonymises business/legal
|
|
9
|
-
* records (orders, withdrawal requests) so tracking survives,
|
|
10
|
-
* user's personal data (reviews, cart, favourites, chat
|
|
11
|
-
* user
|
|
9
|
+
* records (orders, withdrawal requests, bookings) so tracking survives,
|
|
10
|
+
* deletes the user's personal data (reviews, cart, favourites, chat
|
|
11
|
+
* history), deletes the rows the user owns in the project's own tables —
|
|
12
|
+
* the ones the AI made for the site, each through the one column the
|
|
13
|
+
* site's "My …" pages read it by (teleport-shared's resolveOwnerColumn,
|
|
14
|
+
* shipped into the route as text) — and then the user row, and
|
|
12
15
|
* 3. best-effort sends the farewell email (a failed send never fails the
|
|
13
16
|
* deletion).
|
|
14
17
|
*
|
|
15
|
-
* Every table statement is guarded by a runtime `information_schema`
|
|
16
|
-
* check, so the route is a no-op for feature tables a given project
|
|
17
|
-
* provisioned.
|
|
18
|
+
* Every platform-table statement is guarded by a runtime `information_schema`
|
|
19
|
+
* column check, so the route is a no-op for feature tables a given project
|
|
20
|
+
* never provisioned.
|
|
18
21
|
*/
|
|
19
22
|
import { UIDLLocalizedEmailTemplates } from '@teleporthq/teleport-types';
|
|
20
23
|
export interface AccountDeleteRouteOptions {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"account-delete-route-generator.d.ts","sourceRoot":"","sources":["../../src/account-delete-route-generator.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"account-delete-route-generator.d.ts","sourceRoot":"","sources":["../../src/account-delete-route-generator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,OAAO,EAAE,2BAA2B,EAAE,MAAM,4BAA4B,CAAA;AAaxE,MAAM,WAAW,yBAAyB;IAExC,kBAAkB,CAAC,EAAE,MAAM,CAAA;IAI3B,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC7B,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,kBAAkB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAClC,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,aAAa,CAAC,EAAE,MAAM,CAAA;IAGtB,kBAAkB,CAAC,EAAE,2BAA2B,CAAA;IAEhD,QAAQ,CAAC,EAAE,MAAM,CAAA;IAEjB,mBAAmB,CAAC,EAAE,MAAM,CAAA;CAC7B;AAID,eAAO,MAAM,8BAA8B,+CAAiC,CAAA;AAE5E,eAAO,MAAM,0BAA0B,aAAa,yBAAyB,KAAQ,MAqXpF,CAAA"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.generateAccountDeleteRoute = exports.accountDeleteRouteDependencies = void 0;
|
|
4
|
+
const teleport_shared_1 = require("@teleporthq/teleport-shared");
|
|
4
5
|
const pg_client_code_1 = require("./pg-client-code");
|
|
5
6
|
const session_cookie_resolver_1 = require("./session-cookie-resolver");
|
|
6
7
|
const transactional_email_code_1 = require("./transactional-email-code");
|
|
@@ -35,11 +36,22 @@ const DELETE_TABLES = [
|
|
|
35
36
|
'teleport_cart',
|
|
36
37
|
'teleport_ai_chat_messages',
|
|
37
38
|
'teleport_ai_chat_conversations',
|
|
39
|
+
// A push subscription is the capability to notify the user's device.
|
|
40
|
+
'teleport_push_subscriptions',
|
|
38
41
|
];
|
|
39
42
|
// Business / legal records that must survive: the user link is nulled and any
|
|
40
43
|
// stored personal email is replaced with a synthetic, self-describing address.
|
|
41
|
-
const ANONYMIZE_TABLES = [
|
|
44
|
+
const ANONYMIZE_TABLES = [
|
|
45
|
+
'teleport_orders',
|
|
46
|
+
'teleport_withdrawal_requests',
|
|
47
|
+
'teleport_bookings',
|
|
48
|
+
'teleport_subscriptions',
|
|
49
|
+
'teleport_abandoned_carts',
|
|
50
|
+
'teleport_voucher_redemptions',
|
|
51
|
+
];
|
|
42
52
|
const EMAIL_COLUMNS = ['billing_email', 'shipping_email', 'customer_email', 'email'];
|
|
53
|
+
// A booking also names the customer and carries their phone; both are cleared.
|
|
54
|
+
const CLEARED_COLUMNS = { teleport_bookings: ['customer_name', 'customer_phone'] };
|
|
43
55
|
|
|
44
56
|
async function tableColumns(client, table) {
|
|
45
57
|
var r = await client.query(
|
|
@@ -62,6 +74,10 @@ async function anonymizeUserRows(client, table, userId, syntheticEmail) {
|
|
|
62
74
|
sets.push(EMAIL_COLUMNS[i] + ' = $' + params.length);
|
|
63
75
|
}
|
|
64
76
|
}
|
|
77
|
+
var cleared = CLEARED_COLUMNS[table] || [];
|
|
78
|
+
for (var c = 0; c < cleared.length; c++) {
|
|
79
|
+
if (cols.indexOf(cleared[c]) !== -1) { sets.push(cleared[c] + ' = NULL'); }
|
|
80
|
+
}
|
|
65
81
|
await client.query('UPDATE ' + table + ' SET ' + sets.join(', ') + ' WHERE user_id = $1', params);
|
|
66
82
|
}
|
|
67
83
|
|
|
@@ -78,6 +94,119 @@ async function deleteRowsByEmail(client, table, column, email) {
|
|
|
78
94
|
await client.query('DELETE FROM ' + table + ' WHERE ' + column + ' = $1', [email]);
|
|
79
95
|
}
|
|
80
96
|
|
|
97
|
+
// Rows the user owns in the project's OWN tables — the ones the AI made for
|
|
98
|
+
// this site (saved items, requests, reviews …). Their link to the user is
|
|
99
|
+
// ON DELETE SET NULL, so they outlived the account with a blank owner, and the
|
|
100
|
+
// "My …" pages read "owner = me OR owner IS NULL": every other signed-in
|
|
101
|
+
// visitor saw them. Platform tables keep the explicit handling above —
|
|
102
|
+
// deleted, anonymised, or kept as the business's records.
|
|
103
|
+
//
|
|
104
|
+
// Each table is attributed through exactly ONE column, the one its "My …"
|
|
105
|
+
// pages read it by: resolveOwnerColumn below is the AI pipeline's own
|
|
106
|
+
// decision, shipped here as text, so the two sides cannot disagree. Rows go
|
|
107
|
+
// only through an "owner" column (whose the row is: author, submitter,
|
|
108
|
+
// sender); an "actor" column (who approved, reviewed, was assigned or
|
|
109
|
+
// addressed) names somebody else's row and leaves the table alone.
|
|
110
|
+
${teleport_shared_1.OwnerColumns.generateOwnerColumnResolverCode()}
|
|
111
|
+
|
|
112
|
+
const PLATFORM_TABLE_PREFIX = 'teleport_';
|
|
113
|
+
const PLATFORM_TABLES = [AUTH_USERS_TABLE, 'password_reset_tokens'];
|
|
114
|
+
|
|
115
|
+
function isPlatformTable(table) {
|
|
116
|
+
return PLATFORM_TABLES.indexOf(table) !== -1 || table.indexOf(PLATFORM_TABLE_PREFIX) === 0;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function quoteIdentifier(name) {
|
|
120
|
+
return '"' + String(name).split('"').join('""') + '"';
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// The (table, column) pairs through which the project's own tables name their
|
|
124
|
+
// owner: the resolved owner column when it references the users table — or
|
|
125
|
+
// references nothing, in older projects whose link was dropped. One that
|
|
126
|
+
// references another table (account_id -> accounts) is a domain link and the
|
|
127
|
+
// table is left alone. Links are read from pg_constraint rather than
|
|
128
|
+
// information_schema: constraint names are only unique per table, so a join
|
|
129
|
+
// by name can pair the wrong tables.
|
|
130
|
+
async function ownedRowLinks(client) {
|
|
131
|
+
var columns = await client.query(
|
|
132
|
+
"SELECT c.table_name, c.column_name, c.ordinal_position FROM information_schema.columns c" +
|
|
133
|
+
" JOIN information_schema.tables t ON t.table_schema = c.table_schema AND t.table_name = c.table_name" +
|
|
134
|
+
" WHERE c.table_schema = 'public' AND t.table_type = 'BASE TABLE'" +
|
|
135
|
+
" ORDER BY c.table_name, c.ordinal_position"
|
|
136
|
+
);
|
|
137
|
+
var links = await client.query(
|
|
138
|
+
"SELECT cl.relname AS table_name, a.attname AS column_name," +
|
|
139
|
+
" rcl.relname AS referenced_table, ra.attname AS referenced_column" +
|
|
140
|
+
" FROM pg_constraint k" +
|
|
141
|
+
" JOIN pg_class cl ON cl.oid = k.conrelid" +
|
|
142
|
+
" JOIN pg_namespace n ON n.oid = cl.relnamespace" +
|
|
143
|
+
" JOIN pg_attribute a ON a.attrelid = k.conrelid AND a.attnum = k.conkey[1]" +
|
|
144
|
+
" JOIN pg_class rcl ON rcl.oid = k.confrelid" +
|
|
145
|
+
" JOIN pg_attribute ra ON ra.attrelid = k.confrelid AND ra.attnum = k.confkey[1]" +
|
|
146
|
+
" WHERE k.contype = 'f' AND n.nspname = 'public' AND array_length(k.conkey, 1) = 1"
|
|
147
|
+
);
|
|
148
|
+
var referenced = {};
|
|
149
|
+
for (var i = 0; i < links.rows.length; i++) {
|
|
150
|
+
var link = links.rows[i];
|
|
151
|
+
referenced[link.table_name + '.' + link.column_name] = link.referenced_table + '.' + link.referenced_column;
|
|
152
|
+
}
|
|
153
|
+
var tables = [];
|
|
154
|
+
var columnsOf = {};
|
|
155
|
+
for (var j = 0; j < columns.rows.length; j++) {
|
|
156
|
+
var row = columns.rows[j];
|
|
157
|
+
if (!columnsOf[row.table_name]) {
|
|
158
|
+
columnsOf[row.table_name] = [];
|
|
159
|
+
tables.push(row.table_name);
|
|
160
|
+
}
|
|
161
|
+
columnsOf[row.table_name].push({ name: row.column_name, ordinalPosition: Number(row.ordinal_position) });
|
|
162
|
+
}
|
|
163
|
+
var owned = [];
|
|
164
|
+
for (var t = 0; t < tables.length; t++) {
|
|
165
|
+
var table = tables[t];
|
|
166
|
+
if (isPlatformTable(table)) { continue; }
|
|
167
|
+
var resolved = resolveOwnerColumn(columnsOf[table]);
|
|
168
|
+
if (!resolved || resolved.kind !== 'owner') { continue; }
|
|
169
|
+
var target = referenced[table + '.' + resolved.column];
|
|
170
|
+
if (target && target !== AUTH_USERS_TABLE + '.id') { continue; }
|
|
171
|
+
owned.push({ table: table, column: resolved.column });
|
|
172
|
+
}
|
|
173
|
+
return owned;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Each table is deleted from under its own savepoint, so one that refuses
|
|
177
|
+
// (another user's row still points at this user's, through a link without
|
|
178
|
+
// ON DELETE CASCADE) stops neither the other tables nor the account from being
|
|
179
|
+
// deleted. A refused table is retried once the rest are done — the blocker may
|
|
180
|
+
// have been this user's own row in another table; what is still refused stays
|
|
181
|
+
// as it always did, and the log names it.
|
|
182
|
+
async function deleteOwnedRows(client, userId) {
|
|
183
|
+
var pending = await ownedRowLinks(client);
|
|
184
|
+
while (pending.length > 0) {
|
|
185
|
+
var refused = [];
|
|
186
|
+
for (var i = 0; i < pending.length; i++) {
|
|
187
|
+
var link = pending[i];
|
|
188
|
+
await client.query('SAVEPOINT owned_rows');
|
|
189
|
+
try {
|
|
190
|
+
await client.query(
|
|
191
|
+
'DELETE FROM ' + quoteIdentifier(link.table) + ' WHERE ' + quoteIdentifier(link.column) + ' = $1',
|
|
192
|
+
[userId]
|
|
193
|
+
);
|
|
194
|
+
await client.query('RELEASE SAVEPOINT owned_rows');
|
|
195
|
+
} catch (err) {
|
|
196
|
+
await client.query('ROLLBACK TO SAVEPOINT owned_rows');
|
|
197
|
+
await client.query('RELEASE SAVEPOINT owned_rows');
|
|
198
|
+
link.error = err && err.message ? err.message : String(err);
|
|
199
|
+
refused.push(link);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
if (refused.length === pending.length) { break; }
|
|
203
|
+
pending = refused;
|
|
204
|
+
}
|
|
205
|
+
for (var k = 0; k < pending.length; k++) {
|
|
206
|
+
console.error('[account-delete] rows kept in ' + pending[k].table + ' (' + pending[k].column + '): ' + pending[k].error);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
81
210
|
// Invoices are a financial/tax record that must survive, but they store the
|
|
82
211
|
// customer's PII denormalised and link to the user only through order_id (no
|
|
83
212
|
// user_id). Scrub the PII in place, matching invoices whose order belongs to the
|
|
@@ -197,6 +326,7 @@ module.exports = async function handler(req, res) {
|
|
|
197
326
|
for (var d = 0; d < DELETE_TABLES.length; d++) {
|
|
198
327
|
await deleteUserRows(client, DELETE_TABLES[d], userId);
|
|
199
328
|
}
|
|
329
|
+
await deleteOwnedRows(client, userId);
|
|
200
330
|
// password_reset_tokens has no user_id — it is keyed by the email string.
|
|
201
331
|
await deleteRowsByEmail(client, 'password_reset_tokens', 'email', userEmail);
|
|
202
332
|
await client.query('DELETE FROM ' + AUTH_USERS_TABLE + ' WHERE id = $1', [userId]);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"account-delete-route-generator.js","sourceRoot":"","sources":["../../src/account-delete-route-generator.ts"],"names":[],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"account-delete-route-generator.js","sourceRoot":"","sources":["../../src/account-delete-route-generator.ts"],"names":[],"mappings":";;;AAsBA,iEAA0D;AAC1D,qDAAuD;AACvD,uEAAoF;AACpF,yEAOmC;AAuBnC,+EAA+E;AAC/E,oFAAoF;AACvE,QAAA,8BAA8B,GAAG,yDAA8B,CAAA;AAErE,MAAM,0BAA0B,GAAG,CAAC,UAAqC,EAAE,EAAU,EAAE;IAC5F,MAAM,cAAc,GAAG,OAAO,CAAC,kBAAkB,IAAI,OAAO,CAAA;IAC5D,MAAM,QAAQ,GACZ,OAAO,CAAC,aAAa,IAAI,oDAAyB,CAAC,GAAG,CAAC,OAAO,CAAC,aAAa,CAAC;QAC3E,CAAC,CAAC,OAAO,CAAC,aAAa;QACvB,CAAC,CAAC,IAAI,CAAA;IACV,MAAM,mBAAmB,GACvB,OAAO,CAAC,mBAAmB,IAAI,yCAAyC,CAAA;IAE1E,OAAO,GAAG,IAAA,qCAAoB,GAAE;EAChC,IAAA,kEAAwC,GAAE;2BACjB,IAAI,CAAC,SAAS,CAAC,cAAc,CAAC;oBACrC,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,QAAQ,IAAI,EAAE,CAAC;gCAC1B,IAAI,CAAC,SAAS,CAAC,mBAAmB,CAAC;yBAC1C,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC;qBAC5B,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,SAAS,IAAI,EAAE,CAAC;0BAClC,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,QAAQ,IAAI,EAAE,CAAC;gCAChC,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,kBAAkB,IAAI,EAAE,CAAC;wBACxD,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,YAAY,IAAI,+BAA+B,CAAC;0BACrE,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,aAAa,IAAI,EAAE,CAAC;0BAC3C,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,kBAAkB,IAAI,EAAE,CAAC;EACxE,IAAA,qDAA0B,EAAC,UAAU,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAiFtC,8BAAY,CAAC,+BAA+B,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA4I9C,IAAA,iDAAsB,GAAE;;EAExB,IAAA,uDAA4B,EAAC;QAC7B,MAAM,EAAE,0BAA0B;QAClC,YAAY,EAAE,eAAe;QAC7B,SAAS,EAAE,iBAAiB;QAC5B,cAAc,EAAE,iBAAiB;KAClC,CAAC;;EAEA,IAAA,uDAA4B,EAAC,QAAQ,EAAE;QACvC,SAAS,EAAE,iBAAiB;QAC5B,MAAM,EAAE,gBAAgB;QACxB,SAAS,EAAE,4BAA4B;QACvC,cAAc,EAAE,UAAU;KAC3B,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA4GA,QAAQ;QACN,CAAC,CAAC;;CAEL;QACG,CAAC,CAAC,EACN;;;CAGC,CAAA;AACD,CAAC,CAAA;AArXY,QAAA,0BAA0B,8BAqXtC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"invoice-assembly-code.d.ts","sourceRoot":"","sources":["../../../src/invoice/invoice-assembly-code.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAA;AAIhE;;;;;;;;GAQG;AACH,eAAO,MAAM,2BAA2B,aAAc,mBAAmB,KAAG,
|
|
1
|
+
{"version":3,"file":"invoice-assembly-code.d.ts","sourceRoot":"","sources":["../../../src/invoice/invoice-assembly-code.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAA;AAIhE;;;;;;;;GAQG;AACH,eAAO,MAAM,2BAA2B,aAAc,mBAAmB,KAAG,MAijB3E,CAAA"}
|