@teleporthq/teleport-plugin-next-workflows 0.43.57 → 0.43.59

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 (83) hide show
  1. package/__tests__/ai-chat-rag-sql-parameterization.test.ts +28 -8
  2. package/__tests__/ai-select-database-data-guard.test.ts +218 -0
  3. package/__tests__/ai-select-database-data-handler.test.ts +237 -0
  4. package/__tests__/client-config-redaction.test.ts +34 -2
  5. package/__tests__/invoice-generate-hydration.test.ts +7 -2
  6. package/__tests__/invoice-order-items-settle.test.ts +308 -0
  7. package/__tests__/parameterized-stock-chain.test.ts +453 -0
  8. package/__tests__/stock-decrement-audit.test.ts +5 -1
  9. package/__tests__/streaming-chunk-payload.test.ts +91 -0
  10. package/__tests__/streaming-on-end-env-filter.test.ts +2 -2
  11. package/dist/cjs/api-route-generator.d.ts.map +1 -1
  12. package/dist/cjs/api-route-generator.js +2 -3
  13. package/dist/cjs/api-route-generator.js.map +1 -1
  14. package/dist/cjs/data-api-route-generator.d.ts.map +1 -1
  15. package/dist/cjs/data-api-route-generator.js +4 -0
  16. package/dist/cjs/data-api-route-generator.js.map +1 -1
  17. package/dist/cjs/ecommerce/parameterized-stock-chain.d.ts +23 -0
  18. package/dist/cjs/ecommerce/parameterized-stock-chain.d.ts.map +1 -0
  19. package/dist/cjs/ecommerce/parameterized-stock-chain.js +397 -0
  20. package/dist/cjs/ecommerce/parameterized-stock-chain.js.map +1 -0
  21. package/dist/cjs/ecommerce-customhandler-rewriter.d.ts +3 -0
  22. package/dist/cjs/ecommerce-customhandler-rewriter.d.ts.map +1 -1
  23. package/dist/cjs/ecommerce-customhandler-rewriter.js +41 -1
  24. package/dist/cjs/ecommerce-customhandler-rewriter.js.map +1 -1
  25. package/dist/cjs/executor-generator.js +1 -1
  26. package/dist/cjs/invoice/api-routes-code.d.ts.map +1 -1
  27. package/dist/cjs/invoice/api-routes-code.js +132 -3
  28. package/dist/cjs/invoice/api-routes-code.js.map +1 -1
  29. package/dist/cjs/nodes/ai/ai-provider-utils.js +1 -1
  30. package/dist/cjs/nodes/ai/ai-select-database-data.d.ts +3 -0
  31. package/dist/cjs/nodes/ai/ai-select-database-data.d.ts.map +1 -0
  32. package/dist/cjs/nodes/ai/ai-select-database-data.js +141 -0
  33. package/dist/cjs/nodes/ai/ai-select-database-data.js.map +1 -0
  34. package/dist/cjs/nodes/ai/ai-sql-select-guard.d.ts +18 -0
  35. package/dist/cjs/nodes/ai/ai-sql-select-guard.d.ts.map +1 -0
  36. package/dist/cjs/nodes/ai/ai-sql-select-guard.js +388 -0
  37. package/dist/cjs/nodes/ai/ai-sql-select-guard.js.map +1 -0
  38. package/dist/cjs/nodes/index.d.ts.map +1 -1
  39. package/dist/cjs/nodes/index.js +2 -1
  40. package/dist/cjs/nodes/index.js.map +1 -1
  41. package/dist/cjs/tsconfig.tsbuildinfo +1 -1
  42. package/dist/esm/api-route-generator.d.ts.map +1 -1
  43. package/dist/esm/api-route-generator.js +2 -3
  44. package/dist/esm/api-route-generator.js.map +1 -1
  45. package/dist/esm/data-api-route-generator.d.ts.map +1 -1
  46. package/dist/esm/data-api-route-generator.js +4 -0
  47. package/dist/esm/data-api-route-generator.js.map +1 -1
  48. package/dist/esm/ecommerce/parameterized-stock-chain.d.ts +23 -0
  49. package/dist/esm/ecommerce/parameterized-stock-chain.d.ts.map +1 -0
  50. package/dist/esm/ecommerce/parameterized-stock-chain.js +385 -0
  51. package/dist/esm/ecommerce/parameterized-stock-chain.js.map +1 -0
  52. package/dist/esm/ecommerce-customhandler-rewriter.d.ts +3 -0
  53. package/dist/esm/ecommerce-customhandler-rewriter.d.ts.map +1 -1
  54. package/dist/esm/ecommerce-customhandler-rewriter.js +41 -1
  55. package/dist/esm/ecommerce-customhandler-rewriter.js.map +1 -1
  56. package/dist/esm/executor-generator.js +1 -1
  57. package/dist/esm/invoice/api-routes-code.d.ts.map +1 -1
  58. package/dist/esm/invoice/api-routes-code.js +132 -3
  59. package/dist/esm/invoice/api-routes-code.js.map +1 -1
  60. package/dist/esm/nodes/ai/ai-provider-utils.js +1 -1
  61. package/dist/esm/nodes/ai/ai-select-database-data.d.ts +3 -0
  62. package/dist/esm/nodes/ai/ai-select-database-data.d.ts.map +1 -0
  63. package/dist/esm/nodes/ai/ai-select-database-data.js +138 -0
  64. package/dist/esm/nodes/ai/ai-select-database-data.js.map +1 -0
  65. package/dist/esm/nodes/ai/ai-sql-select-guard.d.ts +18 -0
  66. package/dist/esm/nodes/ai/ai-sql-select-guard.d.ts.map +1 -0
  67. package/dist/esm/nodes/ai/ai-sql-select-guard.js +384 -0
  68. package/dist/esm/nodes/ai/ai-sql-select-guard.js.map +1 -0
  69. package/dist/esm/nodes/index.d.ts.map +1 -1
  70. package/dist/esm/nodes/index.js +2 -1
  71. package/dist/esm/nodes/index.js.map +1 -1
  72. package/dist/esm/tsconfig.tsbuildinfo +1 -1
  73. package/package.json +4 -4
  74. package/src/api-route-generator.ts +2 -3
  75. package/src/data-api-route-generator.ts +4 -0
  76. package/src/ecommerce/parameterized-stock-chain.ts +449 -0
  77. package/src/ecommerce-customhandler-rewriter.ts +71 -3
  78. package/src/executor-generator.ts +1 -1
  79. package/src/invoice/api-routes-code.ts +132 -3
  80. package/src/nodes/ai/ai-provider-utils.ts +1 -1
  81. package/src/nodes/ai/ai-select-database-data.ts +162 -0
  82. package/src/nodes/ai/ai-sql-select-guard.ts +402 -0
  83. package/src/nodes/index.ts +2 -0
