@teleporthq/teleport-plugin-next-workflows 0.43.65 → 0.43.66

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 (168) hide show
  1. package/__tests__/cart-clear-db-mirror.test.ts +12 -0
  2. package/__tests__/cart-get-total-regional-pricing.test.ts +166 -0
  3. package/__tests__/continue-on-error.test.ts +119 -0
  4. package/__tests__/invoice-delivery-fee.test.ts +104 -0
  5. package/__tests__/invoice-regional-tax.test.ts +126 -0
  6. package/__tests__/payment-redirect-and-order-notification.test.ts +6 -0
  7. package/__tests__/paypal-webhook-order-notification.test.ts +41 -0
  8. package/__tests__/sent-email-log-module.test.ts +381 -0
  9. package/__tests__/sent-email-log-wiring.test.ts +267 -0
  10. package/dist/cjs/account-delete-route-generator.d.ts.map +1 -1
  11. package/dist/cjs/account-delete-route-generator.js +14 -5
  12. package/dist/cjs/account-delete-route-generator.js.map +1 -1
  13. package/dist/cjs/api-route-generator.d.ts.map +1 -1
  14. package/dist/cjs/api-route-generator.js +44 -9
  15. package/dist/cjs/api-route-generator.js.map +1 -1
  16. package/dist/cjs/auth-generator.d.ts.map +1 -1
  17. package/dist/cjs/auth-generator.js +15 -6
  18. package/dist/cjs/auth-generator.js.map +1 -1
  19. package/dist/cjs/executor-generator.d.ts.map +1 -1
  20. package/dist/cjs/executor-generator.js +17 -4
  21. package/dist/cjs/executor-generator.js.map +1 -1
  22. package/dist/cjs/index.d.ts +1 -0
  23. package/dist/cjs/index.d.ts.map +1 -1
  24. package/dist/cjs/index.js +3 -1
  25. package/dist/cjs/index.js.map +1 -1
  26. package/dist/cjs/invoice/api-routes-code.d.ts.map +1 -1
  27. package/dist/cjs/invoice/api-routes-code.js +42 -4
  28. package/dist/cjs/invoice/api-routes-code.js.map +1 -1
  29. package/dist/cjs/invoice/email-sender-code.d.ts.map +1 -1
  30. package/dist/cjs/invoice/email-sender-code.js +58 -29
  31. package/dist/cjs/invoice/email-sender-code.js.map +1 -1
  32. package/dist/cjs/invoice/index.d.ts +1 -4
  33. package/dist/cjs/invoice/index.d.ts.map +1 -1
  34. package/dist/cjs/invoice/index.js +5 -19
  35. package/dist/cjs/invoice/index.js.map +1 -1
  36. package/dist/cjs/invoice/invoice-html-code.d.ts.map +1 -1
  37. package/dist/cjs/invoice/invoice-html-code.js +31 -8
  38. package/dist/cjs/invoice/invoice-html-code.js.map +1 -1
  39. package/dist/cjs/invoice/regional-invoice-tax-code.d.ts +25 -0
  40. package/dist/cjs/invoice/regional-invoice-tax-code.d.ts.map +1 -0
  41. package/dist/cjs/invoice/regional-invoice-tax-code.js +116 -0
  42. package/dist/cjs/invoice/regional-invoice-tax-code.js.map +1 -0
  43. package/dist/cjs/invoice/resolve-data-source.d.ts +6 -0
  44. package/dist/cjs/invoice/resolve-data-source.d.ts.map +1 -0
  45. package/dist/cjs/invoice/resolve-data-source.js +23 -0
  46. package/dist/cjs/invoice/resolve-data-source.js.map +1 -0
  47. package/dist/cjs/nodes/cart/cart-clear.d.ts.map +1 -1
  48. package/dist/cjs/nodes/cart/cart-clear.js +6 -0
  49. package/dist/cjs/nodes/cart/cart-clear.js.map +1 -1
  50. package/dist/cjs/nodes/cart/cart-get-total.d.ts.map +1 -1
  51. package/dist/cjs/nodes/cart/cart-get-total.js +81 -5
  52. package/dist/cjs/nodes/cart/cart-get-total.js.map +1 -1
  53. package/dist/cjs/nodes/data/data-create-item.d.ts.map +1 -1
  54. package/dist/cjs/nodes/data/data-create-item.js +8 -0
  55. package/dist/cjs/nodes/data/data-create-item.js.map +1 -1
  56. package/dist/cjs/sent-email-log/index.d.ts +16 -0
  57. package/dist/cjs/sent-email-log/index.d.ts.map +1 -0
  58. package/dist/cjs/sent-email-log/index.js +53 -0
  59. package/dist/cjs/sent-email-log/index.js.map +1 -0
  60. package/dist/cjs/sent-email-log/sent-email-log-code.d.ts +4 -0
  61. package/dist/cjs/sent-email-log/sent-email-log-code.d.ts.map +1 -0
  62. package/dist/cjs/sent-email-log/sent-email-log-code.js +415 -0
  63. package/dist/cjs/sent-email-log/sent-email-log-code.js.map +1 -0
  64. package/dist/cjs/sent-email-log/sent-email-log-scope.d.ts +21 -0
  65. package/dist/cjs/sent-email-log/sent-email-log-scope.d.ts.map +1 -0
  66. package/dist/cjs/sent-email-log/sent-email-log-scope.js +36 -0
  67. package/dist/cjs/sent-email-log/sent-email-log-scope.js.map +1 -0
  68. package/dist/cjs/transactional-email-code.d.ts +12 -1
  69. package/dist/cjs/transactional-email-code.d.ts.map +1 -1
  70. package/dist/cjs/transactional-email-code.js +43 -5
  71. package/dist/cjs/transactional-email-code.js.map +1 -1
  72. package/dist/cjs/tsconfig.tsbuildinfo +1 -1
  73. package/dist/cjs/webhook-generator.d.ts.map +1 -1
  74. package/dist/cjs/webhook-generator.js +9 -2
  75. package/dist/cjs/webhook-generator.js.map +1 -1
  76. package/dist/cjs/workflow-project-plugin.d.ts.map +1 -1
  77. package/dist/cjs/workflow-project-plugin.js +14 -0
  78. package/dist/cjs/workflow-project-plugin.js.map +1 -1
  79. package/dist/esm/account-delete-route-generator.d.ts.map +1 -1
  80. package/dist/esm/account-delete-route-generator.js +15 -6
  81. package/dist/esm/account-delete-route-generator.js.map +1 -1
  82. package/dist/esm/api-route-generator.d.ts.map +1 -1
  83. package/dist/esm/api-route-generator.js +44 -9
  84. package/dist/esm/api-route-generator.js.map +1 -1
  85. package/dist/esm/auth-generator.d.ts.map +1 -1
  86. package/dist/esm/auth-generator.js +15 -6
  87. package/dist/esm/auth-generator.js.map +1 -1
  88. package/dist/esm/executor-generator.d.ts.map +1 -1
  89. package/dist/esm/executor-generator.js +17 -4
  90. package/dist/esm/executor-generator.js.map +1 -1
  91. package/dist/esm/index.d.ts +1 -0
  92. package/dist/esm/index.d.ts.map +1 -1
  93. package/dist/esm/index.js +1 -0
  94. package/dist/esm/index.js.map +1 -1
  95. package/dist/esm/invoice/api-routes-code.d.ts.map +1 -1
  96. package/dist/esm/invoice/api-routes-code.js +42 -4
  97. package/dist/esm/invoice/api-routes-code.js.map +1 -1
  98. package/dist/esm/invoice/email-sender-code.d.ts.map +1 -1
  99. package/dist/esm/invoice/email-sender-code.js +58 -29
  100. package/dist/esm/invoice/email-sender-code.js.map +1 -1
  101. package/dist/esm/invoice/index.d.ts +1 -4
  102. package/dist/esm/invoice/index.d.ts.map +1 -1
  103. package/dist/esm/invoice/index.js +4 -18
  104. package/dist/esm/invoice/index.js.map +1 -1
  105. package/dist/esm/invoice/invoice-html-code.d.ts.map +1 -1
  106. package/dist/esm/invoice/invoice-html-code.js +31 -8
  107. package/dist/esm/invoice/invoice-html-code.js.map +1 -1
  108. package/dist/esm/invoice/regional-invoice-tax-code.d.ts +25 -0
  109. package/dist/esm/invoice/regional-invoice-tax-code.d.ts.map +1 -0
  110. package/dist/esm/invoice/regional-invoice-tax-code.js +113 -0
  111. package/dist/esm/invoice/regional-invoice-tax-code.js.map +1 -0
  112. package/dist/esm/invoice/resolve-data-source.d.ts +6 -0
  113. package/dist/esm/invoice/resolve-data-source.d.ts.map +1 -0
  114. package/dist/esm/invoice/resolve-data-source.js +19 -0
  115. package/dist/esm/invoice/resolve-data-source.js.map +1 -0
  116. package/dist/esm/nodes/cart/cart-clear.d.ts.map +1 -1
  117. package/dist/esm/nodes/cart/cart-clear.js +6 -0
  118. package/dist/esm/nodes/cart/cart-clear.js.map +1 -1
  119. package/dist/esm/nodes/cart/cart-get-total.d.ts.map +1 -1
  120. package/dist/esm/nodes/cart/cart-get-total.js +81 -5
  121. package/dist/esm/nodes/cart/cart-get-total.js.map +1 -1
  122. package/dist/esm/nodes/data/data-create-item.d.ts.map +1 -1
  123. package/dist/esm/nodes/data/data-create-item.js +8 -0
  124. package/dist/esm/nodes/data/data-create-item.js.map +1 -1
  125. package/dist/esm/sent-email-log/index.d.ts +16 -0
  126. package/dist/esm/sent-email-log/index.d.ts.map +1 -0
  127. package/dist/esm/sent-email-log/index.js +45 -0
  128. package/dist/esm/sent-email-log/index.js.map +1 -0
  129. package/dist/esm/sent-email-log/sent-email-log-code.d.ts +4 -0
  130. package/dist/esm/sent-email-log/sent-email-log-code.d.ts.map +1 -0
  131. package/dist/esm/sent-email-log/sent-email-log-code.js +411 -0
  132. package/dist/esm/sent-email-log/sent-email-log-code.js.map +1 -0
  133. package/dist/esm/sent-email-log/sent-email-log-scope.d.ts +21 -0
  134. package/dist/esm/sent-email-log/sent-email-log-scope.d.ts.map +1 -0
  135. package/dist/esm/sent-email-log/sent-email-log-scope.js +31 -0
  136. package/dist/esm/sent-email-log/sent-email-log-scope.js.map +1 -0
  137. package/dist/esm/transactional-email-code.d.ts +12 -1
  138. package/dist/esm/transactional-email-code.d.ts.map +1 -1
  139. package/dist/esm/transactional-email-code.js +42 -4
  140. package/dist/esm/transactional-email-code.js.map +1 -1
  141. package/dist/esm/tsconfig.tsbuildinfo +1 -1
  142. package/dist/esm/webhook-generator.d.ts.map +1 -1
  143. package/dist/esm/webhook-generator.js +9 -2
  144. package/dist/esm/webhook-generator.js.map +1 -1
  145. package/dist/esm/workflow-project-plugin.d.ts.map +1 -1
  146. package/dist/esm/workflow-project-plugin.js +14 -0
  147. package/dist/esm/workflow-project-plugin.js.map +1 -1
  148. package/package.json +4 -4
  149. package/src/account-delete-route-generator.ts +16 -5
  150. package/src/api-route-generator.ts +51 -9
  151. package/src/auth-generator.ts +17 -6
  152. package/src/executor-generator.ts +17 -4
  153. package/src/index.ts +1 -0
  154. package/src/invoice/api-routes-code.ts +48 -4
  155. package/src/invoice/email-sender-code.ts +58 -29
  156. package/src/invoice/index.ts +4 -23
  157. package/src/invoice/invoice-html-code.ts +31 -8
  158. package/src/invoice/regional-invoice-tax-code.ts +112 -0
  159. package/src/invoice/resolve-data-source.ts +25 -0
  160. package/src/nodes/cart/cart-clear.ts +6 -0
  161. package/src/nodes/cart/cart-get-total.ts +94 -5
  162. package/src/nodes/data/data-create-item.ts +8 -0
  163. package/src/sent-email-log/index.ts +57 -0
  164. package/src/sent-email-log/sent-email-log-code.ts +419 -0
  165. package/src/sent-email-log/sent-email-log-scope.ts +36 -0
  166. package/src/transactional-email-code.ts +61 -4
  167. package/src/webhook-generator.ts +11 -2
  168. package/src/workflow-project-plugin.ts +15 -0
