@teleporthq/teleport-plugin-next-workflows 0.43.75 → 0.44.1
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__/_helpers/fake-store-database.ts +428 -0
- package/__tests__/_helpers/invoice-store.ts +382 -0
- package/__tests__/booking-calendar-feed-route.test.ts +494 -0
- package/__tests__/custom-node-result-taken-branch.test.ts +107 -0
- package/__tests__/data-api-date-parser.test.ts +45 -0
- package/__tests__/email-locale-senders.test.ts +3 -1
- package/__tests__/email-sender-name.test.ts +513 -0
- package/__tests__/graph-utils-topological-order.test.ts +155 -1
- package/__tests__/invoice-billing-details.test.ts +384 -0
- package/__tests__/invoice-data-access-replace.test.ts +236 -2
- package/__tests__/invoice-order-items-settle.test.ts +1 -1
- package/__tests__/invoice-payment-email.test.ts +191 -0
- package/__tests__/invoice-regenerate-email.test.ts +288 -0
- package/__tests__/invoice-regenerate-route.test.ts +9 -7
- package/__tests__/invoice-reservation.test.ts +1 -1
- package/__tests__/invoice-tender.test.ts +1 -1
- package/__tests__/middleware-admin-panel-roles.test.ts +127 -0
- package/__tests__/page-arrival-remount.test.ts +247 -0
- package/__tests__/payment-drivers-module.test.ts +19 -0
- package/__tests__/raw-query-error-code.test.ts +47 -0
- package/__tests__/runtime-template-token-resolution.test.ts +16 -0
- package/__tests__/segment-reply-changed-keys.test.ts +31 -0
- package/__tests__/segment-route-skipped-branch.test.ts +101 -0
- package/__tests__/segment-splitter-checkout-order.test.ts +306 -0
- package/__tests__/streaming-on-end-env-filter.test.ts +18 -18
- package/__tests__/streaming-on-end-order.test.ts +387 -0
- package/dist/cjs/account-delete-route-generator.d.ts +1 -0
- package/dist/cjs/account-delete-route-generator.d.ts.map +1 -1
- package/dist/cjs/account-delete-route-generator.js +2 -1
- package/dist/cjs/account-delete-route-generator.js.map +1 -1
- package/dist/cjs/api-route-generator.d.ts.map +1 -1
- package/dist/cjs/api-route-generator.js +23 -13
- package/dist/cjs/api-route-generator.js.map +1 -1
- package/dist/cjs/auth-generator.d.ts +1 -0
- package/dist/cjs/auth-generator.d.ts.map +1 -1
- package/dist/cjs/auth-generator.js +77 -28
- package/dist/cjs/auth-generator.js.map +1 -1
- package/dist/cjs/booking-calendar-feed-route-generator.d.ts +28 -0
- package/dist/cjs/booking-calendar-feed-route-generator.d.ts.map +1 -0
- package/dist/cjs/booking-calendar-feed-route-generator.js +296 -0
- package/dist/cjs/booking-calendar-feed-route-generator.js.map +1 -0
- package/dist/cjs/data-api-route-generator.d.ts.map +1 -1
- package/dist/cjs/data-api-route-generator.js +6 -1
- package/dist/cjs/data-api-route-generator.js.map +1 -1
- package/dist/cjs/executor-generator.d.ts.map +1 -1
- package/dist/cjs/executor-generator.js +39 -28
- package/dist/cjs/executor-generator.js.map +1 -1
- package/dist/cjs/graph-utils.d.ts +31 -1
- package/dist/cjs/graph-utils.d.ts.map +1 -1
- package/dist/cjs/graph-utils.js +112 -10
- package/dist/cjs/graph-utils.js.map +1 -1
- package/dist/cjs/index.d.ts +1 -1
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js +2 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/invoice/api-routes-code.d.ts +12 -3
- package/dist/cjs/invoice/api-routes-code.d.ts.map +1 -1
- package/dist/cjs/invoice/api-routes-code.js +152 -21
- package/dist/cjs/invoice/api-routes-code.js.map +1 -1
- package/dist/cjs/invoice/data-access-code.d.ts.map +1 -1
- package/dist/cjs/invoice/data-access-code.js +133 -21
- package/dist/cjs/invoice/data-access-code.js.map +1 -1
- package/dist/cjs/invoice/email-sender-code.d.ts.map +1 -1
- package/dist/cjs/invoice/email-sender-code.js +15 -25
- package/dist/cjs/invoice/email-sender-code.js.map +1 -1
- package/dist/cjs/invoice/index.js +1 -1
- package/dist/cjs/invoice/index.js.map +1 -1
- package/dist/cjs/invoice/invoice-assembly-code.d.ts.map +1 -1
- package/dist/cjs/invoice/invoice-assembly-code.js +50 -6
- 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 +13 -2
- package/dist/cjs/invoice/invoice-html-code.js.map +1 -1
- package/dist/cjs/nodes/data/data-raw-query.d.ts.map +1 -1
- package/dist/cjs/nodes/data/data-raw-query.js +11 -1
- package/dist/cjs/nodes/data/data-raw-query.js.map +1 -1
- package/dist/cjs/nodes/email/email-mailgun.d.ts.map +1 -1
- package/dist/cjs/nodes/email/email-mailgun.js +4 -2
- package/dist/cjs/nodes/email/email-mailgun.js.map +1 -1
- package/dist/cjs/nodes/email/email-postmark.d.ts.map +1 -1
- package/dist/cjs/nodes/email/email-postmark.js +5 -2
- package/dist/cjs/nodes/email/email-postmark.js.map +1 -1
- package/dist/cjs/nodes/email/email-resend.d.ts.map +1 -1
- package/dist/cjs/nodes/email/email-resend.js +5 -2
- package/dist/cjs/nodes/email/email-resend.js.map +1 -1
- package/dist/cjs/nodes/email/email-sendgrid.d.ts.map +1 -1
- package/dist/cjs/nodes/email/email-sendgrid.js +8 -2
- package/dist/cjs/nodes/email/email-sendgrid.js.map +1 -1
- package/dist/cjs/nodes/general/general-custom-js.d.ts.map +1 -1
- package/dist/cjs/nodes/general/general-custom-js.js +11 -0
- package/dist/cjs/nodes/general/general-custom-js.js.map +1 -1
- package/dist/cjs/nodes/storage/storage-local-remove.d.ts.map +1 -1
- package/dist/cjs/nodes/storage/storage-local-remove.js +2 -0
- package/dist/cjs/nodes/storage/storage-local-remove.js.map +1 -1
- package/dist/cjs/nodes/storage/storage-local-set.d.ts.map +1 -1
- package/dist/cjs/nodes/storage/storage-local-set.js +4 -0
- package/dist/cjs/nodes/storage/storage-local-set.js.map +1 -1
- package/dist/cjs/page-arrival.d.ts +10 -0
- package/dist/cjs/page-arrival.d.ts.map +1 -0
- package/dist/cjs/page-arrival.js +86 -0
- package/dist/cjs/page-arrival.js.map +1 -0
- package/dist/cjs/payments/index.d.ts +10 -4
- package/dist/cjs/payments/index.d.ts.map +1 -1
- package/dist/cjs/payments/index.js +18 -7
- package/dist/cjs/payments/index.js.map +1 -1
- package/dist/cjs/segment-splitter.d.ts.map +1 -1
- package/dist/cjs/segment-splitter.js +65 -27
- package/dist/cjs/segment-splitter.js.map +1 -1
- package/dist/cjs/transactional-email-code.d.ts.map +1 -1
- package/dist/cjs/transactional-email-code.js +32 -18
- package/dist/cjs/transactional-email-code.js.map +1 -1
- package/dist/cjs/tsconfig.tsbuildinfo +1 -1
- package/dist/cjs/workflow-component-plugin.d.ts.map +1 -1
- package/dist/cjs/workflow-component-plugin.js +35 -149
- package/dist/cjs/workflow-component-plugin.js.map +1 -1
- package/dist/cjs/workflow-project-plugin.d.ts.map +1 -1
- package/dist/cjs/workflow-project-plugin.js +16 -4
- package/dist/cjs/workflow-project-plugin.js.map +1 -1
- package/dist/cjs/workflow-scope.d.ts +19 -0
- package/dist/cjs/workflow-scope.d.ts.map +1 -0
- package/dist/cjs/workflow-scope.js +147 -0
- package/dist/cjs/workflow-scope.js.map +1 -0
- package/dist/esm/account-delete-route-generator.d.ts +1 -0
- package/dist/esm/account-delete-route-generator.d.ts.map +1 -1
- package/dist/esm/account-delete-route-generator.js +2 -1
- package/dist/esm/account-delete-route-generator.js.map +1 -1
- package/dist/esm/api-route-generator.d.ts.map +1 -1
- package/dist/esm/api-route-generator.js +23 -13
- package/dist/esm/api-route-generator.js.map +1 -1
- package/dist/esm/auth-generator.d.ts +1 -0
- package/dist/esm/auth-generator.d.ts.map +1 -1
- package/dist/esm/auth-generator.js +77 -28
- package/dist/esm/auth-generator.js.map +1 -1
- package/dist/esm/booking-calendar-feed-route-generator.d.ts +28 -0
- package/dist/esm/booking-calendar-feed-route-generator.d.ts.map +1 -0
- package/dist/esm/booking-calendar-feed-route-generator.js +291 -0
- package/dist/esm/booking-calendar-feed-route-generator.js.map +1 -0
- package/dist/esm/data-api-route-generator.d.ts.map +1 -1
- package/dist/esm/data-api-route-generator.js +6 -1
- package/dist/esm/data-api-route-generator.js.map +1 -1
- package/dist/esm/executor-generator.d.ts.map +1 -1
- package/dist/esm/executor-generator.js +39 -28
- package/dist/esm/executor-generator.js.map +1 -1
- package/dist/esm/graph-utils.d.ts +31 -1
- package/dist/esm/graph-utils.d.ts.map +1 -1
- package/dist/esm/graph-utils.js +109 -9
- package/dist/esm/graph-utils.js.map +1 -1
- package/dist/esm/index.d.ts +1 -1
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +1 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/invoice/api-routes-code.d.ts +12 -3
- package/dist/esm/invoice/api-routes-code.d.ts.map +1 -1
- package/dist/esm/invoice/api-routes-code.js +152 -21
- package/dist/esm/invoice/api-routes-code.js.map +1 -1
- package/dist/esm/invoice/data-access-code.d.ts.map +1 -1
- package/dist/esm/invoice/data-access-code.js +133 -21
- package/dist/esm/invoice/data-access-code.js.map +1 -1
- package/dist/esm/invoice/email-sender-code.d.ts.map +1 -1
- package/dist/esm/invoice/email-sender-code.js +15 -25
- package/dist/esm/invoice/email-sender-code.js.map +1 -1
- package/dist/esm/invoice/index.js +1 -1
- package/dist/esm/invoice/index.js.map +1 -1
- package/dist/esm/invoice/invoice-assembly-code.d.ts.map +1 -1
- package/dist/esm/invoice/invoice-assembly-code.js +50 -6
- 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 +13 -2
- package/dist/esm/invoice/invoice-html-code.js.map +1 -1
- package/dist/esm/nodes/data/data-raw-query.d.ts.map +1 -1
- package/dist/esm/nodes/data/data-raw-query.js +11 -1
- package/dist/esm/nodes/data/data-raw-query.js.map +1 -1
- package/dist/esm/nodes/email/email-mailgun.d.ts.map +1 -1
- package/dist/esm/nodes/email/email-mailgun.js +4 -2
- package/dist/esm/nodes/email/email-mailgun.js.map +1 -1
- package/dist/esm/nodes/email/email-postmark.d.ts.map +1 -1
- package/dist/esm/nodes/email/email-postmark.js +5 -2
- package/dist/esm/nodes/email/email-postmark.js.map +1 -1
- package/dist/esm/nodes/email/email-resend.d.ts.map +1 -1
- package/dist/esm/nodes/email/email-resend.js +5 -2
- package/dist/esm/nodes/email/email-resend.js.map +1 -1
- package/dist/esm/nodes/email/email-sendgrid.d.ts.map +1 -1
- package/dist/esm/nodes/email/email-sendgrid.js +8 -2
- package/dist/esm/nodes/email/email-sendgrid.js.map +1 -1
- package/dist/esm/nodes/general/general-custom-js.d.ts.map +1 -1
- package/dist/esm/nodes/general/general-custom-js.js +11 -0
- package/dist/esm/nodes/general/general-custom-js.js.map +1 -1
- package/dist/esm/nodes/storage/storage-local-remove.d.ts.map +1 -1
- package/dist/esm/nodes/storage/storage-local-remove.js +2 -0
- package/dist/esm/nodes/storage/storage-local-remove.js.map +1 -1
- package/dist/esm/nodes/storage/storage-local-set.d.ts.map +1 -1
- package/dist/esm/nodes/storage/storage-local-set.js +4 -0
- package/dist/esm/nodes/storage/storage-local-set.js.map +1 -1
- package/dist/esm/page-arrival.d.ts +10 -0
- package/dist/esm/page-arrival.d.ts.map +1 -0
- package/dist/esm/page-arrival.js +81 -0
- package/dist/esm/page-arrival.js.map +1 -0
- package/dist/esm/payments/index.d.ts +10 -4
- package/dist/esm/payments/index.d.ts.map +1 -1
- package/dist/esm/payments/index.js +16 -6
- package/dist/esm/payments/index.js.map +1 -1
- package/dist/esm/segment-splitter.d.ts.map +1 -1
- package/dist/esm/segment-splitter.js +66 -28
- package/dist/esm/segment-splitter.js.map +1 -1
- package/dist/esm/transactional-email-code.d.ts.map +1 -1
- package/dist/esm/transactional-email-code.js +32 -18
- package/dist/esm/transactional-email-code.js.map +1 -1
- package/dist/esm/tsconfig.tsbuildinfo +1 -1
- package/dist/esm/workflow-component-plugin.d.ts.map +1 -1
- package/dist/esm/workflow-component-plugin.js +32 -146
- package/dist/esm/workflow-component-plugin.js.map +1 -1
- package/dist/esm/workflow-project-plugin.d.ts.map +1 -1
- package/dist/esm/workflow-project-plugin.js +16 -4
- package/dist/esm/workflow-project-plugin.js.map +1 -1
- package/dist/esm/workflow-scope.d.ts +19 -0
- package/dist/esm/workflow-scope.d.ts.map +1 -0
- package/dist/esm/workflow-scope.js +142 -0
- package/dist/esm/workflow-scope.js.map +1 -0
- package/package.json +4 -4
- package/src/account-delete-route-generator.ts +3 -1
- package/src/api-route-generator.ts +23 -13
- package/src/auth-generator.ts +83 -26
- package/src/booking-calendar-feed-route-generator.ts +293 -0
- package/src/data-api-route-generator.ts +6 -1
- package/src/executor-generator.ts +39 -28
- package/src/graph-utils.ts +143 -11
- package/src/index.ts +1 -0
- package/src/invoice/api-routes-code.ts +166 -21
- package/src/invoice/data-access-code.ts +139 -21
- package/src/invoice/email-sender-code.ts +15 -25
- package/src/invoice/index.ts +1 -1
- package/src/invoice/invoice-assembly-code.ts +50 -6
- package/src/invoice/invoice-html-code.ts +13 -2
- package/src/nodes/data/data-raw-query.ts +11 -1
- package/src/nodes/email/email-mailgun.ts +11 -2
- package/src/nodes/email/email-postmark.ts +12 -2
- package/src/nodes/email/email-resend.ts +12 -2
- package/src/nodes/email/email-sendgrid.ts +15 -2
- package/src/nodes/general/general-custom-js.ts +10 -0
- package/src/nodes/storage/storage-local-remove.ts +4 -0
- package/src/nodes/storage/storage-local-set.ts +6 -0
- package/src/page-arrival.ts +115 -0
- package/src/payments/index.ts +18 -5
- package/src/segment-splitter.ts +90 -36
- package/src/transactional-email-code.ts +32 -21
- package/src/workflow-component-plugin.ts +43 -188
- package/src/workflow-project-plugin.ts +17 -4
- package/src/workflow-scope.ts +184 -0
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
/* tslint:disable:function-constructor */
|
|
2
|
+
import { generateInvoiceGenerateRouteCode } from '../src/invoice/api-routes-code'
|
|
3
|
+
import { generatePdfGeneratorCode } from '../src/invoice/pdf-generator-code'
|
|
4
|
+
import type { UIDLInvoiceSettings } from '@teleporthq/teleport-types'
|
|
5
|
+
import {
|
|
6
|
+
INVOICE_TEST_APP_SECRET,
|
|
7
|
+
createInvoiceRouteRequire,
|
|
8
|
+
evaluateEmittedModule,
|
|
9
|
+
serverCallHeaders,
|
|
10
|
+
} from './_helpers/load-invoice-route'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The buyer's own invoice details on the invoice. An order whose buyer gave
|
|
14
|
+
* them (a booking's "Add invoice details") carries the address in its parts
|
|
15
|
+
* and the tax or VAT number in its metadata: the invoice is made out to them
|
|
16
|
+
* — never mixed with a shipping address — and names the VAT number. A booking
|
|
17
|
+
* order also records the tax its final price includes, so its invoice extracts
|
|
18
|
+
* it instead of adding the store's rate on top. And the template's sample
|
|
19
|
+
* customer never fills a part the buyer left out. Every piece below is the
|
|
20
|
+
* EMITTED code, run.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const settingsWith = (overrides: Record<string, unknown>) =>
|
|
24
|
+
({
|
|
25
|
+
invoicePrefix: 'INV-',
|
|
26
|
+
defaultTaxRate: 19,
|
|
27
|
+
showDiscount: false,
|
|
28
|
+
taxIncludedInPrice: false,
|
|
29
|
+
companyDetails: {},
|
|
30
|
+
template: { document: null as unknown },
|
|
31
|
+
tables: { invoicesTable: 'teleport_invoices', invoiceItemsTable: 'teleport_invoice_items' },
|
|
32
|
+
emailDelivery: { enabled: false },
|
|
33
|
+
...overrides,
|
|
34
|
+
} as unknown as UIDLInvoiceSettings)
|
|
35
|
+
|
|
36
|
+
type Row = Record<string, unknown>
|
|
37
|
+
|
|
38
|
+
const BOOKING_ORDER: Row = {
|
|
39
|
+
id: 'order-bk',
|
|
40
|
+
order_number: 'ORD-1',
|
|
41
|
+
currency: 'EUR',
|
|
42
|
+
total_amount: 238,
|
|
43
|
+
shipping_amount: 0,
|
|
44
|
+
billing_name: 'Acme SRL',
|
|
45
|
+
billing_email: 'ana@example.com',
|
|
46
|
+
billing_address: 'Str. Lunga 1, 400000 Cluj, Cluj, Romania',
|
|
47
|
+
shipping_city: 'Shipped-to city',
|
|
48
|
+
tax_breakdown: JSON.stringify({
|
|
49
|
+
taxRate: 19,
|
|
50
|
+
taxIncluded: true,
|
|
51
|
+
taxDecimals: 2,
|
|
52
|
+
shippingTax: 0,
|
|
53
|
+
lines: [{ productId: 'room', variantId: '', taxRate: 19, taxIncluded: true }],
|
|
54
|
+
}),
|
|
55
|
+
metadata: JSON.stringify({
|
|
56
|
+
bookingId: 'b1',
|
|
57
|
+
bookingReference: 'BK-1',
|
|
58
|
+
billingTaxId: 'RO12345678',
|
|
59
|
+
billingStreet: 'Str. Lunga 1',
|
|
60
|
+
billingCity: 'Cluj',
|
|
61
|
+
billingPostalCode: '400000',
|
|
62
|
+
billingRegion: 'Cluj',
|
|
63
|
+
billingCountry: 'Romania',
|
|
64
|
+
}),
|
|
65
|
+
}
|
|
66
|
+
const BOOKING_ITEMS: Row[] = [
|
|
67
|
+
{
|
|
68
|
+
product_id: 'room',
|
|
69
|
+
product_name: 'Double room',
|
|
70
|
+
quantity: 1,
|
|
71
|
+
unit_price: 238,
|
|
72
|
+
total_price: 238,
|
|
73
|
+
},
|
|
74
|
+
]
|
|
75
|
+
|
|
76
|
+
async function runHandler(
|
|
77
|
+
settings: UIDLInvoiceSettings,
|
|
78
|
+
order: Row,
|
|
79
|
+
items: Row[],
|
|
80
|
+
body: Row
|
|
81
|
+
): Promise<{ status: number; invoice: Row }> {
|
|
82
|
+
const code = generateInvoiceGenerateRouteCode(settings)
|
|
83
|
+
let invoice: Row = {}
|
|
84
|
+
const handler = evaluateEmittedModule<(req: unknown, res: unknown) => Promise<void>>(
|
|
85
|
+
code,
|
|
86
|
+
createInvoiceRouteRequire(settings, {
|
|
87
|
+
dataAccess: {
|
|
88
|
+
getOrderWithItems: async (orderId: string) =>
|
|
89
|
+
orderId === order.id ? { order, items } : null,
|
|
90
|
+
reserveInvoice: async (invoiceData: Row) => {
|
|
91
|
+
invoice = invoiceData
|
|
92
|
+
return { invoice: invoiceData, created: true }
|
|
93
|
+
},
|
|
94
|
+
storeInvoicePdf: async () => ({ firstPdf: true }),
|
|
95
|
+
updateInvoice: async () => ({}),
|
|
96
|
+
},
|
|
97
|
+
pdfGenerator: {
|
|
98
|
+
generateInvoicePdf: async () => Buffer.from('%PDF-1.4 fake'),
|
|
99
|
+
COMPANY_DETAILS: {},
|
|
100
|
+
},
|
|
101
|
+
})
|
|
102
|
+
)
|
|
103
|
+
let status = 0
|
|
104
|
+
const res = {
|
|
105
|
+
status(statusCode: number) {
|
|
106
|
+
status = statusCode
|
|
107
|
+
return this
|
|
108
|
+
},
|
|
109
|
+
json() {
|
|
110
|
+
return this
|
|
111
|
+
},
|
|
112
|
+
setHeader() {
|
|
113
|
+
/* unused */
|
|
114
|
+
},
|
|
115
|
+
end() {
|
|
116
|
+
/* unused */
|
|
117
|
+
},
|
|
118
|
+
}
|
|
119
|
+
await handler({ method: 'POST', headers: serverCallHeaders(), body }, res)
|
|
120
|
+
return { status, invoice }
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
interface PdfGeneratorModule {
|
|
124
|
+
buildInvoiceDataScope: (invoiceData: Row) => { invoiceData: Record<string, Row> }
|
|
125
|
+
buildInvoiceHtml: (invoiceData: Row) => string
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function loadPdfGenerator(
|
|
129
|
+
settings: UIDLInvoiceSettings,
|
|
130
|
+
templateUidl: unknown
|
|
131
|
+
): PdfGeneratorModule {
|
|
132
|
+
const code = generatePdfGeneratorCode(settings, templateUidl, {})
|
|
133
|
+
const moduleObject = { exports: {} as Record<string, unknown> }
|
|
134
|
+
new Function('require', 'module', 'exports', `${code}; return module.exports;`)(
|
|
135
|
+
require,
|
|
136
|
+
moduleObject,
|
|
137
|
+
moduleObject.exports
|
|
138
|
+
)
|
|
139
|
+
return moduleObject.exports as unknown as PdfGeneratorModule
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
describe('/api/invoices/generate — the buyer’s own invoice details', () => {
|
|
143
|
+
const originalFetch = globalThis.fetch
|
|
144
|
+
const originalConnString = process.env.TELEPORT_DB_CONNECTION_STRING
|
|
145
|
+
const originalDatabaseUrl = process.env.DATABASE_URL
|
|
146
|
+
const originalAppSecret = process.env.NEXTAUTH_SECRET
|
|
147
|
+
|
|
148
|
+
beforeAll(() => {
|
|
149
|
+
globalThis.fetch = (async () => {
|
|
150
|
+
throw new Error('runtime storage not configured in tests')
|
|
151
|
+
}) as unknown as typeof fetch
|
|
152
|
+
delete process.env.TELEPORT_DB_CONNECTION_STRING
|
|
153
|
+
delete process.env.DATABASE_URL
|
|
154
|
+
process.env.NEXTAUTH_SECRET = INVOICE_TEST_APP_SECRET
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
afterAll(() => {
|
|
158
|
+
globalThis.fetch = originalFetch
|
|
159
|
+
if (originalAppSecret === undefined) {
|
|
160
|
+
delete process.env.NEXTAUTH_SECRET
|
|
161
|
+
} else {
|
|
162
|
+
process.env.NEXTAUTH_SECRET = originalAppSecret
|
|
163
|
+
}
|
|
164
|
+
if (originalConnString !== undefined) {
|
|
165
|
+
process.env.TELEPORT_DB_CONNECTION_STRING = originalConnString
|
|
166
|
+
}
|
|
167
|
+
if (originalDatabaseUrl !== undefined) {
|
|
168
|
+
process.env.DATABASE_URL = originalDatabaseUrl
|
|
169
|
+
}
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
it('makes the invoice out to the address parts and VAT number the order carries', async () => {
|
|
173
|
+
const run = await runHandler(settingsWith({}), BOOKING_ORDER, BOOKING_ITEMS, {
|
|
174
|
+
orderId: 'order-bk',
|
|
175
|
+
})
|
|
176
|
+
expect(run.status).toBe(200)
|
|
177
|
+
expect(run.invoice).toMatchObject({
|
|
178
|
+
customerName: 'Acme SRL',
|
|
179
|
+
customerAddress: 'Str. Lunga 1',
|
|
180
|
+
customerCity: 'Cluj',
|
|
181
|
+
customerZip: '400000',
|
|
182
|
+
customerState: 'Cluj',
|
|
183
|
+
customerCountry: 'Romania',
|
|
184
|
+
customerVat: 'RO12345678',
|
|
185
|
+
})
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
it('extracts the tax a booking price includes, so the invoice totals what was charged', async () => {
|
|
189
|
+
// The store adds 19% on top of its products; the booking's price already holds it.
|
|
190
|
+
const run = await runHandler(settingsWith({}), BOOKING_ORDER, BOOKING_ITEMS, {
|
|
191
|
+
orderId: 'order-bk',
|
|
192
|
+
})
|
|
193
|
+
expect(run.invoice.total).toBe(238)
|
|
194
|
+
expect(run.invoice.taxAmount).toBeCloseTo(38, 2)
|
|
195
|
+
expect(run.invoice.subtotal).toBeCloseTo(200, 2)
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
it('keeps the order’s own billing and shipping address when it carries no parts', async () => {
|
|
199
|
+
const order = {
|
|
200
|
+
...BOOKING_ORDER,
|
|
201
|
+
tax_breakdown: null,
|
|
202
|
+
billing_address: 'Main St 5',
|
|
203
|
+
shipping_city: 'Springfield',
|
|
204
|
+
shipping_country: 'United States',
|
|
205
|
+
metadata: JSON.stringify({ billingTaxId: 'US-99' }),
|
|
206
|
+
}
|
|
207
|
+
const run = await runHandler(settingsWith({ defaultTaxRate: 0 }), order, BOOKING_ITEMS, {
|
|
208
|
+
orderId: 'order-bk',
|
|
209
|
+
})
|
|
210
|
+
expect(run.invoice).toMatchObject({
|
|
211
|
+
customerAddress: 'Main St 5',
|
|
212
|
+
customerCity: 'Springfield',
|
|
213
|
+
customerCountry: 'United States',
|
|
214
|
+
customerVat: 'US-99',
|
|
215
|
+
})
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
it('bills a store order shipped elsewhere at its billing address, under its company', async () => {
|
|
219
|
+
// The store checkout's "Billing Details", as its order records them; the
|
|
220
|
+
// goods go to someone else, in another town.
|
|
221
|
+
const storeOrder: Row = {
|
|
222
|
+
id: 'order-store',
|
|
223
|
+
order_number: 'ORD-7',
|
|
224
|
+
currency: 'USD',
|
|
225
|
+
total_amount: 40,
|
|
226
|
+
shipping_amount: 0,
|
|
227
|
+
billing_name: 'Ana Billing',
|
|
228
|
+
billing_email: 'ana@example.test',
|
|
229
|
+
billing_address: '1 Billing Street, Suite 4',
|
|
230
|
+
shipping_name: 'Ben Recipient',
|
|
231
|
+
shipping_address: '12 Delivery Lane',
|
|
232
|
+
shipping_city: 'Lyon',
|
|
233
|
+
shipping_state: 'Rhone',
|
|
234
|
+
shipping_zip: '69001',
|
|
235
|
+
shipping_country: 'FR',
|
|
236
|
+
metadata: JSON.stringify({
|
|
237
|
+
billingStreet: '1 Billing Street, Suite 4',
|
|
238
|
+
billingCompany: 'Acme Widgets Ltd',
|
|
239
|
+
billingCity: 'Halifax',
|
|
240
|
+
billingRegion: 'NS',
|
|
241
|
+
billingPostalCode: 'B3H 1A1',
|
|
242
|
+
billingCountry: 'CA',
|
|
243
|
+
}),
|
|
244
|
+
}
|
|
245
|
+
const items = [{ product_id: 'mug', product_name: 'Mug', quantity: 2, unit_price: 20 }]
|
|
246
|
+
const run = await runHandler(settingsWith({ defaultTaxRate: 0 }), storeOrder, items, {
|
|
247
|
+
orderId: 'order-store',
|
|
248
|
+
})
|
|
249
|
+
expect(run.status).toBe(200)
|
|
250
|
+
expect(run.invoice).toMatchObject({
|
|
251
|
+
customerName: 'Ana Billing',
|
|
252
|
+
customerCompany: 'Acme Widgets Ltd',
|
|
253
|
+
customerAddress: '1 Billing Street, Suite 4',
|
|
254
|
+
customerCity: 'Halifax',
|
|
255
|
+
customerState: 'NS',
|
|
256
|
+
customerZip: 'B3H 1A1',
|
|
257
|
+
customerCountry: 'CA',
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
const withoutCompany = await runHandler(
|
|
261
|
+
settingsWith({ defaultTaxRate: 0 }),
|
|
262
|
+
{ ...storeOrder, metadata: JSON.stringify({ billingStreet: '1 Billing Street' }) },
|
|
263
|
+
items,
|
|
264
|
+
{ orderId: 'order-store' }
|
|
265
|
+
)
|
|
266
|
+
expect(withoutCompany.invoice.customerCompany).toBe('')
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
it('reads no metadata that is not a JSON object, and lets the caller’s VAT win', async () => {
|
|
270
|
+
for (const metadata of ['not json', '[1,2]', null]) {
|
|
271
|
+
const run = await runHandler(
|
|
272
|
+
settingsWith({ defaultTaxRate: 0 }),
|
|
273
|
+
{ ...BOOKING_ORDER, metadata, tax_breakdown: null },
|
|
274
|
+
BOOKING_ITEMS,
|
|
275
|
+
{ orderId: 'order-bk' }
|
|
276
|
+
)
|
|
277
|
+
expect([metadata, run.status]).toEqual([metadata, 200])
|
|
278
|
+
expect([metadata, run.invoice.customerVat, run.invoice.customerCity]).toEqual([
|
|
279
|
+
metadata,
|
|
280
|
+
'',
|
|
281
|
+
'Shipped-to city',
|
|
282
|
+
])
|
|
283
|
+
}
|
|
284
|
+
const stated = await runHandler(settingsWith({}), BOOKING_ORDER, BOOKING_ITEMS, {
|
|
285
|
+
orderId: 'order-bk',
|
|
286
|
+
customerVat: 'EDITED-1',
|
|
287
|
+
})
|
|
288
|
+
expect(stated.invoice.customerVat).toBe('EDITED-1')
|
|
289
|
+
})
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
describe('invoice PDF renderer — the buyer, never the template’s sample', () => {
|
|
293
|
+
const settings = settingsWith({})
|
|
294
|
+
const templated = loadPdfGenerator(settings, {
|
|
295
|
+
name: 'invoice-template',
|
|
296
|
+
node: {
|
|
297
|
+
type: 'element',
|
|
298
|
+
content: {
|
|
299
|
+
elementType: 'context',
|
|
300
|
+
attrs: {
|
|
301
|
+
Customer: {
|
|
302
|
+
type: 'static',
|
|
303
|
+
content: { name: 'Jane Smith', city: 'San Francisco', state: 'CA', vat: 'US1' },
|
|
304
|
+
},
|
|
305
|
+
Payment: { type: 'static', content: { method: 'Card', intentId: 'pi_sample' } },
|
|
306
|
+
},
|
|
307
|
+
children: [],
|
|
308
|
+
},
|
|
309
|
+
},
|
|
310
|
+
})
|
|
311
|
+
|
|
312
|
+
it('leaves out what the buyer did not give, and flags the lines that show', () => {
|
|
313
|
+
const { invoiceData } = templated.buildInvoiceDataScope({
|
|
314
|
+
customerName: 'Acme SRL',
|
|
315
|
+
customerVat: 'RO12345678',
|
|
316
|
+
currency: 'EUR',
|
|
317
|
+
items: [],
|
|
318
|
+
})
|
|
319
|
+
expect(invoiceData.Customer).toMatchObject({
|
|
320
|
+
name: 'Acme SRL',
|
|
321
|
+
city: '',
|
|
322
|
+
state: '',
|
|
323
|
+
vat: 'RO12345678',
|
|
324
|
+
hasVat: 'true',
|
|
325
|
+
hasZip: 'false',
|
|
326
|
+
hasCountry: 'false',
|
|
327
|
+
})
|
|
328
|
+
expect(invoiceData.Payment.intentId).toBe('')
|
|
329
|
+
})
|
|
330
|
+
|
|
331
|
+
it('prints the company under the name only for a buyer who gave one', () => {
|
|
332
|
+
const withCompany = templated.buildInvoiceDataScope({
|
|
333
|
+
customerName: 'Ana Billing',
|
|
334
|
+
customerCompany: 'Acme Widgets Ltd',
|
|
335
|
+
items: [],
|
|
336
|
+
}).invoiceData.Customer
|
|
337
|
+
expect(withCompany).toMatchObject({ company: 'Acme Widgets Ltd', hasCompany: 'true' })
|
|
338
|
+
const withoutCompany = templated.buildInvoiceDataScope({ customerName: 'Ana', items: [] })
|
|
339
|
+
.invoiceData.Customer
|
|
340
|
+
expect(withoutCompany).toMatchObject({ company: '', hasCompany: 'false' })
|
|
341
|
+
|
|
342
|
+
// The template's company line, gated on its flag like the editor builds it.
|
|
343
|
+
const text = (children: unknown[]) => ({
|
|
344
|
+
type: 'element',
|
|
345
|
+
content: { elementType: 'text', semanticType: 'p', children },
|
|
346
|
+
})
|
|
347
|
+
const billTo = loadPdfGenerator(settings, {
|
|
348
|
+
name: 'invoice-template',
|
|
349
|
+
node: {
|
|
350
|
+
type: 'element',
|
|
351
|
+
content: {
|
|
352
|
+
elementType: 'container',
|
|
353
|
+
children: [
|
|
354
|
+
text([{ type: 'expr', content: 'invoiceData?.Customer?.name' }]),
|
|
355
|
+
{
|
|
356
|
+
type: 'conditional',
|
|
357
|
+
content: {
|
|
358
|
+
reference: { type: 'expr', content: 'invoiceData?.Customer?.hasCompany' },
|
|
359
|
+
condition: { conditions: [{ operation: '=', operand: 'true' }] },
|
|
360
|
+
node: text([{ type: 'expr', content: 'invoiceData?.Customer?.company' }]),
|
|
361
|
+
},
|
|
362
|
+
},
|
|
363
|
+
],
|
|
364
|
+
},
|
|
365
|
+
},
|
|
366
|
+
})
|
|
367
|
+
expect(
|
|
368
|
+
billTo.buildInvoiceHtml({
|
|
369
|
+
customerName: 'Ana',
|
|
370
|
+
customerCompany: 'Acme Widgets Ltd',
|
|
371
|
+
items: [],
|
|
372
|
+
})
|
|
373
|
+
).toContain('Acme Widgets Ltd')
|
|
374
|
+
expect(billTo.buildInvoiceHtml({ customerName: 'Ana', items: [] })).not.toContain('Acme')
|
|
375
|
+
|
|
376
|
+
// And the built-in layout used when a store has no template.
|
|
377
|
+
const fallback = loadPdfGenerator(settings, null).buildInvoiceHtml({
|
|
378
|
+
customerName: 'Ana Billing',
|
|
379
|
+
customerCompany: 'Acme Widgets Ltd',
|
|
380
|
+
items: [],
|
|
381
|
+
})
|
|
382
|
+
expect(fallback).toContain('<strong>Ana Billing</strong><br/>Acme Widgets Ltd<br/>')
|
|
383
|
+
})
|
|
384
|
+
})
|
|
@@ -21,6 +21,8 @@ interface DataAccess {
|
|
|
21
21
|
invoiceNumber: string,
|
|
22
22
|
pdfUrl: string
|
|
23
23
|
) => Promise<boolean>
|
|
24
|
+
recordOrderEvent: (event: Row) => Promise<void>
|
|
25
|
+
storeInvoicePdf: (id: string, pdf: Buffer, pdfUrl: string) => Promise<Row>
|
|
24
26
|
}
|
|
25
27
|
|
|
26
28
|
interface Statement {
|
|
@@ -153,6 +155,96 @@ describe('invoice data-access — replaceInvoice', () => {
|
|
|
153
155
|
)
|
|
154
156
|
expect(released()).toBe(2)
|
|
155
157
|
})
|
|
158
|
+
|
|
159
|
+
it('tells the regeneration that gave the invoice its first PDF, by writing only a row without one', async () => {
|
|
160
|
+
const { dataAccess, statements } = loadPostgres()
|
|
161
|
+
expect(await dataAccess.replaceInvoice('inv-1', INVOICE_DATA, ITEMS)).toEqual({
|
|
162
|
+
firstPdf: true,
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
const updates = statements.filter((statement) => statement.sql.startsWith('UPDATE'))
|
|
166
|
+
expect(updates).toHaveLength(1)
|
|
167
|
+
expect(updates[0].sql).toMatch(/ WHERE id = \$\d+ AND pdf_size_bytes IS NULL$/)
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
it('rewrites an invoice that already had a PDF all the same, and says it was not the first', async () => {
|
|
171
|
+
const { dataAccess, statements } = loadPostgres((sql) =>
|
|
172
|
+
sql.endsWith('AND pdf_size_bytes IS NULL') ? { rowCount: 0 } : { rowCount: 1 }
|
|
173
|
+
)
|
|
174
|
+
expect(await dataAccess.replaceInvoice('inv-1', INVOICE_DATA, ITEMS)).toEqual({
|
|
175
|
+
firstPdf: false,
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
const updates = statements.filter((statement) => statement.sql.startsWith('UPDATE'))
|
|
179
|
+
expect(updates).toHaveLength(2)
|
|
180
|
+
expect(updates[1].sql).toMatch(/ WHERE id = \$\d+$/)
|
|
181
|
+
expect(updates[1].values).toEqual(updates[0].values)
|
|
182
|
+
expect(statements.map((statement) => statement.sql)).toContain('COMMIT')
|
|
183
|
+
})
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
/** The emitted MySQL data access over a mysql2 stub; `affected` answers each UPDATE. */
|
|
187
|
+
function loadMysql(affected: (sql: string) => number) {
|
|
188
|
+
const statements: string[] = []
|
|
189
|
+
const connection = {
|
|
190
|
+
beginTransaction: async () => statements.push('BEGIN'),
|
|
191
|
+
commit: async () => statements.push('COMMIT'),
|
|
192
|
+
rollback: async () => statements.push('ROLLBACK'),
|
|
193
|
+
release: (): void => undefined,
|
|
194
|
+
execute: async (sql: string) => {
|
|
195
|
+
statements.push(sql)
|
|
196
|
+
return [{ affectedRows: sql.startsWith('UPDATE') ? affected(sql) : 1, insertId: 1 }]
|
|
197
|
+
},
|
|
198
|
+
}
|
|
199
|
+
const mysqlStub = {
|
|
200
|
+
createPool: () => ({ getConnection: async () => connection, execute: connection.execute }),
|
|
201
|
+
}
|
|
202
|
+
const code = `${generateDataAccessCode(SETTINGS, 'mysql', null)}\n${getRecordMappingCode(
|
|
203
|
+
'mysql'
|
|
204
|
+
)}`
|
|
205
|
+
const moduleObject = { exports: {} as unknown }
|
|
206
|
+
const dataAccess = new Function(
|
|
207
|
+
'require',
|
|
208
|
+
'module',
|
|
209
|
+
'exports',
|
|
210
|
+
`${code}; return module.exports;`
|
|
211
|
+
)(
|
|
212
|
+
(id: string) => (id === 'mysql2/promise' ? mysqlStub : require(id)),
|
|
213
|
+
moduleObject,
|
|
214
|
+
moduleObject.exports
|
|
215
|
+
) as DataAccess
|
|
216
|
+
return { dataAccess, statements }
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
describe('invoice data-access — replaceInvoice on MySQL', () => {
|
|
220
|
+
it('tells the regeneration that gave the invoice its first PDF', async () => {
|
|
221
|
+
const { dataAccess, statements } = loadMysql(() => 1)
|
|
222
|
+
expect(await dataAccess.replaceInvoice('inv-1', INVOICE_DATA, ITEMS)).toEqual({
|
|
223
|
+
firstPdf: true,
|
|
224
|
+
})
|
|
225
|
+
const updates = statements.filter((sql) => sql.startsWith('UPDATE'))
|
|
226
|
+
expect(updates).toHaveLength(1)
|
|
227
|
+
expect(updates[0]).toMatch(/ WHERE id = \? AND pdf_size_bytes IS NULL$/)
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
it('rewrites an invoice that already had a PDF, and says it was not the first', async () => {
|
|
231
|
+
const { dataAccess, statements } = loadMysql((sql) =>
|
|
232
|
+
sql.endsWith('AND pdf_size_bytes IS NULL') ? 0 : 1
|
|
233
|
+
)
|
|
234
|
+
expect(await dataAccess.replaceInvoice('inv-1', INVOICE_DATA, ITEMS)).toEqual({
|
|
235
|
+
firstPdf: false,
|
|
236
|
+
})
|
|
237
|
+
expect(statements.filter((sql) => sql.startsWith('UPDATE'))).toHaveLength(2)
|
|
238
|
+
expect(statements).toContain('COMMIT')
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
it('reports an invoice that disappeared', async () => {
|
|
242
|
+
const { dataAccess, statements } = loadMysql(() => 0)
|
|
243
|
+
await expect(dataAccess.replaceInvoice('inv-1', INVOICE_DATA, ITEMS)).rejects.toThrow(
|
|
244
|
+
'Invoice inv-1 no longer exists'
|
|
245
|
+
)
|
|
246
|
+
expect(statements).toContain('ROLLBACK')
|
|
247
|
+
})
|
|
156
248
|
})
|
|
157
249
|
|
|
158
250
|
type SupabaseResult = { data: unknown; error: { message: string } | null }
|
|
@@ -162,9 +254,14 @@ type SupabaseResult = { data: unknown; error: { message: string } | null }
|
|
|
162
254
|
* `failOn(op, table, nth)` fails the nth (1-based) call of that operation on
|
|
163
255
|
* that table.
|
|
164
256
|
*/
|
|
165
|
-
function loadSupabase(
|
|
257
|
+
function loadSupabase(
|
|
258
|
+
failOn: (op: string, table: string, nth: number) => boolean = () => false,
|
|
259
|
+
storedInvoice: Row = {}
|
|
260
|
+
) {
|
|
166
261
|
const store: Record<string, Row[]> = {
|
|
167
|
-
teleport_invoices: [
|
|
262
|
+
teleport_invoices: [
|
|
263
|
+
{ id: 'inv-1', invoice_number: 'INV-0004', total: 90, notes: 'old', ...storedInvoice },
|
|
264
|
+
],
|
|
168
265
|
teleport_invoice_items: [{ id: 'old-1', invoice_id: 'inv-1', name: 'Old lamp' }],
|
|
169
266
|
}
|
|
170
267
|
const counts: Record<string, number> = {}
|
|
@@ -225,6 +322,11 @@ function loadSupabase(failOn: (op: string, table: string, nth: number) => boolea
|
|
|
225
322
|
filters.push((row) => values.includes(row[column]))
|
|
226
323
|
return builder
|
|
227
324
|
},
|
|
325
|
+
// `is(column, null)`: SQL NULL, which an absent column also reads as.
|
|
326
|
+
is: (column: string, value: unknown) => {
|
|
327
|
+
filters.push((row) => (value === null ? row[column] == null : row[column] === value))
|
|
328
|
+
return builder
|
|
329
|
+
},
|
|
228
330
|
maybeSingle: () => {
|
|
229
331
|
single = true
|
|
230
332
|
return builder
|
|
@@ -294,6 +396,35 @@ describe('invoice data-access — replaceInvoice on Supabase (no transactions)',
|
|
|
294
396
|
)
|
|
295
397
|
expect(lineNames()).toEqual(['Old lamp'])
|
|
296
398
|
})
|
|
399
|
+
|
|
400
|
+
it('tells the regeneration that gave the invoice its first PDF', async () => {
|
|
401
|
+
const { dataAccess, invoice } = loadSupabase()
|
|
402
|
+
expect(
|
|
403
|
+
await dataAccess.replaceInvoice('inv-1', { ...INVOICE_DATA, pdfSizeBytes: 2048 }, ITEMS)
|
|
404
|
+
).toEqual({ firstPdf: true })
|
|
405
|
+
expect(invoice()).toMatchObject({ total: 105, pdf_size_bytes: 2048 })
|
|
406
|
+
})
|
|
407
|
+
|
|
408
|
+
it('rewrites an invoice that already had a PDF, and says it was not the first', async () => {
|
|
409
|
+
const { dataAccess, invoice } = loadSupabase(() => false, { pdf_size_bytes: 1024 })
|
|
410
|
+
expect(
|
|
411
|
+
await dataAccess.replaceInvoice('inv-1', { ...INVOICE_DATA, pdfSizeBytes: 2048 }, ITEMS)
|
|
412
|
+
).toEqual({ firstPdf: false })
|
|
413
|
+
expect(invoice()).toMatchObject({ total: 105, pdf_size_bytes: 2048 })
|
|
414
|
+
})
|
|
415
|
+
|
|
416
|
+
it('puts the invoice back without its PDF when a later step fails, so the next regeneration is the first', async () => {
|
|
417
|
+
const { dataAccess, invoice } = loadSupabase(
|
|
418
|
+
(op, table, nth) => op === 'delete' && table === 'teleport_invoice_items' && nth === 1
|
|
419
|
+
)
|
|
420
|
+
await expect(
|
|
421
|
+
dataAccess.replaceInvoice('inv-1', { ...INVOICE_DATA, pdfSizeBytes: 2048 }, ITEMS)
|
|
422
|
+
).rejects.toThrow('delete teleport_invoice_items failed')
|
|
423
|
+
expect(invoice().pdf_size_bytes == null).toBe(true)
|
|
424
|
+
expect(
|
|
425
|
+
await dataAccess.replaceInvoice('inv-1', { ...INVOICE_DATA, pdfSizeBytes: 2048 }, ITEMS)
|
|
426
|
+
).toEqual({ firstPdf: true })
|
|
427
|
+
})
|
|
297
428
|
})
|
|
298
429
|
|
|
299
430
|
describe('invoice data-access — linkOrderToInvoice', () => {
|
|
@@ -327,6 +458,46 @@ describe('invoice data-access — linkOrderToInvoice', () => {
|
|
|
327
458
|
})
|
|
328
459
|
})
|
|
329
460
|
|
|
461
|
+
describe('invoice data-access — recordOrderEvent', () => {
|
|
462
|
+
it("writes the order's history line once per key", async () => {
|
|
463
|
+
const { dataAccess, statements } = loadPostgres()
|
|
464
|
+
await dataAccess.recordOrderEvent({
|
|
465
|
+
type: 'order-email-sent',
|
|
466
|
+
dedupeKey: 'order-email-sent:inv-1:invoice-email',
|
|
467
|
+
orderId: 'order-1',
|
|
468
|
+
payload: { purpose: 'invoice-email', to: 'ana@buyer.test' },
|
|
469
|
+
actor: 'Store admin',
|
|
470
|
+
actorType: 'merchant',
|
|
471
|
+
})
|
|
472
|
+
|
|
473
|
+
const [statement] = statements
|
|
474
|
+
expect(statement.sql).toBe(
|
|
475
|
+
'INSERT INTO teleport_events (id, event_type, dedupe_key, order_id, payload, actor, actor_type, status) ' +
|
|
476
|
+
'VALUES ($1, $2, $3, $4, $5, $6, $7, $8) ON CONFLICT (dedupe_key) DO NOTHING'
|
|
477
|
+
)
|
|
478
|
+
expect(statement.values.slice(1)).toEqual([
|
|
479
|
+
'order-email-sent',
|
|
480
|
+
'order-email-sent:inv-1:invoice-email',
|
|
481
|
+
'order-1',
|
|
482
|
+
'{"purpose":"invoice-email","to":"ana@buyer.test"}',
|
|
483
|
+
'Store admin',
|
|
484
|
+
'merchant',
|
|
485
|
+
'recorded',
|
|
486
|
+
])
|
|
487
|
+
})
|
|
488
|
+
|
|
489
|
+
it('keeps the key within the column', async () => {
|
|
490
|
+
const { dataAccess, statements } = loadPostgres()
|
|
491
|
+
await dataAccess.recordOrderEvent({
|
|
492
|
+
type: 'order-email-sent',
|
|
493
|
+
dedupeKey: 'k'.repeat(300),
|
|
494
|
+
actorType: 'merchant',
|
|
495
|
+
})
|
|
496
|
+
expect(String(statements[0].values[2])).toHaveLength(255)
|
|
497
|
+
expect(statements[0].values[3]).toBeNull()
|
|
498
|
+
})
|
|
499
|
+
})
|
|
500
|
+
|
|
330
501
|
describe('invoice data-access — every dialect exports the regeneration helpers', () => {
|
|
331
502
|
it.each(['postgresql', 'mysql', 'supabase'])('%s', (dialect) => {
|
|
332
503
|
const code = generateDataAccessCode(SETTINGS, dialect as never, null)
|
|
@@ -334,7 +505,70 @@ describe('invoice data-access — every dialect exports the regeneration helpers
|
|
|
334
505
|
expect(code).toContain(
|
|
335
506
|
'async function linkOrderToInvoice(orderId, invoiceId, invoiceNumber, pdfUrl)'
|
|
336
507
|
)
|
|
508
|
+
expect(code).toContain('async function recordOrderEvent(event)')
|
|
337
509
|
expect(code).toContain('replaceInvoice: replaceInvoice,')
|
|
338
510
|
expect(code).toContain('linkOrderToInvoice: linkOrderToInvoice,')
|
|
511
|
+
expect(code).toContain('recordOrderEvent: recordOrderEvent,')
|
|
512
|
+
})
|
|
513
|
+
})
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* `storeInvoicePdf` — the payment's PDF, written only on an invoice that has
|
|
517
|
+
* none, so the invoice's first PDF (and with it its email) goes to exactly one
|
|
518
|
+
* of the payment and a regeneration, as `replaceInvoice` claims it.
|
|
519
|
+
*/
|
|
520
|
+
describe('invoice data-access — storeInvoicePdf writes only the first PDF', () => {
|
|
521
|
+
const PDF = Buffer.from('%PDF-1.4 INV-0004')
|
|
522
|
+
|
|
523
|
+
it('Postgres: one statement, on a row without a PDF', async () => {
|
|
524
|
+
const { dataAccess, statements } = loadPostgres()
|
|
525
|
+
expect(await dataAccess.storeInvoicePdf('inv-1', PDF, '/api/invoices/inv-1/pdf')).toEqual({
|
|
526
|
+
firstPdf: true,
|
|
527
|
+
})
|
|
528
|
+
expect(statements).toHaveLength(1)
|
|
529
|
+
expect(statements[0].sql).toBe(
|
|
530
|
+
'UPDATE teleport_invoices SET pdf_data = $1, pdf_size_bytes = $2, pdf_content_type = $3, ' +
|
|
531
|
+
'pdf_url = $4, updated_at = $5 WHERE id = $6 AND pdf_size_bytes IS NULL'
|
|
532
|
+
)
|
|
533
|
+
expect(statements[0].values).toEqual([
|
|
534
|
+
PDF,
|
|
535
|
+
PDF.length,
|
|
536
|
+
'application/pdf',
|
|
537
|
+
'/api/invoices/inv-1/pdf',
|
|
538
|
+
expect.any(String),
|
|
539
|
+
'inv-1',
|
|
540
|
+
])
|
|
541
|
+
})
|
|
542
|
+
|
|
543
|
+
it('Postgres: an invoice that already has a PDF keeps it, and is not the first', async () => {
|
|
544
|
+
const { dataAccess } = loadPostgres(() => ({ rowCount: 0 }))
|
|
545
|
+
expect(await dataAccess.storeInvoicePdf('inv-1', PDF, '/api/invoices/inv-1/pdf')).toEqual({
|
|
546
|
+
firstPdf: false,
|
|
547
|
+
})
|
|
548
|
+
})
|
|
549
|
+
|
|
550
|
+
it('MySQL: the same condition, and the rows it changed', async () => {
|
|
551
|
+
const first = loadMysql(() => 1)
|
|
552
|
+
expect(await first.dataAccess.storeInvoicePdf('inv-1', PDF, '/x')).toEqual({ firstPdf: true })
|
|
553
|
+
expect(first.statements).toEqual([
|
|
554
|
+
'UPDATE teleport_invoices SET pdf_data = ?, pdf_size_bytes = ?, pdf_content_type = ?, ' +
|
|
555
|
+
'pdf_url = ?, updated_at = ? WHERE id = ? AND pdf_size_bytes IS NULL',
|
|
556
|
+
])
|
|
557
|
+
const later = loadMysql(() => 0)
|
|
558
|
+
expect(await later.dataAccess.storeInvoicePdf('inv-1', PDF, '/x')).toEqual({ firstPdf: false })
|
|
559
|
+
})
|
|
560
|
+
|
|
561
|
+
it('Supabase: one filtered update', async () => {
|
|
562
|
+
const first = loadSupabase()
|
|
563
|
+
expect(await first.dataAccess.storeInvoicePdf('inv-1', PDF, '/x')).toEqual({ firstPdf: true })
|
|
564
|
+
expect(first.invoice()).toMatchObject({
|
|
565
|
+
pdf_data: PDF.toString('base64'),
|
|
566
|
+
pdf_size_bytes: PDF.length,
|
|
567
|
+
pdf_url: '/x',
|
|
568
|
+
})
|
|
569
|
+
|
|
570
|
+
const later = loadSupabase(() => false, { pdf_size_bytes: 1024, pdf_url: '/regenerated' })
|
|
571
|
+
expect(await later.dataAccess.storeInvoicePdf('inv-1', PDF, '/x')).toEqual({ firstPdf: false })
|
|
572
|
+
expect(later.invoice()).toMatchObject({ pdf_size_bytes: 1024, pdf_url: '/regenerated' })
|
|
339
573
|
})
|
|
340
574
|
})
|
|
@@ -120,7 +120,7 @@ async function runHandler(options: {
|
|
|
120
120
|
insertedItems.push(...items)
|
|
121
121
|
return { invoice: invoiceData, created: true }
|
|
122
122
|
},
|
|
123
|
-
storeInvoicePdf: async () => ({}),
|
|
123
|
+
storeInvoicePdf: async () => ({ firstPdf: true }),
|
|
124
124
|
updateInvoice: async () => ({}),
|
|
125
125
|
}
|
|
126
126
|
|