@teleporthq/teleport-plugin-next-workflows 0.43.39 → 0.43.41

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.
@@ -58,6 +58,21 @@ export const looksLikeStockDecrementBuilder = (code: string): boolean => {
58
58
  return false
59
59
  }
60
60
  if (code.indexOf(STOCK_DECREMENT_MARKER) >= 0) {
61
+ // Already our rewrite. A prior rewriter version (the
62
+ // `GREATEST(0, COALESCE(quantity, 0) - …)` shape below) permanently
63
+ // floored an unlimited-stock (NULL quantity) product to 0 the first
64
+ // time an order touched it — turning "infinite stock" into
65
+ // "out of stock" forever. Re-match that legacy shape so the next
66
+ // regeneration upgrades already-rewritten projects to the current,
67
+ // NULL-preserving SQL (`buildStockDecrementBuilder` below). A rewrite
68
+ // that already carries the NULL guard is up to date — leave it alone.
69
+ if (
70
+ code.indexOf('teleport_products') >= 0 &&
71
+ /quantity\s*=\s*GREATEST\s*\(\s*\d+\s*,\s*COALESCE\s*\(\s*quantity/i.test(code) &&
72
+ code.indexOf('WHEN quantity IS NULL THEN NULL') < 0
73
+ ) {
74
+ return true
75
+ }
61
76
  return false
62
77
  }
63
78
  if (code.indexOf('teleport_products') < 0) {
@@ -188,31 +203,27 @@ ${STOCK_DECREMENT_HELPERS}
188
203
  // the row AFTER the SET clause runs (so bare \`quantity\` would
189
204
  // be the new value, not the old).
190
205
  //
191
- // Two SQL helpers do the heavy lifting:
192
- //
193
- // 1. \`COALESCE(quantity, 0)\` every new project starts with
194
- // \`teleport_products.quantity\` seeded NULL, and the
195
- // cart-availability pre-flight tolerates NULL as "unlimited"
196
- // so those orders reach this UPDATE. Without COALESCE the
197
- // NULL row's arithmetic would be NULL (no decrement) and the
198
- // merchant would see "order placed, stock unchanged" and
199
- // assume the workflow is broken.
200
- // 2. \`GREATEST(0, …)\` clamps the post-update value at zero so
201
- // we never persist a negative quantity. Negative stock is a
202
- // confusing signal in the admin panel ("we sold more than we
203
- // have?"); the same "set your initial stock" feedback is
204
- // achieved by the row sitting at exactly 0 subsequent
205
- // orders are then blocked by the cart-availability check
206
- // (which compares against 0 and finds insufficient stock).
207
- var setClause = "quantity = GREATEST(0, COALESCE(quantity, 0) - CASE id" + caseExpr + " ELSE 0 END), updated_at = NOW()";
208
- // \`old_quantity\` in RETURNING reconstructs the pre-update value.
209
- // After the GREATEST clamp the SET value is no longer
210
- // "old - delta" — it might be 0 because of the floor. So we can't
206
+ // \`teleport_products.quantity\` is NULL for every product the
207
+ // merchant hasn't given a finite count — that NULL means
208
+ // "unlimited stock", not "zero stock waiting to be bootstrapped".
209
+ // The SET clause's \`CASE WHEN quantity IS NULL THEN NULL …\` guard
210
+ // leaves those rows untouched forever: an unlimited product must
211
+ // stay unlimited across every order, not silently become
212
+ // out-of-stock (0) the first time someone buys it. Only a row
213
+ // that already carries a real, finite count is decremented, and
214
+ // \`GREATEST(0, …)\` floors THAT arithmetic at zero so we never
215
+ // persist a negative quantity for a tracked product.
216
+ var setClause = "quantity = CASE WHEN quantity IS NULL THEN NULL ELSE GREATEST(0, quantity - (CASE id" + caseExpr + " ELSE 0 END)) END, updated_at = NOW()";
217
+ // \`old_quantity\` in RETURNING reconstructs the pre-update value. A
218
+ // row whose (post-update) \`new_quantity\` is NULL was NULL before too
219
+ // (the SET clause never changes a NULL row), so \`old_quantity\` is
220
+ // NULL as well. For a tracked row, the GREATEST clamp means the SET
221
+ // value is no longer "old - delta" once it floors at 0, so we can't
211
222
  // just invert the SET expression; instead we expose 0 as a
212
223
  // conservative lower bound for what was there before. Downstream
213
224
  // (low-stock SELECT) only uses \`new_quantity\` for the threshold
214
225
  // comparison, so this approximation is fine.
215
- var returningClause = "RETURNING id, quantity AS new_quantity, GREATEST(0, COALESCE(quantity, 0) + (CASE id" + caseExpr + " ELSE 0 END)) AS old_quantity";
226
+ var returningClause = "RETURNING id, quantity AS new_quantity, (CASE WHEN quantity IS NULL THEN NULL ELSE GREATEST(0, quantity + (CASE id" + caseExpr + " ELSE 0 END)) END) AS old_quantity";
216
227
 
217
228
  // When backorders are DISALLOWED we add a per-row guard:
218
229
  // AND (quantity IS NULL OR quantity >= CASE id ... END)
@@ -223,17 +234,12 @@ ${STOCK_DECREMENT_HELPERS}
223
234
  // would otherwise allow two simultaneous orders to drive stock
224
235
  // below zero.
225
236
  //
226
- // The "quantity IS NULL OR" clause is the new-project bootstrap:
227
- // a freshly-seeded products table has NULL quantity everywhere,
228
- // and the cart-availability pre-flight (which also tolerates NULL
229
- // as "unlimited") lets those orders through. We MUST decrement
230
- // those rows too, otherwise the user sees "order placed but stock
231
- // unchanged" and concludes the workflow is broken. After the first
232
- // decrement the row holds an integer (now floored at 0 by the
233
- // GREATEST clamp in the SET clause) and from then on the normal
234
- // \`quantity >= delta\` guard is enforced — so the merchant sees
235
- // the zero value in the admin panel and gets a clear "set initial
236
- // stock for this product" signal.
237
+ // The "quantity IS NULL OR" clause lets unlimited-stock products
238
+ // through the guard so the row is still touched (its RETURNING row
239
+ // still surfaces to the caller, e.g. for the low-stock SELECT) the
240
+ // SET clause above then leaves the value itself untouched, so an
241
+ // unlimited product stays unlimited no matter how many orders are
242
+ // placed against it, with or without backorders enabled.
237
243
  var whereClause;
238
244
  if (ALLOW_BACKORDERS) {
239
245
  whereClause = "WHERE id IN (" + quotedIds + ")";
@@ -328,10 +334,12 @@ const looksLikeOrderDecrementCustomHandler = (code: string): boolean => {
328
334
  // Belt-and-braces: even when the marker is present, the code must
329
335
  // actually be a stock-decrement (not, say, an unrelated rewritten
330
336
  // node that happens to live next to teleport_products references).
331
- // Accept BOTH the legacy concat shape (`quantity = quantity - CASE
332
- // id …`) AND the current safety-wrapped shape (`quantity =
333
- // GREATEST(0, COALESCE(quantity, 0) - CASE id …)`) they're both
334
- // emitted by `buildStockDecrementBuilder` across time.
337
+ // Accept every shape `buildStockDecrementBuilder` has emitted over
338
+ // time: the legacy concat shape (`quantity = quantity - CASE id …`),
339
+ // the COALESCE safety-wrapped shape (`quantity = GREATEST(0,
340
+ // COALESCE(quantity, 0) - CASE id …)`), and the current
341
+ // NULL-preserving shape (`quantity = CASE WHEN quantity IS NULL
342
+ // THEN NULL ELSE GREATEST(0, quantity - CASE id …) END`).
335
343
  if (code.indexOf('teleport_products') >= 0) {
336
344
  if (/quantity\s*=\s*quantity\s*-/.test(code)) {
337
345
  return true
@@ -339,6 +347,13 @@ const looksLikeOrderDecrementCustomHandler = (code: string): boolean => {
339
347
  if (/quantity\s*=\s*GREATEST\s*\(\s*\d+\s*,\s*COALESCE\s*\(\s*quantity/i.test(code)) {
340
348
  return true
341
349
  }
350
+ if (
351
+ /quantity\s*=\s*CASE\s+WHEN\s+quantity\s+IS\s+NULL\s+THEN\s+NULL\s+ELSE\s+GREATEST/i.test(
352
+ code
353
+ )
354
+ ) {
355
+ return true
356
+ }
342
357
  }
343
358
  return false
344
359
  }
@@ -1,31 +1,5 @@
1
1
  import { NodeHandlerGenerator, handlerToString } from '../types'
2
2
 
3
- // Stripe treats these currencies as zero-decimal (no fractional units). The
4
- // amount passed to the Stripe API must be the whole-currency integer, not
5
- // multiplied by 100. See https://stripe.com/docs/currencies#zero-decimal
6
- const STRIPE_ZERO_DECIMAL_CURRENCIES = [
7
- 'BIF',
8
- 'CLP',
9
- 'DJF',
10
- 'GNF',
11
- 'JPY',
12
- 'KMF',
13
- 'KRW',
14
- 'MGA',
15
- 'PYG',
16
- 'RWF',
17
- 'UGX',
18
- 'VND',
19
- 'VUV',
20
- 'XAF',
21
- 'XOF',
22
- 'XPF',
23
- ]
24
-
25
- // Currencies where the smallest unit is 1/1000 of the major unit. Stripe
26
- // expects amounts in the smallest unit (e.g. 1.500 JOD -> 1500).
27
- const STRIPE_THREE_DECIMAL_CURRENCIES = ['BHD', 'IQD', 'JOD', 'KWD', 'LYD', 'OMR', 'TND']
28
-
29
3
  // Contract for the `amount` config field:
30
4
  // A floating-point number in the MAJOR currency unit (e.g. 99.99 USD,
31
5
  // 9999 JPY). Individual line items use the same `unitAmount` semantics.
@@ -157,16 +131,54 @@ async function payment_charge_user(config: any, _context: Record<string, unknown
157
131
  // smallest-unit integer. Zero-decimal currencies pass through rounded;
158
132
  // three-decimal currencies multiply by 1000; everything else multiplies by
159
133
  // 100. Always returns a safe non-negative integer.
134
+ //
135
+ // The currency lists are declared LOCAL to this function rather than as
136
+ // shared top-level consts. `generateHandler()` below assembles the final
137
+ // handler by calling `.toString()` on several functions independently and
138
+ // concatenating the source; a top-level const is fine in an unminified
139
+ // build, but when this package itself is bundled + minified by a consumer
140
+ // (e.g. teleport-gui's browser packer, built with webpack/Terser), the
141
+ // minifier is free to rename the top-level const's declaration — nothing
142
+ // in the bundle calls it by name, only this runtime `.toString()` read
143
+ // does, which is invisible to the minifier. `toStripeMinorUnits.toString()`
144
+ // would then embed a reference to a name (e.g. a mangled `Oo`) that is
145
+ // never declared anywhere in the generated workflow segment file, throwing
146
+ // "<mangled name> is not defined" the first time a Stripe charge runs.
147
+ // Declaring the lists inside the function keeps the declaration and every
148
+ // reference to it in the SAME `.toString()` snapshot, so a consistent
149
+ // rename by the minifier can never separate them. See `resolveProviderSecret`
150
+ // above for the same class of bug with the `process` global.
160
151
  function toStripeMinorUnits(major: any, currency: string): number {
152
+ const zeroDecimalCurrencies = [
153
+ 'BIF',
154
+ 'CLP',
155
+ 'DJF',
156
+ 'GNF',
157
+ 'JPY',
158
+ 'KMF',
159
+ 'KRW',
160
+ 'MGA',
161
+ 'PYG',
162
+ 'RWF',
163
+ 'UGX',
164
+ 'VND',
165
+ 'VUV',
166
+ 'XAF',
167
+ 'XOF',
168
+ 'XPF',
169
+ ]
170
+ // Currencies where the smallest unit is 1/1000 of the major unit. Stripe
171
+ // expects amounts in the smallest unit (e.g. 1.500 JOD -> 1500).
172
+ const threeDecimalCurrencies = ['BHD', 'IQD', 'JOD', 'KWD', 'LYD', 'OMR', 'TND']
161
173
  const amt = Number(major)
162
174
  if (!isFinite(amt) || amt <= 0) {
163
175
  return 0
164
176
  }
165
177
  const upper = String(currency || '').toUpperCase()
166
- if (STRIPE_ZERO_DECIMAL_CURRENCIES.indexOf(upper) >= 0) {
178
+ if (zeroDecimalCurrencies.indexOf(upper) >= 0) {
167
179
  return Math.round(amt)
168
180
  }
169
- if (STRIPE_THREE_DECIMAL_CURRENCIES.indexOf(upper) >= 0) {
181
+ if (threeDecimalCurrencies.indexOf(upper) >= 0) {
170
182
  return Math.round(amt * 1000)
171
183
  }
172
184
  return Math.round(amt * 100)
@@ -470,12 +482,6 @@ export const paymentChargeUser: NodeHandlerGenerator = {
470
482
  generateHandler(): string {
471
483
  return (
472
484
  handlerToString(payment_charge_user) +
473
- '\nvar STRIPE_ZERO_DECIMAL_CURRENCIES = ' +
474
- JSON.stringify(STRIPE_ZERO_DECIMAL_CURRENCIES) +
475
- ';' +
476
- '\nvar STRIPE_THREE_DECIMAL_CURRENCIES = ' +
477
- JSON.stringify(STRIPE_THREE_DECIMAL_CURRENCIES) +
478
- ';' +
479
485
  '\n' +
480
486
  resolveProviderSecret.toString() +
481
487
  '\n' +