@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.
- package/__tests__/ai-chat-rag-sql-parameterization.test.ts +28 -8
- package/__tests__/ai-select-database-data-guard.test.ts +218 -0
- package/__tests__/ai-select-database-data-handler.test.ts +237 -0
- package/__tests__/client-config-redaction.test.ts +34 -2
- package/__tests__/invoice-generate-hydration.test.ts +7 -2
- package/__tests__/invoice-order-items-settle.test.ts +308 -0
- package/__tests__/parameterized-stock-chain.test.ts +453 -0
- package/__tests__/stock-decrement-audit.test.ts +5 -1
- package/__tests__/streaming-chunk-payload.test.ts +91 -0
- package/__tests__/streaming-on-end-env-filter.test.ts +2 -2
- package/dist/cjs/api-route-generator.d.ts.map +1 -1
- package/dist/cjs/api-route-generator.js +2 -3
- package/dist/cjs/api-route-generator.js.map +1 -1
- package/dist/cjs/data-api-route-generator.d.ts.map +1 -1
- package/dist/cjs/data-api-route-generator.js +4 -0
- package/dist/cjs/data-api-route-generator.js.map +1 -1
- package/dist/cjs/ecommerce/parameterized-stock-chain.d.ts +23 -0
- package/dist/cjs/ecommerce/parameterized-stock-chain.d.ts.map +1 -0
- package/dist/cjs/ecommerce/parameterized-stock-chain.js +397 -0
- package/dist/cjs/ecommerce/parameterized-stock-chain.js.map +1 -0
- package/dist/cjs/ecommerce-customhandler-rewriter.d.ts +3 -0
- package/dist/cjs/ecommerce-customhandler-rewriter.d.ts.map +1 -1
- package/dist/cjs/ecommerce-customhandler-rewriter.js +41 -1
- package/dist/cjs/ecommerce-customhandler-rewriter.js.map +1 -1
- package/dist/cjs/executor-generator.js +1 -1
- package/dist/cjs/invoice/api-routes-code.d.ts.map +1 -1
- package/dist/cjs/invoice/api-routes-code.js +132 -3
- package/dist/cjs/invoice/api-routes-code.js.map +1 -1
- package/dist/cjs/nodes/ai/ai-provider-utils.js +1 -1
- package/dist/cjs/nodes/ai/ai-select-database-data.d.ts +3 -0
- package/dist/cjs/nodes/ai/ai-select-database-data.d.ts.map +1 -0
- package/dist/cjs/nodes/ai/ai-select-database-data.js +141 -0
- package/dist/cjs/nodes/ai/ai-select-database-data.js.map +1 -0
- package/dist/cjs/nodes/ai/ai-sql-select-guard.d.ts +18 -0
- package/dist/cjs/nodes/ai/ai-sql-select-guard.d.ts.map +1 -0
- package/dist/cjs/nodes/ai/ai-sql-select-guard.js +388 -0
- package/dist/cjs/nodes/ai/ai-sql-select-guard.js.map +1 -0
- package/dist/cjs/nodes/index.d.ts.map +1 -1
- package/dist/cjs/nodes/index.js +2 -1
- package/dist/cjs/nodes/index.js.map +1 -1
- package/dist/cjs/tsconfig.tsbuildinfo +1 -1
- package/dist/esm/api-route-generator.d.ts.map +1 -1
- package/dist/esm/api-route-generator.js +2 -3
- package/dist/esm/api-route-generator.js.map +1 -1
- package/dist/esm/data-api-route-generator.d.ts.map +1 -1
- package/dist/esm/data-api-route-generator.js +4 -0
- package/dist/esm/data-api-route-generator.js.map +1 -1
- package/dist/esm/ecommerce/parameterized-stock-chain.d.ts +23 -0
- package/dist/esm/ecommerce/parameterized-stock-chain.d.ts.map +1 -0
- package/dist/esm/ecommerce/parameterized-stock-chain.js +385 -0
- package/dist/esm/ecommerce/parameterized-stock-chain.js.map +1 -0
- package/dist/esm/ecommerce-customhandler-rewriter.d.ts +3 -0
- package/dist/esm/ecommerce-customhandler-rewriter.d.ts.map +1 -1
- package/dist/esm/ecommerce-customhandler-rewriter.js +41 -1
- package/dist/esm/ecommerce-customhandler-rewriter.js.map +1 -1
- package/dist/esm/executor-generator.js +1 -1
- package/dist/esm/invoice/api-routes-code.d.ts.map +1 -1
- package/dist/esm/invoice/api-routes-code.js +132 -3
- package/dist/esm/invoice/api-routes-code.js.map +1 -1
- package/dist/esm/nodes/ai/ai-provider-utils.js +1 -1
- package/dist/esm/nodes/ai/ai-select-database-data.d.ts +3 -0
- package/dist/esm/nodes/ai/ai-select-database-data.d.ts.map +1 -0
- package/dist/esm/nodes/ai/ai-select-database-data.js +138 -0
- package/dist/esm/nodes/ai/ai-select-database-data.js.map +1 -0
- package/dist/esm/nodes/ai/ai-sql-select-guard.d.ts +18 -0
- package/dist/esm/nodes/ai/ai-sql-select-guard.d.ts.map +1 -0
- package/dist/esm/nodes/ai/ai-sql-select-guard.js +384 -0
- package/dist/esm/nodes/ai/ai-sql-select-guard.js.map +1 -0
- package/dist/esm/nodes/index.d.ts.map +1 -1
- package/dist/esm/nodes/index.js +2 -1
- package/dist/esm/nodes/index.js.map +1 -1
- package/dist/esm/tsconfig.tsbuildinfo +1 -1
- package/package.json +4 -4
- package/src/api-route-generator.ts +2 -3
- package/src/data-api-route-generator.ts +4 -0
- package/src/ecommerce/parameterized-stock-chain.ts +449 -0
- package/src/ecommerce-customhandler-rewriter.ts +71 -3
- package/src/executor-generator.ts +1 -1
- package/src/invoice/api-routes-code.ts +132 -3
- package/src/nodes/ai/ai-provider-utils.ts +1 -1
- package/src/nodes/ai/ai-select-database-data.ts +162 -0
- package/src/nodes/ai/ai-sql-select-guard.ts +402 -0
- 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
|
+
})
|