@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.
Files changed (248) hide show
  1. package/__tests__/_helpers/fake-store-database.ts +428 -0
  2. package/__tests__/_helpers/invoice-store.ts +382 -0
  3. package/__tests__/booking-calendar-feed-route.test.ts +494 -0
  4. package/__tests__/custom-node-result-taken-branch.test.ts +107 -0
  5. package/__tests__/data-api-date-parser.test.ts +45 -0
  6. package/__tests__/email-locale-senders.test.ts +3 -1
  7. package/__tests__/email-sender-name.test.ts +513 -0
  8. package/__tests__/graph-utils-topological-order.test.ts +155 -1
  9. package/__tests__/invoice-billing-details.test.ts +384 -0
  10. package/__tests__/invoice-data-access-replace.test.ts +236 -2
  11. package/__tests__/invoice-order-items-settle.test.ts +1 -1
  12. package/__tests__/invoice-payment-email.test.ts +191 -0
  13. package/__tests__/invoice-regenerate-email.test.ts +288 -0
  14. package/__tests__/invoice-regenerate-route.test.ts +9 -7
  15. package/__tests__/invoice-reservation.test.ts +1 -1
  16. package/__tests__/invoice-tender.test.ts +1 -1
  17. package/__tests__/middleware-admin-panel-roles.test.ts +127 -0
  18. package/__tests__/page-arrival-remount.test.ts +247 -0
  19. package/__tests__/payment-drivers-module.test.ts +19 -0
  20. package/__tests__/raw-query-error-code.test.ts +47 -0
  21. package/__tests__/runtime-template-token-resolution.test.ts +16 -0
  22. package/__tests__/segment-reply-changed-keys.test.ts +31 -0
  23. package/__tests__/segment-route-skipped-branch.test.ts +101 -0
  24. package/__tests__/segment-splitter-checkout-order.test.ts +306 -0
  25. package/__tests__/streaming-on-end-env-filter.test.ts +18 -18
  26. package/__tests__/streaming-on-end-order.test.ts +387 -0
  27. package/dist/cjs/account-delete-route-generator.d.ts +1 -0
  28. package/dist/cjs/account-delete-route-generator.d.ts.map +1 -1
  29. package/dist/cjs/account-delete-route-generator.js +2 -1
  30. package/dist/cjs/account-delete-route-generator.js.map +1 -1
  31. package/dist/cjs/api-route-generator.d.ts.map +1 -1
  32. package/dist/cjs/api-route-generator.js +23 -13
  33. package/dist/cjs/api-route-generator.js.map +1 -1
  34. package/dist/cjs/auth-generator.d.ts +1 -0
  35. package/dist/cjs/auth-generator.d.ts.map +1 -1
  36. package/dist/cjs/auth-generator.js +77 -28
  37. package/dist/cjs/auth-generator.js.map +1 -1
  38. package/dist/cjs/booking-calendar-feed-route-generator.d.ts +28 -0
  39. package/dist/cjs/booking-calendar-feed-route-generator.d.ts.map +1 -0
  40. package/dist/cjs/booking-calendar-feed-route-generator.js +296 -0
  41. package/dist/cjs/booking-calendar-feed-route-generator.js.map +1 -0
  42. package/dist/cjs/data-api-route-generator.d.ts.map +1 -1
  43. package/dist/cjs/data-api-route-generator.js +6 -1
  44. package/dist/cjs/data-api-route-generator.js.map +1 -1
  45. package/dist/cjs/executor-generator.d.ts.map +1 -1
  46. package/dist/cjs/executor-generator.js +39 -28
  47. package/dist/cjs/executor-generator.js.map +1 -1
  48. package/dist/cjs/graph-utils.d.ts +31 -1
  49. package/dist/cjs/graph-utils.d.ts.map +1 -1
  50. package/dist/cjs/graph-utils.js +112 -10
  51. package/dist/cjs/graph-utils.js.map +1 -1
  52. package/dist/cjs/index.d.ts +1 -1
  53. package/dist/cjs/index.d.ts.map +1 -1
  54. package/dist/cjs/index.js +2 -1
  55. package/dist/cjs/index.js.map +1 -1
  56. package/dist/cjs/invoice/api-routes-code.d.ts +12 -3
  57. package/dist/cjs/invoice/api-routes-code.d.ts.map +1 -1
  58. package/dist/cjs/invoice/api-routes-code.js +152 -21
  59. package/dist/cjs/invoice/api-routes-code.js.map +1 -1
  60. package/dist/cjs/invoice/data-access-code.d.ts.map +1 -1
  61. package/dist/cjs/invoice/data-access-code.js +133 -21
  62. package/dist/cjs/invoice/data-access-code.js.map +1 -1
  63. package/dist/cjs/invoice/email-sender-code.d.ts.map +1 -1
  64. package/dist/cjs/invoice/email-sender-code.js +15 -25
  65. package/dist/cjs/invoice/email-sender-code.js.map +1 -1
  66. package/dist/cjs/invoice/index.js +1 -1
  67. package/dist/cjs/invoice/index.js.map +1 -1
  68. package/dist/cjs/invoice/invoice-assembly-code.d.ts.map +1 -1
  69. package/dist/cjs/invoice/invoice-assembly-code.js +50 -6
  70. package/dist/cjs/invoice/invoice-assembly-code.js.map +1 -1
  71. package/dist/cjs/invoice/invoice-html-code.d.ts.map +1 -1
  72. package/dist/cjs/invoice/invoice-html-code.js +13 -2
  73. package/dist/cjs/invoice/invoice-html-code.js.map +1 -1
  74. package/dist/cjs/nodes/data/data-raw-query.d.ts.map +1 -1
  75. package/dist/cjs/nodes/data/data-raw-query.js +11 -1
  76. package/dist/cjs/nodes/data/data-raw-query.js.map +1 -1
  77. package/dist/cjs/nodes/email/email-mailgun.d.ts.map +1 -1
  78. package/dist/cjs/nodes/email/email-mailgun.js +4 -2
  79. package/dist/cjs/nodes/email/email-mailgun.js.map +1 -1
  80. package/dist/cjs/nodes/email/email-postmark.d.ts.map +1 -1
  81. package/dist/cjs/nodes/email/email-postmark.js +5 -2
  82. package/dist/cjs/nodes/email/email-postmark.js.map +1 -1
  83. package/dist/cjs/nodes/email/email-resend.d.ts.map +1 -1
  84. package/dist/cjs/nodes/email/email-resend.js +5 -2
  85. package/dist/cjs/nodes/email/email-resend.js.map +1 -1
  86. package/dist/cjs/nodes/email/email-sendgrid.d.ts.map +1 -1
  87. package/dist/cjs/nodes/email/email-sendgrid.js +8 -2
  88. package/dist/cjs/nodes/email/email-sendgrid.js.map +1 -1
  89. package/dist/cjs/nodes/general/general-custom-js.d.ts.map +1 -1
  90. package/dist/cjs/nodes/general/general-custom-js.js +11 -0
  91. package/dist/cjs/nodes/general/general-custom-js.js.map +1 -1
  92. package/dist/cjs/nodes/storage/storage-local-remove.d.ts.map +1 -1
  93. package/dist/cjs/nodes/storage/storage-local-remove.js +2 -0
  94. package/dist/cjs/nodes/storage/storage-local-remove.js.map +1 -1
  95. package/dist/cjs/nodes/storage/storage-local-set.d.ts.map +1 -1
  96. package/dist/cjs/nodes/storage/storage-local-set.js +4 -0
  97. package/dist/cjs/nodes/storage/storage-local-set.js.map +1 -1
  98. package/dist/cjs/page-arrival.d.ts +10 -0
  99. package/dist/cjs/page-arrival.d.ts.map +1 -0
  100. package/dist/cjs/page-arrival.js +86 -0
  101. package/dist/cjs/page-arrival.js.map +1 -0
  102. package/dist/cjs/payments/index.d.ts +10 -4
  103. package/dist/cjs/payments/index.d.ts.map +1 -1
  104. package/dist/cjs/payments/index.js +18 -7
  105. package/dist/cjs/payments/index.js.map +1 -1
  106. package/dist/cjs/segment-splitter.d.ts.map +1 -1
  107. package/dist/cjs/segment-splitter.js +65 -27
  108. package/dist/cjs/segment-splitter.js.map +1 -1
  109. package/dist/cjs/transactional-email-code.d.ts.map +1 -1
  110. package/dist/cjs/transactional-email-code.js +32 -18
  111. package/dist/cjs/transactional-email-code.js.map +1 -1
  112. package/dist/cjs/tsconfig.tsbuildinfo +1 -1
  113. package/dist/cjs/workflow-component-plugin.d.ts.map +1 -1
  114. package/dist/cjs/workflow-component-plugin.js +35 -149
  115. package/dist/cjs/workflow-component-plugin.js.map +1 -1
  116. package/dist/cjs/workflow-project-plugin.d.ts.map +1 -1
  117. package/dist/cjs/workflow-project-plugin.js +16 -4
  118. package/dist/cjs/workflow-project-plugin.js.map +1 -1
  119. package/dist/cjs/workflow-scope.d.ts +19 -0
  120. package/dist/cjs/workflow-scope.d.ts.map +1 -0
  121. package/dist/cjs/workflow-scope.js +147 -0
  122. package/dist/cjs/workflow-scope.js.map +1 -0
  123. package/dist/esm/account-delete-route-generator.d.ts +1 -0
  124. package/dist/esm/account-delete-route-generator.d.ts.map +1 -1
  125. package/dist/esm/account-delete-route-generator.js +2 -1
  126. package/dist/esm/account-delete-route-generator.js.map +1 -1
  127. package/dist/esm/api-route-generator.d.ts.map +1 -1
  128. package/dist/esm/api-route-generator.js +23 -13
  129. package/dist/esm/api-route-generator.js.map +1 -1
  130. package/dist/esm/auth-generator.d.ts +1 -0
  131. package/dist/esm/auth-generator.d.ts.map +1 -1
  132. package/dist/esm/auth-generator.js +77 -28
  133. package/dist/esm/auth-generator.js.map +1 -1
  134. package/dist/esm/booking-calendar-feed-route-generator.d.ts +28 -0
  135. package/dist/esm/booking-calendar-feed-route-generator.d.ts.map +1 -0
  136. package/dist/esm/booking-calendar-feed-route-generator.js +291 -0
  137. package/dist/esm/booking-calendar-feed-route-generator.js.map +1 -0
  138. package/dist/esm/data-api-route-generator.d.ts.map +1 -1
  139. package/dist/esm/data-api-route-generator.js +6 -1
  140. package/dist/esm/data-api-route-generator.js.map +1 -1
  141. package/dist/esm/executor-generator.d.ts.map +1 -1
  142. package/dist/esm/executor-generator.js +39 -28
  143. package/dist/esm/executor-generator.js.map +1 -1
  144. package/dist/esm/graph-utils.d.ts +31 -1
  145. package/dist/esm/graph-utils.d.ts.map +1 -1
  146. package/dist/esm/graph-utils.js +109 -9
  147. package/dist/esm/graph-utils.js.map +1 -1
  148. package/dist/esm/index.d.ts +1 -1
  149. package/dist/esm/index.d.ts.map +1 -1
  150. package/dist/esm/index.js +1 -1
  151. package/dist/esm/index.js.map +1 -1
  152. package/dist/esm/invoice/api-routes-code.d.ts +12 -3
  153. package/dist/esm/invoice/api-routes-code.d.ts.map +1 -1
  154. package/dist/esm/invoice/api-routes-code.js +152 -21
  155. package/dist/esm/invoice/api-routes-code.js.map +1 -1
  156. package/dist/esm/invoice/data-access-code.d.ts.map +1 -1
  157. package/dist/esm/invoice/data-access-code.js +133 -21
  158. package/dist/esm/invoice/data-access-code.js.map +1 -1
  159. package/dist/esm/invoice/email-sender-code.d.ts.map +1 -1
  160. package/dist/esm/invoice/email-sender-code.js +15 -25
  161. package/dist/esm/invoice/email-sender-code.js.map +1 -1
  162. package/dist/esm/invoice/index.js +1 -1
  163. package/dist/esm/invoice/index.js.map +1 -1
  164. package/dist/esm/invoice/invoice-assembly-code.d.ts.map +1 -1
  165. package/dist/esm/invoice/invoice-assembly-code.js +50 -6
  166. package/dist/esm/invoice/invoice-assembly-code.js.map +1 -1
  167. package/dist/esm/invoice/invoice-html-code.d.ts.map +1 -1
  168. package/dist/esm/invoice/invoice-html-code.js +13 -2
  169. package/dist/esm/invoice/invoice-html-code.js.map +1 -1
  170. package/dist/esm/nodes/data/data-raw-query.d.ts.map +1 -1
  171. package/dist/esm/nodes/data/data-raw-query.js +11 -1
  172. package/dist/esm/nodes/data/data-raw-query.js.map +1 -1
  173. package/dist/esm/nodes/email/email-mailgun.d.ts.map +1 -1
  174. package/dist/esm/nodes/email/email-mailgun.js +4 -2
  175. package/dist/esm/nodes/email/email-mailgun.js.map +1 -1
  176. package/dist/esm/nodes/email/email-postmark.d.ts.map +1 -1
  177. package/dist/esm/nodes/email/email-postmark.js +5 -2
  178. package/dist/esm/nodes/email/email-postmark.js.map +1 -1
  179. package/dist/esm/nodes/email/email-resend.d.ts.map +1 -1
  180. package/dist/esm/nodes/email/email-resend.js +5 -2
  181. package/dist/esm/nodes/email/email-resend.js.map +1 -1
  182. package/dist/esm/nodes/email/email-sendgrid.d.ts.map +1 -1
  183. package/dist/esm/nodes/email/email-sendgrid.js +8 -2
  184. package/dist/esm/nodes/email/email-sendgrid.js.map +1 -1
  185. package/dist/esm/nodes/general/general-custom-js.d.ts.map +1 -1
  186. package/dist/esm/nodes/general/general-custom-js.js +11 -0
  187. package/dist/esm/nodes/general/general-custom-js.js.map +1 -1
  188. package/dist/esm/nodes/storage/storage-local-remove.d.ts.map +1 -1
  189. package/dist/esm/nodes/storage/storage-local-remove.js +2 -0
  190. package/dist/esm/nodes/storage/storage-local-remove.js.map +1 -1
  191. package/dist/esm/nodes/storage/storage-local-set.d.ts.map +1 -1
  192. package/dist/esm/nodes/storage/storage-local-set.js +4 -0
  193. package/dist/esm/nodes/storage/storage-local-set.js.map +1 -1
  194. package/dist/esm/page-arrival.d.ts +10 -0
  195. package/dist/esm/page-arrival.d.ts.map +1 -0
  196. package/dist/esm/page-arrival.js +81 -0
  197. package/dist/esm/page-arrival.js.map +1 -0
  198. package/dist/esm/payments/index.d.ts +10 -4
  199. package/dist/esm/payments/index.d.ts.map +1 -1
  200. package/dist/esm/payments/index.js +16 -6
  201. package/dist/esm/payments/index.js.map +1 -1
  202. package/dist/esm/segment-splitter.d.ts.map +1 -1
  203. package/dist/esm/segment-splitter.js +66 -28
  204. package/dist/esm/segment-splitter.js.map +1 -1
  205. package/dist/esm/transactional-email-code.d.ts.map +1 -1
  206. package/dist/esm/transactional-email-code.js +32 -18
  207. package/dist/esm/transactional-email-code.js.map +1 -1
  208. package/dist/esm/tsconfig.tsbuildinfo +1 -1
  209. package/dist/esm/workflow-component-plugin.d.ts.map +1 -1
  210. package/dist/esm/workflow-component-plugin.js +32 -146
  211. package/dist/esm/workflow-component-plugin.js.map +1 -1
  212. package/dist/esm/workflow-project-plugin.d.ts.map +1 -1
  213. package/dist/esm/workflow-project-plugin.js +16 -4
  214. package/dist/esm/workflow-project-plugin.js.map +1 -1
  215. package/dist/esm/workflow-scope.d.ts +19 -0
  216. package/dist/esm/workflow-scope.d.ts.map +1 -0
  217. package/dist/esm/workflow-scope.js +142 -0
  218. package/dist/esm/workflow-scope.js.map +1 -0
  219. package/package.json +4 -4
  220. package/src/account-delete-route-generator.ts +3 -1
  221. package/src/api-route-generator.ts +23 -13
  222. package/src/auth-generator.ts +83 -26
  223. package/src/booking-calendar-feed-route-generator.ts +293 -0
  224. package/src/data-api-route-generator.ts +6 -1
  225. package/src/executor-generator.ts +39 -28
  226. package/src/graph-utils.ts +143 -11
  227. package/src/index.ts +1 -0
  228. package/src/invoice/api-routes-code.ts +166 -21
  229. package/src/invoice/data-access-code.ts +139 -21
  230. package/src/invoice/email-sender-code.ts +15 -25
  231. package/src/invoice/index.ts +1 -1
  232. package/src/invoice/invoice-assembly-code.ts +50 -6
  233. package/src/invoice/invoice-html-code.ts +13 -2
  234. package/src/nodes/data/data-raw-query.ts +11 -1
  235. package/src/nodes/email/email-mailgun.ts +11 -2
  236. package/src/nodes/email/email-postmark.ts +12 -2
  237. package/src/nodes/email/email-resend.ts +12 -2
  238. package/src/nodes/email/email-sendgrid.ts +15 -2
  239. package/src/nodes/general/general-custom-js.ts +10 -0
  240. package/src/nodes/storage/storage-local-remove.ts +4 -0
  241. package/src/nodes/storage/storage-local-set.ts +6 -0
  242. package/src/page-arrival.ts +115 -0
  243. package/src/payments/index.ts +18 -5
  244. package/src/segment-splitter.ts +90 -36
  245. package/src/transactional-email-code.ts +32 -21
  246. package/src/workflow-component-plugin.ts +43 -188
  247. package/src/workflow-project-plugin.ts +17 -4
  248. 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(failOn: (op: string, table: string, nth: number) => boolean = () => false) {
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: [{ id: 'inv-1', invoice_number: 'INV-0004', total: 90, notes: 'old' }],
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