@@ -0,0 +1,308 @@
1
+ import { generateInvoiceGenerateRouteCode } from '../src/invoice/api-routes-code'
2
+ import type { UIDLInvoiceSettings } from '@teleporthq/teleport-types'
3
+
4
+ // `/api/invoices/generate` used to invoice a HALF-WRITTEN order.
5
+ //
6
+ // The runtime `data-create-item` handler fire-and-forgets
7
+ // `/api/ecommerce/order-notification` the moment the `teleport_orders` row
8
+ // lands, and that route generates the invoice for every non-webhook payment
9
+ // method. But the checkout workflow writes `teleport_order_items` AFTER the
10
+ // order row, one HTTP round-trip per cart line — so the invoice endpoint was
11
+ // reading the line items while they were still being inserted.
12
+ //
13
+ // Observed in a real store: a 3-line order (4x Linen Table Runner, 3x Ceramic
14
+ // Dinner Plate Set, 2x Wireless Bluetooth Speaker, charged 474.45) produced an
15
+ // invoice with ONE line item and a total of 249.00 — the two rows that had not
16
+ // been inserted yet were simply absent, and nothing anywhere said so.
17
+ //
18
+ // These tests run the EMITTED handler against a data-access stub whose rows
19
+ // arrive progressively, which is what the race actually looks like.
20
+
21
+ const FAKE_SETTINGS = {
22
+ invoicePrefix: 'INV-',
23
+ defaultTaxRate: 10,
24
+ showDiscount: false,
25
+ taxIncludedInPrice: false,
26
+ companyDetails: {},
27
+ template: { document: null as unknown },
28
+ tables: { invoicesTable: 'teleport_invoices', invoiceItemsTable: 'teleport_invoice_items' },
29
+ emailDelivery: { enabled: false },
30
+ } as unknown as UIDLInvoiceSettings
31
+
32
+ interface OrderItemRow {
33
+ product_id: string
34
+ product_name: string
35
+ quantity: number
36
+ unit_price: number
37
+ total_price: number
38
+ }
39
+
40
+ const ORDER_ROW_BASE = {
41
+ id: 'order-1',
42
+ currency: 'USD',
43
+ total_amount: 474.45,
44
+ shipping_amount: 150,
45
+ billing_name: 'Jane Buyer',
46
+ billing_email: 'jane@example.com',
47
+ }
48
+
49
+ const ITEM_ROWS: OrderItemRow[] = [
50
+ {
51
+ product_id: 'p1',
52
+ product_name: 'Linen Table Runner',
53
+ quantity: 4,
54
+ unit_price: 22.5,
55
+ total_price: 90,
56
+ },
57
+ {
58
+ product_id: 'p2',
59
+ product_name: 'Ceramic Dinner Plate Set',
60
+ quantity: 3,
61
+ unit_price: 34.99,
62
+ total_price: 104.97,
63
+ },
64
+ {
65
+ product_id: 'p3',
66
+ product_name: 'Wireless Bluetooth Speaker',
67
+ quantity: 2,
68
+ unit_price: 49.99,
69
+ total_price: 99.98,
70
+ },
71
+ ]
72
+
73
+ interface HandlerRun {
74
+ status: number
75
+ payload: Record<string, unknown>
76
+ insertedItems: unknown[]
77
+ reads: number
78
+ }
79
+
80
+ /**
81
+ * Loads the emitted `pages/api/invoices/generate.js` as a real module and
82
+ * invokes its handler.
83
+ *
84
+ * `orderNumberAfterRead` controls when checkout's order-number backfill
85
+ * becomes visible — that write happens after the item loop in both the COD
86
+ * and the online-payment branch, so it is one of the endpoint's "the order is
87
+ * whole now" signals. `Infinity` models a flow that never sets one.
88
+ */
89
+ async function runHandler(options: {
90
+ body: Record<string, unknown>
91
+ /** Number of item rows visible on the Nth read (1-indexed reads). */
92
+ visibleItemsPerRead: (read: number) => number
93
+ orderNumberAfterRead?: number
94
+ }): Promise<HandlerRun> {
95
+ const code = generateInvoiceGenerateRouteCode(FAKE_SETTINGS)
96
+
97
+ let reads = 0
98
+ const insertedItems: unknown[] = []
99
+ const orderNumberAfterRead = options.orderNumberAfterRead ?? 1
100
+
101
+ const dataAccessStub = {
102
+ getOrderWithItems: async (orderId: string) => {
103
+ reads += 1
104
+ if (orderId !== ORDER_ROW_BASE.id) {
105
+ return null
106
+ }
107
+ return {
108
+ order: {
109
+ ...ORDER_ROW_BASE,
110
+ order_number: reads >= orderNumberAfterRead ? 'ORD-1' : null,
111
+ },
112
+ items: ITEM_ROWS.slice(0, options.visibleItemsPerRead(reads)),
113
+ }
114
+ },
115
+ getNextInvoiceNumber: async () => 1,
116
+ insertInvoice: async (invoiceData: Record<string, unknown>) => invoiceData,
117
+ insertInvoiceItems: async (_invoiceId: string, items: unknown[]) => {
118
+ insertedItems.push(...items)
119
+ return items
120
+ },
121
+ updateInvoice: async () => ({}),
122
+ }
123
+
124
+ const pdfGeneratorStub = {
125
+ generateInvoicePdf: async () => Buffer.from('%PDF-1.4 fake'),
126
+ COMPANY_DETAILS: {},
127
+ }
128
+
129
+ const stubRequire = ((id: string) => {
130
+ if (id.indexOf('data-access') !== -1) return dataAccessStub
131
+ if (id.indexOf('pdf-generator') !== -1) return pdfGeneratorStub
132
+ if (id === 'pg') return { Client: class {} }
133
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
134
+ return require(id)
135
+ }) as unknown as NodeRequire
136
+
137
+ const factory = new Function(
138
+ 'require',
139
+ 'module',
140
+ 'exports',
141
+ `${code}; return module.exports;`
142
+ ) as (
143
+ req: NodeRequire,
144
+ mod: { exports: unknown },
145
+ exp: unknown
146
+ ) => (req: unknown, res: unknown) => Promise<void>
147
+
148
+ const moduleObject = { exports: {} as unknown }
149
+ const handler = factory(stubRequire, moduleObject, moduleObject.exports)
150
+
151
+ let status = 0
152
+ let payload: Record<string, unknown> = {}
153
+ const res = {
154
+ status(statusCode: number) {
155
+ status = statusCode
156
+ return this
157
+ },
158
+ json(value: Record<string, unknown>) {
159
+ payload = value
160
+ return this
161
+ },
162
+ setHeader() {
163
+ /* not used by this route */
164
+ },
165
+ end() {
166
+ /* not used by this route */
167
+ },
168
+ }
169
+
170
+ await handler({ method: 'POST', headers: { host: 'localhost:3000' }, body: options.body }, res)
171
+
172
+ return { status, payload, insertedItems, reads }
173
+ }
174
+
175
+ describe('/api/invoices/generate — waits for the order to be fully written', () => {
176
+ const originalFetch = globalThis.fetch
177
+ const originalConnString = process.env.TELEPORT_DB_CONNECTION_STRING
178
+ const originalDatabaseUrl = process.env.DATABASE_URL
179
+
180
+ beforeAll(() => {
181
+ // The route self-fetches the runtime-storage proxy and mirrors onto
182
+ // `teleport_orders`; neither is under test and neither may reach the
183
+ // network from a unit test.
184
+ globalThis.fetch = (async () => {
185
+ throw new Error('runtime storage not configured in tests')
186
+ }) as unknown as typeof fetch
187
+ delete process.env.TELEPORT_DB_CONNECTION_STRING
188
+ delete process.env.DATABASE_URL
189
+ })
190
+
191
+ afterAll(() => {
192
+ globalThis.fetch = originalFetch
193
+ if (originalConnString !== undefined) {
194
+ process.env.TELEPORT_DB_CONNECTION_STRING = originalConnString
195
+ }
196
+ if (originalDatabaseUrl !== undefined) {
197
+ process.env.DATABASE_URL = originalDatabaseUrl
198
+ }
199
+ })
200
+
201
+ it('invoices every line of an order whose items are still being inserted', async () => {
202
+ // The exact shape of the reported bug: one row visible on the first read,
203
+ // the rest landing while the endpoint runs. `expectedItemCount` is what
204
+ // the order-notification route forwards from the cart it was handed.
205
+ const run = await runHandler({
206
+ body: { orderId: 'order-1', expectedItemCount: 3 },
207
+ visibleItemsPerRead: (read) => Math.min(read, 3),
208
+ orderNumberAfterRead: Infinity,
209
+ })
210
+
211
+ expect(run.status).toBe(200)
212
+ expect(run.insertedItems).toHaveLength(3)
213
+ expect(run.insertedItems.map((item) => (item as { name: string }).name)).toEqual([
214
+ 'Linen Table Runner',
215
+ 'Ceramic Dinner Plate Set',
216
+ 'Wireless Bluetooth Speaker',
217
+ ])
218
+ // 90 + 104.97 + 99.98 = 294.95 net, +10% VAT = 324.45, +150 delivery.
219
+ expect(run.payload.total).toBe(474.45)
220
+ })
221
+
222
+ it('accepts the order as whole once checkout has backfilled the order number', async () => {
223
+ // No `expectedItemCount` — the webhook-driven callers send only an
224
+ // orderId. `teleport_orders.order_number` is written after the item loop,
225
+ // so its presence is the endpoint's own proof that no line is missing.
226
+ const run = await runHandler({
227
+ body: { orderId: 'order-1' },
228
+ visibleItemsPerRead: (read) => Math.min(read, 3),
229
+ orderNumberAfterRead: 3,
230
+ })
231
+
232
+ expect(run.status).toBe(200)
233
+ expect(run.insertedItems).toHaveLength(3)
234
+ expect(run.reads).toBe(3)
235
+ })
236
+
237
+ it('does not poll an order that is already complete on the first read', async () => {
238
+ // The common case — every payment-webhook invoice. It must cost exactly
239
+ // one query and no added latency.
240
+ const run = await runHandler({
241
+ body: { orderId: 'order-1' },
242
+ visibleItemsPerRead: () => 3,
243
+ orderNumberAfterRead: 1,
244
+ })
245
+
246
+ expect(run.status).toBe(200)
247
+ expect(run.insertedItems).toHaveLength(3)
248
+ expect(run.reads).toBe(1)
249
+ })
250
+
251
+ it('does not wait at all when the caller supplied its own line items', async () => {
252
+ const run = await runHandler({
253
+ body: {
254
+ orderId: 'order-1',
255
+ items: [{ name: 'Handpicked line', quantity: 1, unitPrice: 10, totalPrice: 10 }],
256
+ },
257
+ visibleItemsPerRead: () => 0,
258
+ orderNumberAfterRead: Infinity,
259
+ })
260
+
261
+ expect(run.status).toBe(200)
262
+ expect(run.reads).toBe(1)
263
+ expect(run.insertedItems).toHaveLength(1)
264
+ expect((run.insertedItems[0] as { name: string }).name).toBe('Handpicked line')
265
+ })
266
+
267
+ it('still rejects an order that never gets any line items', async () => {
268
+ const run = await runHandler({
269
+ body: { orderId: 'order-1', expectedItemCount: 3 },
270
+ visibleItemsPerRead: () => 0,
271
+ orderNumberAfterRead: 1,
272
+ })
273
+
274
+ expect(run.status).toBe(400)
275
+ expect(run.payload.error).toBe('At least one item is required')
276
+ }, 15000)
277
+ })
278
+
279
+ describe('/api/invoices/generate — settle contract (emitted source)', () => {
280
+ const route = generateInvoiceGenerateRouteCode(FAKE_SETTINGS)
281
+
282
+ it('reads the line items through the settling helper, never a bare query', () => {
283
+ expect(route).toContain('async function hydrateOrderWhenSettled(orderId, expectedItemCount)')
284
+ expect(route).toContain('await hydrateOrderWhenSettled(body.orderId, expectedItemCount)')
285
+ })
286
+
287
+ it('treats a populated order_number as proof the item loop finished', () => {
288
+ // Checkout writes it in "Mark Order As Cash On Delivery Confirmed" /
289
+ // "Set Order Number Before Payment Redirect", both downstream of the loop.
290
+ expect(route).toContain('var orderNumber = hydrated.order.order_number;')
291
+ expect(route).toContain(
292
+ 'if (count > 0 && orderNumber != null && String(orderNumber).length > 0) {'
293
+ )
294
+ })
295
+
296
+ it('bounds the wait so a stuck order still produces an invoice', () => {
297
+ expect(route).toContain('var SETTLE_TIMEOUT_MS = 6000;')
298
+ expect(route).toContain('did not settle within ')
299
+ })
300
+
301
+ it('warns when the invoice total disagrees with the amount charged', () => {
302
+ // The tripwire that would have caught this the first time: the invoice is
303
+ // summed from line items, `teleport_orders.total_amount` is what the buyer
304
+ // actually paid, and a missing line makes them disagree.
305
+ expect(route).toContain('var orderTotalAmount = Number(orderShippingSource.total_amount);')
306
+ expect(route).toContain('[invoice] Total mismatch for order ')
307
+ })
308
+ })