@@ -1,3 +1,4 @@
1
+ import { RegionalPricing } from '@teleporthq/teleport-shared'
1
2
  import { cartClear } from '../src/nodes/cart/cart-clear'
2
3
 
3
4
  // Regression: after a cash-on-delivery order the buyer's cart came back.
@@ -42,4 +43,15 @@ describe('cart-clear mirrors the empty cart into the database cart', () => {
42
43
  it('stamps the clear so the provider can tell it apart from a first visit', () => {
43
44
  expect(handlerCode).toContain("localStorage.setItem('workflow_cart_cleared_at'")
44
45
  })
46
+
47
+ it('forgets the shipping method picked for the emptied cart', () => {
48
+ // Otherwise the next order silently pre-selects (and charges) a rate the
49
+ // buyer chose for a previous one instead of the cheapest.
50
+ expect(handlerCode).toContain(
51
+ `localStorage.removeItem('${RegionalPricing.SHIPPING_RATE_STORAGE_KEY}')`
52
+ )
53
+ expect(handlerCode).toContain(
54
+ `dispatchEvent(new CustomEvent('${RegionalPricing.SHIPPING_RATE_CHANGED_EVENT}'))`
55
+ )
56
+ })
45
57
  })
@@ -0,0 +1,166 @@
1
+ /* tslint:disable:no-eval */
2
+ import { cartGetTotal } from '../src/nodes/cart/cart-get-total'
3
+ import { resolveHandlerEntryName } from '../src/nodes/types'
4
+
5
+ /**
6
+ * `cart-get-total` is where the place-order workflow's money comes from, so on
7
+ * a store with shipping zones and tax jurisdictions it re-prices the basket AT
8
+ * SUBMIT — for the address on the checkout form at that moment, from the rows
9
+ * the provider displayed — with the same shared rule the provider uses.
10
+ *
11
+ * A store without the feature must come out exactly as before: no `regional`
12
+ * key, and the untouched single-rate fields.
13
+ */
14
+
15
+ function evalHandler(code: string): any {
16
+ return eval('(' + code + ')')
17
+ }
18
+
19
+ const runHandler = async () => evalHandler(cartGetTotal.generateHandler())()
20
+
21
+ function mount(storage: Record<string, unknown>, fields: Record<string, string>): void {
22
+ const store: Record<string, string> = {}
23
+ for (const key of Object.keys(storage)) {
24
+ const value = storage[key]
25
+ store[key] = typeof value === 'string' ? value : JSON.stringify(value)
26
+ }
27
+ ;(global as any).localStorage = {
28
+ getItem: (key: string) => (key in store ? store[key] : null),
29
+ }
30
+ ;(global as any).document = {
31
+ querySelector: (selector: string) => {
32
+ const match = /^\[name="([^"]+)"\]$/.exec(selector)
33
+ const name = match ? match[1] : ''
34
+ return name in fields ? { value: fields[name] } : null
35
+ },
36
+ }
37
+ }
38
+
39
+ afterEach(() => {
40
+ delete (global as any).localStorage
41
+ delete (global as any).document
42
+ })
43
+
44
+ const STORE = {
45
+ deliveryEnabled: true,
46
+ storePickupEnabled: true,
47
+ deliveryPrice: 5,
48
+ freeDeliveryEnabled: false,
49
+ freeDeliveryThreshold: 0,
50
+ defaultTaxRate: 20,
51
+ defaultTaxIncluded: false,
52
+ }
53
+
54
+ const ROWS = {
55
+ zones: [{ id: 'z1', name: 'Germany', countries: 'DE', cod_enabled: 'f', is_active: 't' }],
56
+ rates: [
57
+ { id: 'r1', zone_id: 'z1', name: 'Standard', rate_type: 'flat', price: '10.00' },
58
+ { id: 'r2', zone_id: 'z1', name: 'Express', rate_type: 'flat', price: '20.00', sort_order: 1 },
59
+ ],
60
+ taxRates: [{ id: 't1', country_code: 'DE', rate: '19', tax_mode: 'added', tax_shipping: true }],
61
+ }
62
+
63
+ const CART = [{ id: 'l1', productId: 'p1', price: 100, quantity: 1 }]
64
+
65
+ const settingsWith = (regional: Record<string, unknown> | null) => ({
66
+ maxQuantityPerProduct: null,
67
+ deliveryConfig: { deliveryPrice: 5, freeDeliveryEnabled: false, freeDeliveryThreshold: 0 },
68
+ taxConfig: { storefrontTaxRate: 20 },
69
+ ...(regional ? { regional } : {}),
70
+ })
71
+
72
+ const REGIONAL = {
73
+ store: STORE,
74
+ currency: 'EUR',
75
+ countryCodes: { germany: 'DE', brazil: 'BR' },
76
+ rows: ROWS,
77
+ }
78
+
79
+ describe('cart-get-total — regional pricing', () => {
80
+ it('stays one function expression whose entry point still resolves', () => {
81
+ const source = cartGetTotal.generateHandler()
82
+ expect(source.trim().startsWith('async function cart_get_total()')).toBe(true)
83
+ expect(resolveHandlerEntryName(source, 'cart-get-total')).toBe('cart_get_total')
84
+ })
85
+
86
+ it('adds no regional figures for a store without the feature', async () => {
87
+ mount({ workflow_cart: CART, workflow_cart_settings: settingsWith(null) }, {})
88
+ const out = await runHandler()
89
+ expect(out.regional).toBeUndefined()
90
+ expect(out.tax).toBe(20)
91
+ expect(out.total).toBe(120)
92
+ })
93
+
94
+ it("prices the destination read off the form, with the buyer's chosen rate", async () => {
95
+ mount(
96
+ {
97
+ workflow_cart: CART,
98
+ workflow_cart_settings: settingsWith(REGIONAL),
99
+ workflow_shipping_rate: 'r2',
100
+ },
101
+ { billingCountry: 'Germany', state: '' }
102
+ )
103
+ const out = await runHandler()
104
+ expect(out.subtotal).toBe(100)
105
+ expect(out.tax).toBe(19)
106
+ expect(out.total).toBe(119)
107
+ expect(out.regional).toMatchObject({
108
+ status: 'ok',
109
+ destination: { countryCode: 'DE', countryName: 'Germany' },
110
+ zoneName: 'Germany',
111
+ rateName: 'Express',
112
+ codAvailable: false,
113
+ goodsGross: 119,
114
+ shippingNet: 20,
115
+ shippingTax: 3.8,
116
+ shippingGross: 23.8,
117
+ taxRate: 19,
118
+ lines: [{ key: 'l1', productId: 'p1', unitGross: 119, taxRate: 19 }],
119
+ })
120
+ })
121
+
122
+ it('prices an address no zone lists at the store fee and default rate', async () => {
123
+ mount(
124
+ { workflow_cart: CART, workflow_cart_settings: settingsWith(REGIONAL) },
125
+ { billingCountry: 'Brazil', state: '' }
126
+ )
127
+ const out = await runHandler()
128
+ expect(out.regional).toMatchObject({
129
+ status: 'ok',
130
+ zoneName: '',
131
+ rateName: '',
132
+ codAvailable: true,
133
+ shippingNet: 5,
134
+ shippingGross: 5,
135
+ })
136
+ // Brazil has no tax row: the store default applies.
137
+ expect(out.tax).toBe(20)
138
+ })
139
+
140
+ it('prices the shipping address once its block is on the form', async () => {
141
+ mount(
142
+ { workflow_cart: CART, workflow_cart_settings: settingsWith(REGIONAL) },
143
+ { billingCountry: 'Germany', state: '', shippingCountry: 'Brazil', shippingState: '' }
144
+ )
145
+ const out = await runHandler()
146
+ expect(out.regional).toMatchObject({
147
+ status: 'ok',
148
+ zoneName: '',
149
+ shippingGross: 5,
150
+ destination: { countryCode: 'BR' },
151
+ })
152
+ })
153
+
154
+ it('falls back to the single-rate figures while the rows are still loading', async () => {
155
+ mount(
156
+ {
157
+ workflow_cart: CART,
158
+ workflow_cart_settings: settingsWith({ ...REGIONAL, rows: null }),
159
+ },
160
+ { billingCountry: 'Germany', state: '' }
161
+ )
162
+ const out = await runHandler()
163
+ expect(out.tax).toBe(20)
164
+ expect(out.regional).toMatchObject({ status: 'ok', shippingNet: 5, taxRate: 20 })
165
+ })
166
+ })
@@ -0,0 +1,119 @@
1
+ import { generateSharedRuntimeUtilsCode, generateServerSegmentAPIRoute } from '../src'
2
+ import type { WorkflowSegment } from '../src/types'
3
+
4
+ // A node marked `continueOnError` reports a failed RESULT ({ success: false } /
5
+ // { error }) under its id instead of aborting the workflow, so the next
6
+ // if-statement can read `success` and warn. The default is unchanged: a
7
+ // failed result aborts, and so does a handler that throws — with or without
8
+ // the flag, because every provider handler reports failure as a result.
9
+
10
+ interface SharedUtils {
11
+ executeNodes: (
12
+ nodes: unknown[],
13
+ edges: unknown[],
14
+ context: Record<string, unknown>,
15
+ handlers: Record<string, unknown>,
16
+ workflowConfig: unknown,
17
+ callServerSegment: unknown,
18
+ executionId: string
19
+ ) => Promise<void>
20
+ continuesOnError: (node: unknown) => boolean
21
+ }
22
+
23
+ function loadSharedRuntime(): SharedUtils {
24
+ const src = generateSharedRuntimeUtilsCode()
25
+ const wrapper: { exports: Record<string, unknown> } = { exports: {} }
26
+ // eslint-disable-next-line @typescript-eslint/no-implied-eval
27
+ new Function('module', 'exports', src)(wrapper, wrapper.exports)
28
+ return wrapper.exports as unknown as SharedUtils
29
+ }
30
+
31
+ const sendNode = (continueOnError?: boolean) => ({
32
+ id: 'send',
33
+ type: 'email-resend',
34
+ label: 'Send Shipped Email',
35
+ stepNumber: 1,
36
+ config: {
37
+ to: 'jane@example.com',
38
+ ...(continueOnError === undefined ? {} : { continueOnError }),
39
+ },
40
+ })
41
+ const afterNode = { id: 'after', type: 'toast-show', label: 'Toast', stepNumber: 2, config: {} }
42
+ const edges = [{ id: 'e1', source: 'send', target: 'after' }]
43
+
44
+ describe('continuesOnError — classification', () => {
45
+ it('opts in only on an explicit true', () => {
46
+ const utils = loadSharedRuntime()
47
+ expect(utils.continuesOnError(sendNode(true))).toBe(true)
48
+ expect(utils.continuesOnError(sendNode(false))).toBe(false)
49
+ expect(utils.continuesOnError(sendNode())).toBe(false)
50
+ expect(utils.continuesOnError({ id: 'x', type: 'toast-show' })).toBe(false)
51
+ expect(utils.continuesOnError(null)).toBe(false)
52
+ })
53
+ })
54
+
55
+ describe('executeNodes — a refused result on a continueOnError node', () => {
56
+ const refused = { id: '', success: false, error: 'Domain not verified' }
57
+
58
+ it('is published under the node id and the chain goes on', async () => {
59
+ const utils = loadSharedRuntime()
60
+ const ran: string[] = []
61
+ const handlers = {
62
+ 'email-resend': async () => refused,
63
+ 'toast-show': async () => {
64
+ ran.push('toast')
65
+ return { shown: true }
66
+ },
67
+ }
68
+ const context: Record<string, unknown> = {}
69
+ await utils.executeNodes([sendNode(true), afterNode], edges, context, handlers, {}, null, 'x1')
70
+ expect(context.send).toEqual(refused)
71
+ expect(ran).toEqual(['toast'])
72
+ })
73
+
74
+ it('still aborts the chain without the flag', async () => {
75
+ const utils = loadSharedRuntime()
76
+ const ran: string[] = []
77
+ const handlers = {
78
+ 'email-resend': async () => refused,
79
+ 'toast-show': async () => {
80
+ ran.push('toast')
81
+ return { shown: true }
82
+ },
83
+ }
84
+ const context: Record<string, unknown> = {}
85
+ await expect(
86
+ utils.executeNodes([sendNode(), afterNode], edges, context, handlers, {}, null, 'x2')
87
+ ).rejects.toThrow('Domain not verified')
88
+ expect(ran).toEqual([])
89
+ })
90
+
91
+ it('does not swallow a handler that throws', async () => {
92
+ const utils = loadSharedRuntime()
93
+ const handlers = {
94
+ 'email-resend': async () => {
95
+ throw new Error('network down')
96
+ },
97
+ 'toast-show': async () => ({ shown: true }),
98
+ }
99
+ await expect(
100
+ utils.executeNodes([sendNode(true), afterNode], edges, {}, handlers, {}, null, 'x3')
101
+ ).rejects.toThrow('network down')
102
+ })
103
+ })
104
+
105
+ describe('generated server segment route', () => {
106
+ const segment: WorkflowSegment = {
107
+ id: 'server-1',
108
+ env: 'server',
109
+ nodeIds: ['send'],
110
+ nodes: [{ ...sendNode(true), executionEnv: 'server' }],
111
+ edges: [],
112
+ }
113
+ it('gates its fatal-result check on the same predicate', () => {
114
+ const route = generateServerSegmentAPIRoute(segment, 'Send Shipped Email')
115
+ expect(route).toContain(
116
+ 'if (utils.isFatalNodeResult(result) && !utils.continuesOnError(node)) {'
117
+ )
118
+ })
119
+ })
@@ -140,6 +140,66 @@ describe('invoice PDF renderer — delivery fee', () => {
140
140
  expect(withoutFee).not.toContain('<span>Delivery</span>')
141
141
  })
