@teleporthq/teleport-plugin-next-workflows 0.43.64 → 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.
- package/__tests__/cart-clear-db-mirror.test.ts +12 -0
- package/__tests__/cart-get-total-regional-pricing.test.ts +166 -0
- package/__tests__/continue-on-error.test.ts +119 -0
- package/__tests__/custom-node-page-route-rewrite.test.ts +109 -0
- package/__tests__/invoice-delivery-fee.test.ts +104 -0
- package/__tests__/invoice-regional-tax.test.ts +126 -0
- package/__tests__/payment-redirect-and-order-notification.test.ts +6 -0
- package/__tests__/paypal-webhook-order-notification.test.ts +41 -0
- package/__tests__/resolve-handler-entry-name.test.ts +25 -0
- package/__tests__/sent-email-log-module.test.ts +381 -0
- package/__tests__/sent-email-log-wiring.test.ts +267 -0
- package/dist/cjs/account-delete-route-generator.d.ts.map +1 -1
- package/dist/cjs/account-delete-route-generator.js +14 -5
- package/dist/cjs/account-delete-route-generator.js.map +1 -1
- package/dist/cjs/api-route-generator.d.ts.map +1 -1
- package/dist/cjs/api-route-generator.js +44 -9
- package/dist/cjs/api-route-generator.js.map +1 -1
- package/dist/cjs/auth-generator.d.ts.map +1 -1
- package/dist/cjs/auth-generator.js +15 -6
- package/dist/cjs/auth-generator.js.map +1 -1
- package/dist/cjs/executor-generator.d.ts.map +1 -1
- package/dist/cjs/executor-generator.js +17 -4
- package/dist/cjs/executor-generator.js.map +1 -1
- package/dist/cjs/index.d.ts +1 -0
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js +3 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/invoice/api-routes-code.d.ts.map +1 -1
- package/dist/cjs/invoice/api-routes-code.js +42 -4
- package/dist/cjs/invoice/api-routes-code.js.map +1 -1
- package/dist/cjs/invoice/email-sender-code.d.ts.map +1 -1
- package/dist/cjs/invoice/email-sender-code.js +58 -29
- package/dist/cjs/invoice/email-sender-code.js.map +1 -1
- package/dist/cjs/invoice/index.d.ts +1 -4
- package/dist/cjs/invoice/index.d.ts.map +1 -1
- package/dist/cjs/invoice/index.js +5 -19
- package/dist/cjs/invoice/index.js.map +1 -1
- package/dist/cjs/invoice/invoice-html-code.d.ts.map +1 -1
- package/dist/cjs/invoice/invoice-html-code.js +31 -8
- package/dist/cjs/invoice/invoice-html-code.js.map +1 -1
- package/dist/cjs/invoice/regional-invoice-tax-code.d.ts +25 -0
- package/dist/cjs/invoice/regional-invoice-tax-code.d.ts.map +1 -0
- package/dist/cjs/invoice/regional-invoice-tax-code.js +116 -0
- package/dist/cjs/invoice/regional-invoice-tax-code.js.map +1 -0
- package/dist/cjs/invoice/resolve-data-source.d.ts +6 -0
- package/dist/cjs/invoice/resolve-data-source.d.ts.map +1 -0
- package/dist/cjs/invoice/resolve-data-source.js +23 -0
- package/dist/cjs/invoice/resolve-data-source.js.map +1 -0
- package/dist/cjs/nodes/analytics/commerce-tracking.d.ts +38 -2
- package/dist/cjs/nodes/analytics/commerce-tracking.d.ts.map +1 -1
- package/dist/cjs/nodes/analytics/commerce-tracking.js +41 -26
- package/dist/cjs/nodes/analytics/commerce-tracking.js.map +1 -1
- package/dist/cjs/nodes/cart/cart-add-item.d.ts.map +1 -1
- package/dist/cjs/nodes/cart/cart-add-item.js +3 -1
- package/dist/cjs/nodes/cart/cart-add-item.js.map +1 -1
- package/dist/cjs/nodes/cart/cart-clear.d.ts.map +1 -1
- package/dist/cjs/nodes/cart/cart-clear.js +6 -0
- package/dist/cjs/nodes/cart/cart-clear.js.map +1 -1
- package/dist/cjs/nodes/cart/cart-get-total.d.ts.map +1 -1
- package/dist/cjs/nodes/cart/cart-get-total.js +81 -5
- package/dist/cjs/nodes/cart/cart-get-total.js.map +1 -1
- package/dist/cjs/nodes/data/data-create-item.d.ts.map +1 -1
- package/dist/cjs/nodes/data/data-create-item.js +8 -0
- package/dist/cjs/nodes/data/data-create-item.js.map +1 -1
- package/dist/cjs/nodes/types.d.ts.map +1 -1
- package/dist/cjs/nodes/types.js +19 -0
- package/dist/cjs/nodes/types.js.map +1 -1
- package/dist/cjs/sent-email-log/index.d.ts +16 -0
- package/dist/cjs/sent-email-log/index.d.ts.map +1 -0
- package/dist/cjs/sent-email-log/index.js +53 -0
- package/dist/cjs/sent-email-log/index.js.map +1 -0
- package/dist/cjs/sent-email-log/sent-email-log-code.d.ts +4 -0
- package/dist/cjs/sent-email-log/sent-email-log-code.d.ts.map +1 -0
- package/dist/cjs/sent-email-log/sent-email-log-code.js +415 -0
- package/dist/cjs/sent-email-log/sent-email-log-code.js.map +1 -0
- package/dist/cjs/sent-email-log/sent-email-log-scope.d.ts +21 -0
- package/dist/cjs/sent-email-log/sent-email-log-scope.d.ts.map +1 -0
- package/dist/cjs/sent-email-log/sent-email-log-scope.js +36 -0
- package/dist/cjs/sent-email-log/sent-email-log-scope.js.map +1 -0
- package/dist/cjs/transactional-email-code.d.ts +12 -1
- package/dist/cjs/transactional-email-code.d.ts.map +1 -1
- package/dist/cjs/transactional-email-code.js +43 -5
- package/dist/cjs/transactional-email-code.js.map +1 -1
- package/dist/cjs/tsconfig.tsbuildinfo +1 -1
- package/dist/cjs/webhook-generator.d.ts.map +1 -1
- package/dist/cjs/webhook-generator.js +9 -2
- package/dist/cjs/webhook-generator.js.map +1 -1
- package/dist/cjs/workflow-project-plugin.d.ts.map +1 -1
- package/dist/cjs/workflow-project-plugin.js +53 -30
- package/dist/cjs/workflow-project-plugin.js.map +1 -1
- package/dist/esm/account-delete-route-generator.d.ts.map +1 -1
- package/dist/esm/account-delete-route-generator.js +15 -6
- package/dist/esm/account-delete-route-generator.js.map +1 -1
- package/dist/esm/api-route-generator.d.ts.map +1 -1
- package/dist/esm/api-route-generator.js +44 -9
- package/dist/esm/api-route-generator.js.map +1 -1
- package/dist/esm/auth-generator.d.ts.map +1 -1
- package/dist/esm/auth-generator.js +15 -6
- package/dist/esm/auth-generator.js.map +1 -1
- package/dist/esm/executor-generator.d.ts.map +1 -1
- package/dist/esm/executor-generator.js +17 -4
- package/dist/esm/executor-generator.js.map +1 -1
- package/dist/esm/index.d.ts +1 -0
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +1 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/invoice/api-routes-code.d.ts.map +1 -1
- package/dist/esm/invoice/api-routes-code.js +42 -4
- package/dist/esm/invoice/api-routes-code.js.map +1 -1
- package/dist/esm/invoice/email-sender-code.d.ts.map +1 -1
- package/dist/esm/invoice/email-sender-code.js +58 -29
- package/dist/esm/invoice/email-sender-code.js.map +1 -1
- package/dist/esm/invoice/index.d.ts +1 -4
- package/dist/esm/invoice/index.d.ts.map +1 -1
- package/dist/esm/invoice/index.js +4 -18
- package/dist/esm/invoice/index.js.map +1 -1
- package/dist/esm/invoice/invoice-html-code.d.ts.map +1 -1
- package/dist/esm/invoice/invoice-html-code.js +31 -8
- package/dist/esm/invoice/invoice-html-code.js.map +1 -1
- package/dist/esm/invoice/regional-invoice-tax-code.d.ts +25 -0
- package/dist/esm/invoice/regional-invoice-tax-code.d.ts.map +1 -0
- package/dist/esm/invoice/regional-invoice-tax-code.js +113 -0
- package/dist/esm/invoice/regional-invoice-tax-code.js.map +1 -0
- package/dist/esm/invoice/resolve-data-source.d.ts +6 -0
- package/dist/esm/invoice/resolve-data-source.d.ts.map +1 -0
- package/dist/esm/invoice/resolve-data-source.js +19 -0
- package/dist/esm/invoice/resolve-data-source.js.map +1 -0
- package/dist/esm/nodes/analytics/commerce-tracking.d.ts +38 -2
- package/dist/esm/nodes/analytics/commerce-tracking.d.ts.map +1 -1
- package/dist/esm/nodes/analytics/commerce-tracking.js +41 -26
- package/dist/esm/nodes/analytics/commerce-tracking.js.map +1 -1
- package/dist/esm/nodes/cart/cart-add-item.d.ts.map +1 -1
- package/dist/esm/nodes/cart/cart-add-item.js +3 -1
- package/dist/esm/nodes/cart/cart-add-item.js.map +1 -1
- package/dist/esm/nodes/cart/cart-clear.d.ts.map +1 -1
- package/dist/esm/nodes/cart/cart-clear.js +6 -0
- package/dist/esm/nodes/cart/cart-clear.js.map +1 -1
- package/dist/esm/nodes/cart/cart-get-total.d.ts.map +1 -1
- package/dist/esm/nodes/cart/cart-get-total.js +81 -5
- package/dist/esm/nodes/cart/cart-get-total.js.map +1 -1
- package/dist/esm/nodes/data/data-create-item.d.ts.map +1 -1
- package/dist/esm/nodes/data/data-create-item.js +8 -0
- package/dist/esm/nodes/data/data-create-item.js.map +1 -1
- package/dist/esm/nodes/types.d.ts.map +1 -1
- package/dist/esm/nodes/types.js +19 -0
- package/dist/esm/nodes/types.js.map +1 -1
- package/dist/esm/sent-email-log/index.d.ts +16 -0
- package/dist/esm/sent-email-log/index.d.ts.map +1 -0
- package/dist/esm/sent-email-log/index.js +45 -0
- package/dist/esm/sent-email-log/index.js.map +1 -0
- package/dist/esm/sent-email-log/sent-email-log-code.d.ts +4 -0
- package/dist/esm/sent-email-log/sent-email-log-code.d.ts.map +1 -0
- package/dist/esm/sent-email-log/sent-email-log-code.js +411 -0
- package/dist/esm/sent-email-log/sent-email-log-code.js.map +1 -0
- package/dist/esm/sent-email-log/sent-email-log-scope.d.ts +21 -0
- package/dist/esm/sent-email-log/sent-email-log-scope.d.ts.map +1 -0
- package/dist/esm/sent-email-log/sent-email-log-scope.js +31 -0
- package/dist/esm/sent-email-log/sent-email-log-scope.js.map +1 -0
- package/dist/esm/transactional-email-code.d.ts +12 -1
- package/dist/esm/transactional-email-code.d.ts.map +1 -1
- package/dist/esm/transactional-email-code.js +42 -4
- package/dist/esm/transactional-email-code.js.map +1 -1
- package/dist/esm/tsconfig.tsbuildinfo +1 -1
- package/dist/esm/webhook-generator.d.ts.map +1 -1
- package/dist/esm/webhook-generator.js +9 -2
- package/dist/esm/webhook-generator.js.map +1 -1
- package/dist/esm/workflow-project-plugin.d.ts.map +1 -1
- package/dist/esm/workflow-project-plugin.js +53 -30
- package/dist/esm/workflow-project-plugin.js.map +1 -1
- package/package.json +4 -4
- package/src/account-delete-route-generator.ts +16 -5
- package/src/api-route-generator.ts +51 -9
- package/src/auth-generator.ts +17 -6
- package/src/executor-generator.ts +17 -4
- package/src/index.ts +1 -0
- package/src/invoice/api-routes-code.ts +48 -4
- package/src/invoice/email-sender-code.ts +58 -29
- package/src/invoice/index.ts +4 -23
- package/src/invoice/invoice-html-code.ts +31 -8
- package/src/invoice/regional-invoice-tax-code.ts +112 -0
- package/src/invoice/resolve-data-source.ts +25 -0
- package/src/nodes/analytics/commerce-tracking.ts +37 -21
- package/src/nodes/cart/cart-add-item.ts +3 -1
- package/src/nodes/cart/cart-clear.ts +6 -0
- package/src/nodes/cart/cart-get-total.ts +94 -5
- package/src/nodes/data/data-create-item.ts +8 -0
- package/src/nodes/types.ts +23 -0
- package/src/sent-email-log/index.ts +57 -0
- package/src/sent-email-log/sent-email-log-code.ts +419 -0
- package/src/sent-email-log/sent-email-log-scope.ts +36 -0
- package/src/transactional-email-code.ts +61 -4
- package/src/webhook-generator.ts +11 -2
- package/src/workflow-project-plugin.ts +52 -29
|
@@ -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
|
+
})
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { NextWorkflowProjectPlugin } from '../src/workflow-project-plugin'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `runBefore` rewrites every `navigation-go-to-page` node's `config.pageId`
|
|
5
|
+
* from a UIDL page id to the real Next.js route. It used to walk only
|
|
6
|
+
* `workflows.workflows`, so the same node inside a shared CUSTOM NODE reached
|
|
7
|
+
* the generated `custom-nodes.js` still holding the raw page id.
|
|
8
|
+
*
|
|
9
|
+
* The runtime then fell through to `targetPage.staticUrl`, which is the
|
|
10
|
+
* mapper's best guess: it is `/home` for the home page even though Next serves
|
|
11
|
+
* it at `/`, and it keeps pointing at a page that has since been deleted. Both
|
|
12
|
+
* end on a 404, and only when the navigation came from a custom node.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const goToPage = (id: string, pageId: string, targetPage?: Record<string, unknown>) => ({
|
|
16
|
+
id,
|
|
17
|
+
type: 'navigation-go-to-page',
|
|
18
|
+
config: { pageId, ...(targetPage ? { targetPage } : {}) },
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
const structure = (uidl: unknown): any => ({
|
|
22
|
+
uidl,
|
|
23
|
+
strategy: { pages: { options: {} } },
|
|
24
|
+
files: new Map(),
|
|
25
|
+
dependencies: {},
|
|
26
|
+
devDependencies: {},
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
const projectUidl = (): any => ({
|
|
30
|
+
name: 'Store',
|
|
31
|
+
globals: { settings: { title: 'Store' }, env: {} },
|
|
32
|
+
root: {
|
|
33
|
+
name: 'Root',
|
|
34
|
+
stateDefinitions: {
|
|
35
|
+
route: {
|
|
36
|
+
type: 'string',
|
|
37
|
+
defaultValue: 'Home',
|
|
38
|
+
values: [
|
|
39
|
+
{ value: 'Home', pageId: 'TQ_home' },
|
|
40
|
+
{ value: 'products-list', pageId: 'TQ_products', pageOptions: { navLink: '/menu' } },
|
|
41
|
+
{ value: '404 - Not Found', pageOptions: { fallback: true } },
|
|
42
|
+
],
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
node: { type: 'element', content: { elementType: 'container', children: [] } },
|
|
46
|
+
},
|
|
47
|
+
components: {},
|
|
48
|
+
workflows: {
|
|
49
|
+
workflows: {
|
|
50
|
+
wf1: {
|
|
51
|
+
id: 'wf1',
|
|
52
|
+
name: 'Go home',
|
|
53
|
+
trigger: { type: 'event-element-clicked', scope: 'page', nodeId: 't1' },
|
|
54
|
+
nodes: [goToPage('n1', 'TQ_home', { pageId: 'TQ_home', staticUrl: '/home' })],
|
|
55
|
+
edges: [],
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
customNodes: {
|
|
59
|
+
cn1: {
|
|
60
|
+
id: 'cn1',
|
|
61
|
+
name: 'Require login',
|
|
62
|
+
nodes: [
|
|
63
|
+
goToPage('n2', 'TQ_home', { pageId: 'TQ_home', staticUrl: '/home' }),
|
|
64
|
+
goToPage('n3', 'TQ_products', { pageId: 'TQ_products', staticUrl: '/products-list' }),
|
|
65
|
+
],
|
|
66
|
+
edges: [],
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
describe('runBefore — navigation-go-to-page page ids inside custom nodes', () => {
|
|
73
|
+
it('rewrites custom-node page ids to the real route, like workflow nodes', async () => {
|
|
74
|
+
const uidl = projectUidl()
|
|
75
|
+
await new NextWorkflowProjectPlugin().runBefore(structure(uidl))
|
|
76
|
+
|
|
77
|
+
const customNodes = uidl.workflows.customNodes.cn1.nodes
|
|
78
|
+
// The home page's staticUrl is '/home', but Next serves it at '/'. Leaving
|
|
79
|
+
// the raw id made the runtime prefer staticUrl and 404.
|
|
80
|
+
expect(customNodes[0].config.pageId).toBe('/')
|
|
81
|
+
expect(customNodes[1].config.pageId).toBe('/menu')
|
|
82
|
+
// Unchanged behaviour for the workflow that already worked.
|
|
83
|
+
expect(uidl.workflows.workflows.wf1.nodes[0].config.pageId).toBe('/')
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
// The reported scenario: every page was deleted while e-commerce stayed on,
|
|
87
|
+
// so the route map is empty and the custom node still points at a page id
|
|
88
|
+
// that resolves to nothing. It gets the same default-route treatment a
|
|
89
|
+
// workflow node has always had, instead of shipping the raw id.
|
|
90
|
+
it('sends a custom-node reference to a deleted page to the default route', async () => {
|
|
91
|
+
const uidl = projectUidl()
|
|
92
|
+
uidl.root.stateDefinitions.route = {
|
|
93
|
+
type: 'string',
|
|
94
|
+
defaultValue: '404 - Not Found',
|
|
95
|
+
values: [{ value: '404 - Not Found', pageOptions: { fallback: true } }],
|
|
96
|
+
}
|
|
97
|
+
uidl.workflows.customNodes.cn1.nodes = [goToPage('n4', 'TQ_deleted')]
|
|
98
|
+
await new NextWorkflowProjectPlugin().runBefore(structure(uidl))
|
|
99
|
+
|
|
100
|
+
expect(uidl.workflows.customNodes.cn1.nodes[0].config.pageId).toBe('/')
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it('does not throw for a project that has custom nodes but no workflows', async () => {
|
|
104
|
+
const uidl = projectUidl()
|
|
105
|
+
delete uidl.workflows.workflows
|
|
106
|
+
await expect(new NextWorkflowProjectPlugin().runBefore(structure(uidl))).resolves.toBeDefined()
|
|
107
|
+
expect(uidl.workflows.customNodes.cn1.nodes[0].config.pageId).toBe('/')
|
|
108
|
+
})
|
|
109
|
+
})
|
|
@@ -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
|