142
142
 
143
+ it('renders the template Delivery row gated on Invoice.hasShipping only when there is a fee', () => {
144
+ // The GUI emits the row's rendering condition as an `expr` reference
145
+ // (`invoiceData?.Invoice?.hasShipping`) compared with the STRING operand
146
+ // 'true'. The renderer resolves that expr to boolean true, so a strict
147
+ // `===` against the operand can never pass — the row was hidden on every
148
+ // invoice that had a fee.
149
+ const textNode = (children: unknown[]) => ({
150
+ type: 'element',
151
+ content: { elementType: 'text', semanticType: 'p', children },
152
+ })
153
+ const templateUidl = {
154
+ name: 'invoice-template',
155
+ node: {
156
+ type: 'element',
157
+ content: {
158
+ elementType: 'container',
159
+ children: [
160
+ {
161
+ type: 'conditional',
162
+ content: {
163
+ reference: { type: 'expr', content: 'invoiceData?.Invoice?.hasShipping' },
164
+ condition: { conditions: [{ operation: '=', operand: 'true' }] },
165
+ node: {
166
+ type: 'element',
167
+ content: {
168
+ elementType: 'container',
169
+ name: 'Delivery Row',
170
+ children: [
171
+ textNode([{ type: 'static', content: 'Delivery:' }]),
172
+ textNode([{ type: 'expr', content: 'invoiceData?.Invoice?.shippingAmount' }]),
173
+ ],
174
+ },
175
+ },
176
+ },
177
+ },
178
+ ],
179
+ },
180
+ },
181
+ }
182
+ const code = generatePdfGeneratorCode(FAKE_SETTINGS, templateUidl, {})
183
+ const moduleObject = { exports: {} as Record<string, unknown> }
184
+ new Function('require', 'module', 'exports', `${code}; return module.exports;`)(
185
+ require,
186
+ moduleObject,
187
+ moduleObject.exports
188
+ )
189
+ const templated = moduleObject.exports as unknown as PdfGeneratorModule
190
+
191
+ const withFee = templated.buildInvoiceHtml({
192
+ ...baseInvoice,
193
+ shippingAmount: 9.99,
194
+ total: 209.99,
195
+ })
196
+ expect(withFee).toContain('Delivery:')
197
+ expect(withFee).toContain('$9.99')
198
+
199
+ const withoutFee = templated.buildInvoiceHtml({ ...baseInvoice, total: 200 })
200
+ expect(withoutFee).not.toContain('Delivery:')
201
+ })
202
+
143
203
  it('fills the {{shippingAmount}} merge token in the invoice email', () => {
144
204
  const data = pdfGenerator.buildDataContext({
145
205
  ...baseInvoice,
@@ -153,3 +213,47 @@ describe('invoice PDF renderer — delivery fee', () => {
153
213
  expect(pdfGenerator.replacePlaceholders('Due: {{totalAmount}}', data)).toBe('Due: $209.99')
154
214
  })
155
215
  })
216
+
217
+ describe('invoice PDF renderer — each line says which VAT rate it was taxed at', () => {
218
+ const pdfGenerator = loadPdfGenerator()
219
+ const regionalInvoice = {
220
+ invoiceNumber: 'INV-0001',
221
+ taxRate: 20,
222
+ taxIncludedInPrice: false,
223
+ subtotal: 247,
224
+ taxAmount: 40,
225
+ shippingAmount: 0,
226
+ total: 287,
227
+ currency: 'RON',
228
+ items: [
229
+ {
230
+ name: 'Kettle',
231
+ quantity: 2,
232
+ unitPrice: 100,
233
+ totalPrice: 200,
234
+ taxRate: 20,
235
+ taxIncluded: false,
236
+ },
237
+ { name: 'Book', quantity: 1, unitPrice: 47, totalPrice: 47, taxRate: 9, taxIncluded: true },
238
+ ],
239
+ }
240
+
241
+ it('binds the per-line rate and VAT beside the net price', () => {
242
+ const products = (
243
+ pdfGenerator.buildInvoiceDataScope(regionalInvoice).invoiceData as unknown as {
244
+ Products: Array<Record<string, string>>
245
+ }
246
+ ).Products
247
+ expect(products.map((product) => product.lineVatRate)).toEqual(['20%', '9%'])
248
+ expect(products[0].lineVatAmount).toContain('40.00')
249
+ expect(products[1].lineVatAmount).toContain('3.88')
250
+ })
251
+
252
+ it('prints the VAT % and VAT columns and a VAT total on the fallback invoice', () => {
253
+ const html = pdfGenerator.buildInvoiceHtml(regionalInvoice)
254
+ expect(html).toContain('>VAT %</th>')
255
+ expect(html).toContain('>20%</td>')
256
+ expect(html).toContain('>9%</td>')
257
+ expect(html).toContain('<span>VAT</span>')
258
+ })
259
+ })
@@ -0,0 +1,126 @@
1
+ /* tslint:disable:function-constructor */
2
+ import { generateInvoiceGenerateRouteCode } from '../src/invoice/api-routes-code'
3
+ import { REGIONAL_INVOICE_TAX_CODE } from '../src/invoice/regional-invoice-tax-code'
4
+ import type { UIDLInvoiceSettings } from '@teleporthq/teleport-types'
5
+
6
+ /**
7
+ * The invoice of an order priced by region (shipping zones + tax
8
+ * jurisdictions) is computed from the rates the order was CHARGED at —
9
+ * `teleport_orders.tax_breakdown` — not from the store's single default rate,
10
+ * and its total must equal `goods + shipping_amount - discount_amount`, the
11
+ * amount the buyer was debited.
12
+ */
13
+
14
+ interface RegionalInvoiceTax {
15
+ taxRate: number
16
+ taxIncluded: boolean
17
+ subtotal: number
18
+ taxAmount: number
19
+ shippingNet: number
20
+ total: number
21
+ itemTax: Array<{ rate: number; included: boolean; amount: number }>
22
+ }
23
+
24
+ const api = new Function(
25
+ REGIONAL_INVOICE_TAX_CODE +
26
+ '\nreturn { parse: parseOrderTaxBreakdown, resolve: resolveRegionalInvoiceTax };'
27
+ )() as {
28
+ parse: (raw: unknown) => Record<string, unknown> | null
29
+ resolve: (
30
+ breakdown: Record<string, unknown>,
31
+ items: Array<Record<string, unknown>>,
32
+ discountAmount: number,
33
+ shippingAmount: number
34
+ ) => RegionalInvoiceTax
35
+ }
36
+
37
+ const cents = (n: number) => Math.round(n * 100) / 100
38
+
39
+ // Germany: food at 7% and tools at 19%, both added on top; 6.00 shipping taxed
40
+ // at 19% → shipping_amount 7.14 on the order.
41
+ const GERMAN_ORDER = {
42
+ taxRate: 19,
43
+ taxIncluded: false,
44
+ taxDecimals: 2,
45
+ shippingTax: 1.14,
46
+ lines: [
47
+ { productId: 'p1', variantId: '', taxRate: 7, taxIncluded: false },
48
+ { productId: 'p2', variantId: 'v2', taxRate: 19, taxIncluded: false },
49
+ ],
50
+ }
51
+ const GERMAN_ITEMS = [
52
+ { productId: 'p1', variantId: null, quantity: 2, unitPrice: 10, totalPrice: 20 },
53
+ { productId: 'p2', variantId: 'v2', quantity: 1, unitPrice: 25.99, totalPrice: 25.99 },
54
+ ]
55
+
56
+ describe('invoice — regional tax breakdown', () => {
57
+ it('reads the breakdown from the TEXT column, and nothing from a legacy order', () => {
58
+ expect(api.parse(JSON.stringify(GERMAN_ORDER))).toMatchObject({ taxRate: 19 })
59
+ expect(api.parse(null)).toBeNull()
60
+ expect(api.parse('not json')).toBeNull()
61
+ expect(api.parse('{"taxRate":19}')).toBeNull()
62
+ })
63
+
64
+ it('taxes each line at its own rate and reconciles with the order total', () => {
65
+ const result = api.resolve(GERMAN_ORDER, GERMAN_ITEMS, 0, 7.14)
66
+ // goods: 21.40 + 30.93 = 52.33, tax 1.40 + 4.94 = 6.34; shipping tax 1.14.
67
+ expect(cents(result.total)).toBe(59.47)
68
+ expect(cents(result.taxAmount)).toBe(7.48)
69
+ expect(cents(result.subtotal)).toBe(45.99)
70
+ // Delivery prints net, so Subtotal + Tax + Delivery is the total.
71
+ expect(cents(result.shippingNet)).toBe(6)
72
+ expect(cents(result.subtotal + result.taxAmount + result.shippingNet)).toBe(59.47)
73
+ expect(result.itemTax).toEqual([
74
+ { rate: 7, included: false, amount: 1.4 },
75
+ { rate: 19, included: false, amount: 4.94 },
76
+ ])
77
+ expect(result.taxRate).toBe(19)
78
+ })
79
+
80
+ it('takes a discount off the goods first, then the shipping, shrinking each tax in proportion', () => {
81
+ const goodsOnly = api.resolve(GERMAN_ORDER, GERMAN_ITEMS, 10.466, 7.14)
82
+ // 52.33 - 10.466 leaves 80% of the goods: 6.34 * 0.8 = 5.072, shipping untouched.
83
+ expect(cents(goodsOnly.total)).toBe(cents(52.33 - 10.466 + 7.14))
84
+ expect(cents(goodsOnly.taxAmount)).toBe(cents(6.34 * 0.8 + 1.14))
85
+
86
+ const intoShipping = api.resolve(GERMAN_ORDER, GERMAN_ITEMS, 55.9, 7.14)
87
+ // The goods are fully discounted and 3.57 of 7.14 shipping is left: half its tax.
88
+ expect(cents(intoShipping.total)).toBe(3.57)
89
+ expect(cents(intoShipping.taxAmount)).toBe(0.57)
90
+ expect(intoShipping.subtotal).toBe(0)
91
+ expect(cents(intoShipping.shippingNet)).toBe(3)
92
+ })
93
+
94
+ it('extracts included tax without adding it, and falls back to the standard rate for an unmatched line', () => {
95
+ const french = api.resolve(
96
+ { taxRate: 20, taxIncluded: true, taxDecimals: 2, shippingTax: 1, lines: [] },
97
+ [{ productId: 'p9', quantity: 1, unitPrice: 25.99, totalPrice: 25.99 }],
98
+ 0,
99
+ 6
100
+ )
101
+ expect(cents(french.total)).toBe(31.99)
102
+ expect(cents(french.taxAmount)).toBe(5.33)
103
+ expect(french.itemTax).toEqual([{ rate: 20, included: true, amount: 4.33 }])
104
+ })
105
+ })
106
+
107
+ describe('/api/invoices/generate — regional tax wiring', () => {
108
+ const route = generateInvoiceGenerateRouteCode({
109
+ invoicePrefix: 'INV-',
110
+ defaultTaxRate: 20,
111
+ showDiscount: false,
112
+ taxIncludedInPrice: false,
113
+ companyDetails: {},
114
+ template: { document: null },
115
+ tables: { invoicesTable: 'teleport_invoices', invoiceItemsTable: 'teleport_invoice_items' },
116
+ emailDelivery: { enabled: false },
117
+ } as unknown as UIDLInvoiceSettings)
118
+
119
+ it('prices from the order breakdown unless the caller names its own rate', () => {
120
+ expect(route).toContain('function resolveRegionalInvoiceTax(')
121
+ expect(route).toContain('var regionalTaxBreakdown = body.taxRate == null')
122
+ expect(route).toContain('parseOrderTaxBreakdown(orderShippingSource.tax_breakdown)')
123
+ expect(route).toContain('taxIncludedInPrice: taxIncludedInPrice,')
124
+ expect(route).toContain('variantId: row.variant_id || row.variantId || null,')
125
+ })
126
+ })
@@ -93,6 +93,12 @@ describe('data-create-item auto-fires order-notification for teleport_orders ins
93
93
  expect(src).toContain('paymentMethod: item.payment_method')
94
94
  })
95
95
 
96
+ it("sends the order's tax breakdown and each line's product ids, so the route prices lines at their rate", () => {
97
+ expect(src).toContain('taxBreakdown: item.tax_breakdown || null')
98
+ expect(src).toContain("product_id: it.productId || it.product_id || ''")
99
+ expect(src).toContain("variant_id: it.variantId || it.variant_id || ''")
100
+ })
101
+
96
102
  it('fires-and-forgets — does NOT await the notification call', () => {
97
103
  // Awaiting an email provider that times out would block the
98
104
  // workflow's success response back to the buyer, leaving the
@@ -0,0 +1,41 @@
1
+ import { generatePaypalWebhookCode } from '../src/webhook-generator'
2
+ import type { UIDLEcommerceSettings, UIDLInvoiceSettings } from '@teleporthq/teleport-types'
3
+
4
+ /**
5
+ * The PayPal webhook tells the order-notification route which order was paid.
6
+ * The capture resource's own id is a PayPal id — it matches no `teleport_orders`
7
+ * row, so a notification keyed on it reached the merchant with no lines at all.
8
+ * The internal id travels in `custom_id`, and the helper that reads it must be
9
+ * emitted whenever notifications are on, not only when invoices are.
10
+ */
11
+ const ecommerce = (orderNotifications: boolean): UIDLEcommerceSettings =>
12
+ ({ orderNotifications } as unknown as UIDLEcommerceSettings)
13
+
14
+ const invoices = (enabled: boolean): UIDLInvoiceSettings =>
15
+ ({ enabled, autoGenerateOnPayment: enabled } as unknown as UIDLInvoiceSettings)
16
+
17
+ describe('PayPal webhook — order notification order id', () => {
18
+ it('sends the internal order id, falling back to the capture id', () => {
19
+ const source = generatePaypalWebhookCode(invoices(false), ecommerce(true))
20
+ expect(source).toContain("orderId: __extractInternalOrderId(resource) || resource.id || ''")
21
+ expect(source).toContain('function __extractInternalOrderId(resource)')
22
+ // Invoices are off: their handler is not emitted, only the id helper both share.
23
+ expect(source).not.toContain('async function handlePaypalInvoiceGeneration')
24
+ })
25
+
26
+ it('emits the helper for invoices alone too, and both handlers when both are on', () => {
27
+ const invoicesOnly = generatePaypalWebhookCode(invoices(true), ecommerce(false))
28
+ expect(invoicesOnly).toContain('function __extractInternalOrderId(resource)')
29
+ expect(invoicesOnly).toContain('async function handlePaypalInvoiceGeneration')
30
+ expect(invoicesOnly).not.toContain('/api/ecommerce/order-notification')
31
+
32
+ const both = generatePaypalWebhookCode(invoices(true), ecommerce(true))
33
+ expect(both.match(/function __extractInternalOrderId\(resource\)/g)).toHaveLength(1)
34
+ expect(both).toContain('/api/ecommerce/order-notification')
35
+ })
36
+
37
+ it('emits neither when neither feature is on', () => {
38
+ const source = generatePaypalWebhookCode(invoices(false), ecommerce(false))
39
+ expect(source).not.toContain('__extractInternalOrderId')
40
+ })
41
+ })