@teleporthq/teleport-plugin-next-workflows 0.43.74 → 0.44.0
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__/booking-calendar-feed-route.test.ts +494 -0
- package/__tests__/custom-node-result-taken-branch.test.ts +107 -0
- package/__tests__/data-api-date-parser.test.ts +45 -0
- package/__tests__/emit-custom-event-channel.test.ts +13 -0
- package/__tests__/invoice-billing-details.test.ts +278 -0
- package/__tests__/middleware-admin-panel-roles.test.ts +127 -0
- package/__tests__/page-arrival-remount.test.ts +247 -0
- package/__tests__/payment-driver-stripe.test.ts +23 -0
- package/__tests__/payment-drivers-module.test.ts +19 -0
- package/__tests__/payment-subscription-nodes.test.ts +8 -0
- package/__tests__/raw-query-error-code.test.ts +47 -0
- package/__tests__/runtime-template-token-resolution.test.ts +16 -0
- package/__tests__/segment-reply-changed-keys.test.ts +31 -0
- package/__tests__/segment-route-skipped-branch.test.ts +101 -0
- package/dist/cjs/api-route-generator.d.ts.map +1 -1
- package/dist/cjs/api-route-generator.js +23 -13
- 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 +75 -27
- package/dist/cjs/auth-generator.js.map +1 -1
- package/dist/cjs/booking-calendar-feed-route-generator.d.ts +28 -0
- package/dist/cjs/booking-calendar-feed-route-generator.d.ts.map +1 -0
- package/dist/cjs/booking-calendar-feed-route-generator.js +296 -0
- package/dist/cjs/booking-calendar-feed-route-generator.js.map +1 -0
- package/dist/cjs/data-api-route-generator.d.ts +6 -8
- package/dist/cjs/data-api-route-generator.d.ts.map +1 -1
- package/dist/cjs/data-api-route-generator.js +12 -16
- package/dist/cjs/data-api-route-generator.js.map +1 -1
- package/dist/cjs/executor-generator.d.ts.map +1 -1
- package/dist/cjs/executor-generator.js +30 -2
- package/dist/cjs/executor-generator.js.map +1 -1
- package/dist/cjs/index.d.ts +1 -1
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js +2 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/invoice/invoice-assembly-code.d.ts.map +1 -1
- package/dist/cjs/invoice/invoice-assembly-code.js +46 -6
- package/dist/cjs/invoice/invoice-assembly-code.js.map +1 -1
- package/dist/cjs/invoice/invoice-html-code.d.ts.map +1 -1
- package/dist/cjs/invoice/invoice-html-code.js +10 -2
- package/dist/cjs/invoice/invoice-html-code.js.map +1 -1
- package/dist/cjs/nodes/data/data-raw-query.d.ts.map +1 -1
- package/dist/cjs/nodes/data/data-raw-query.js +11 -1
- package/dist/cjs/nodes/data/data-raw-query.js.map +1 -1
- package/dist/cjs/nodes/general/general-custom-js.d.ts.map +1 -1
- package/dist/cjs/nodes/general/general-custom-js.js +11 -0
- package/dist/cjs/nodes/general/general-custom-js.js.map +1 -1
- package/dist/cjs/nodes/general/general-emit-custom-event.d.ts.map +1 -1
- package/dist/cjs/nodes/general/general-emit-custom-event.js +3 -1
- package/dist/cjs/nodes/general/general-emit-custom-event.js.map +1 -1
- package/dist/cjs/page-arrival.d.ts +10 -0
- package/dist/cjs/page-arrival.d.ts.map +1 -0
- package/dist/cjs/page-arrival.js +86 -0
- package/dist/cjs/page-arrival.js.map +1 -0
- package/dist/cjs/payments/drivers/stripe-driver-code.d.ts.map +1 -1
- package/dist/cjs/payments/drivers/stripe-driver-code.js +11 -0
- package/dist/cjs/payments/drivers/stripe-driver-code.js.map +1 -1
- package/dist/cjs/payments/index.d.ts +10 -4
- package/dist/cjs/payments/index.d.ts.map +1 -1
- package/dist/cjs/payments/index.js +18 -7
- package/dist/cjs/payments/index.js.map +1 -1
- package/dist/cjs/tsconfig.tsbuildinfo +1 -1
- package/dist/cjs/workflow-component-plugin.d.ts.map +1 -1
- package/dist/cjs/workflow-component-plugin.js +35 -149
- package/dist/cjs/workflow-component-plugin.js.map +1 -1
- package/dist/cjs/workflow-project-plugin.d.ts.map +1 -1
- package/dist/cjs/workflow-project-plugin.js +14 -4
- package/dist/cjs/workflow-project-plugin.js.map +1 -1
- package/dist/cjs/workflow-scope.d.ts +19 -0
- package/dist/cjs/workflow-scope.d.ts.map +1 -0
- package/dist/cjs/workflow-scope.js +147 -0
- package/dist/cjs/workflow-scope.js.map +1 -0
- package/dist/esm/api-route-generator.d.ts.map +1 -1
- package/dist/esm/api-route-generator.js +23 -13
- 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 +75 -27
- package/dist/esm/auth-generator.js.map +1 -1
- package/dist/esm/booking-calendar-feed-route-generator.d.ts +28 -0
- package/dist/esm/booking-calendar-feed-route-generator.d.ts.map +1 -0
- package/dist/esm/booking-calendar-feed-route-generator.js +291 -0
- package/dist/esm/booking-calendar-feed-route-generator.js.map +1 -0
- package/dist/esm/data-api-route-generator.d.ts +6 -8
- package/dist/esm/data-api-route-generator.d.ts.map +1 -1
- package/dist/esm/data-api-route-generator.js +13 -17
- package/dist/esm/data-api-route-generator.js.map +1 -1
- package/dist/esm/executor-generator.d.ts.map +1 -1
- package/dist/esm/executor-generator.js +30 -2
- package/dist/esm/executor-generator.js.map +1 -1
- package/dist/esm/index.d.ts +1 -1
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +1 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/invoice/invoice-assembly-code.d.ts.map +1 -1
- package/dist/esm/invoice/invoice-assembly-code.js +46 -6
- package/dist/esm/invoice/invoice-assembly-code.js.map +1 -1
- package/dist/esm/invoice/invoice-html-code.d.ts.map +1 -1
- package/dist/esm/invoice/invoice-html-code.js +10 -2
- package/dist/esm/invoice/invoice-html-code.js.map +1 -1
- package/dist/esm/nodes/data/data-raw-query.d.ts.map +1 -1
- package/dist/esm/nodes/data/data-raw-query.js +11 -1
- package/dist/esm/nodes/data/data-raw-query.js.map +1 -1
- package/dist/esm/nodes/general/general-custom-js.d.ts.map +1 -1
- package/dist/esm/nodes/general/general-custom-js.js +11 -0
- package/dist/esm/nodes/general/general-custom-js.js.map +1 -1
- package/dist/esm/nodes/general/general-emit-custom-event.d.ts.map +1 -1
- package/dist/esm/nodes/general/general-emit-custom-event.js +3 -1
- package/dist/esm/nodes/general/general-emit-custom-event.js.map +1 -1
- package/dist/esm/page-arrival.d.ts +10 -0
- package/dist/esm/page-arrival.d.ts.map +1 -0
- package/dist/esm/page-arrival.js +81 -0
- package/dist/esm/page-arrival.js.map +1 -0
- package/dist/esm/payments/drivers/stripe-driver-code.d.ts.map +1 -1
- package/dist/esm/payments/drivers/stripe-driver-code.js +11 -0
- package/dist/esm/payments/drivers/stripe-driver-code.js.map +1 -1
- package/dist/esm/payments/index.d.ts +10 -4
- package/dist/esm/payments/index.d.ts.map +1 -1
- package/dist/esm/payments/index.js +16 -6
- package/dist/esm/payments/index.js.map +1 -1
- package/dist/esm/tsconfig.tsbuildinfo +1 -1
- package/dist/esm/workflow-component-plugin.d.ts.map +1 -1
- package/dist/esm/workflow-component-plugin.js +32 -146
- package/dist/esm/workflow-component-plugin.js.map +1 -1
- package/dist/esm/workflow-project-plugin.d.ts.map +1 -1
- package/dist/esm/workflow-project-plugin.js +14 -4
- package/dist/esm/workflow-project-plugin.js.map +1 -1
- package/dist/esm/workflow-scope.d.ts +19 -0
- package/dist/esm/workflow-scope.d.ts.map +1 -0
- package/dist/esm/workflow-scope.js +142 -0
- package/dist/esm/workflow-scope.js.map +1 -0
- package/package.json +4 -4
- package/src/api-route-generator.ts +23 -13
- package/src/auth-generator.ts +80 -25
- package/src/booking-calendar-feed-route-generator.ts +293 -0
- package/src/data-api-route-generator.ts +14 -19
- package/src/executor-generator.ts +30 -2
- package/src/index.ts +1 -0
- package/src/invoice/invoice-assembly-code.ts +46 -6
- package/src/invoice/invoice-html-code.ts +10 -2
- package/src/nodes/data/data-raw-query.ts +11 -1
- package/src/nodes/general/general-custom-js.ts +10 -0
- package/src/nodes/general/general-emit-custom-event.ts +3 -1
- package/src/page-arrival.ts +115 -0
- package/src/payments/drivers/stripe-driver-code.ts +11 -0
- package/src/payments/index.ts +18 -5
- package/src/workflow-component-plugin.ts +43 -188
- package/src/workflow-project-plugin.ts +15 -4
- package/src/workflow-scope.ts +184 -0
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
/* tslint:disable:function-constructor */
|
|
2
|
+
import { generateInvoiceGenerateRouteCode } from '../src/invoice/api-routes-code'
|
|
3
|
+
import { generatePdfGeneratorCode } from '../src/invoice/pdf-generator-code'
|
|
4
|
+
import type { UIDLInvoiceSettings } from '@teleporthq/teleport-types'
|
|
5
|
+
import {
|
|
6
|
+
INVOICE_TEST_APP_SECRET,
|
|
7
|
+
createInvoiceRouteRequire,
|
|
8
|
+
evaluateEmittedModule,
|
|
9
|
+
serverCallHeaders,
|
|
10
|
+
} from './_helpers/load-invoice-route'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The buyer's own invoice details on the invoice. An order whose buyer gave
|
|
14
|
+
* them (a booking's "Add invoice details") carries the address in its parts
|
|
15
|
+
* and the tax or VAT number in its metadata: the invoice is made out to them
|
|
16
|
+
* — never mixed with a shipping address — and names the VAT number. A booking
|
|
17
|
+
* order also records the tax its final price includes, so its invoice extracts
|
|
18
|
+
* it instead of adding the store's rate on top. And the template's sample
|
|
19
|
+
* customer never fills a part the buyer left out. Every piece below is the
|
|
20
|
+
* EMITTED code, run.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const settingsWith = (overrides: Record<string, unknown>) =>
|
|
24
|
+
({
|
|
25
|
+
invoicePrefix: 'INV-',
|
|
26
|
+
defaultTaxRate: 19,
|
|
27
|
+
showDiscount: false,
|
|
28
|
+
taxIncludedInPrice: false,
|
|
29
|
+
companyDetails: {},
|
|
30
|
+
template: { document: null as unknown },
|
|
31
|
+
tables: { invoicesTable: 'teleport_invoices', invoiceItemsTable: 'teleport_invoice_items' },
|
|
32
|
+
emailDelivery: { enabled: false },
|
|
33
|
+
...overrides,
|
|
34
|
+
} as unknown as UIDLInvoiceSettings)
|
|
35
|
+
|
|
36
|
+
type Row = Record<string, unknown>
|
|
37
|
+
|
|
38
|
+
const BOOKING_ORDER: Row = {
|
|
39
|
+
id: 'order-bk',
|
|
40
|
+
order_number: 'ORD-1',
|
|
41
|
+
currency: 'EUR',
|
|
42
|
+
total_amount: 238,
|
|
43
|
+
shipping_amount: 0,
|
|
44
|
+
billing_name: 'Acme SRL',
|
|
45
|
+
billing_email: 'ana@example.com',
|
|
46
|
+
billing_address: 'Str. Lunga 1, 400000 Cluj, Cluj, Romania',
|
|
47
|
+
shipping_city: 'Shipped-to city',
|
|
48
|
+
tax_breakdown: JSON.stringify({
|
|
49
|
+
taxRate: 19,
|
|
50
|
+
taxIncluded: true,
|
|
51
|
+
taxDecimals: 2,
|
|
52
|
+
shippingTax: 0,
|
|
53
|
+
lines: [{ productId: 'room', variantId: '', taxRate: 19, taxIncluded: true }],
|
|
54
|
+
}),
|
|
55
|
+
metadata: JSON.stringify({
|
|
56
|
+
bookingId: 'b1',
|
|
57
|
+
bookingReference: 'BK-1',
|
|
58
|
+
billingTaxId: 'RO12345678',
|
|
59
|
+
billingStreet: 'Str. Lunga 1',
|
|
60
|
+
billingCity: 'Cluj',
|
|
61
|
+
billingPostalCode: '400000',
|
|
62
|
+
billingRegion: 'Cluj',
|
|
63
|
+
billingCountry: 'Romania',
|
|
64
|
+
}),
|
|
65
|
+
}
|
|
66
|
+
const BOOKING_ITEMS: Row[] = [
|
|
67
|
+
{
|
|
68
|
+
product_id: 'room',
|
|
69
|
+
product_name: 'Double room',
|
|
70
|
+
quantity: 1,
|
|
71
|
+
unit_price: 238,
|
|
72
|
+
total_price: 238,
|
|
73
|
+
},
|
|
74
|
+
]
|
|
75
|
+
|
|
76
|
+
async function runHandler(
|
|
77
|
+
settings: UIDLInvoiceSettings,
|
|
78
|
+
order: Row,
|
|
79
|
+
items: Row[],
|
|
80
|
+
body: Row
|
|
81
|
+
): Promise<{ status: number; invoice: Row }> {
|
|
82
|
+
const code = generateInvoiceGenerateRouteCode(settings)
|
|
83
|
+
let invoice: Row = {}
|
|
84
|
+
const handler = evaluateEmittedModule<(req: unknown, res: unknown) => Promise<void>>(
|
|
85
|
+
code,
|
|
86
|
+
createInvoiceRouteRequire(settings, {
|
|
87
|
+
dataAccess: {
|
|
88
|
+
getOrderWithItems: async (orderId: string) =>
|
|
89
|
+
orderId === order.id ? { order, items } : null,
|
|
90
|
+
reserveInvoice: async (invoiceData: Row) => {
|
|
91
|
+
invoice = invoiceData
|
|
92
|
+
return { invoice: invoiceData, created: true }
|
|
93
|
+
},
|
|
94
|
+
storeInvoicePdf: async () => ({}),
|
|
95
|
+
updateInvoice: async () => ({}),
|
|
96
|
+
},
|
|
97
|
+
pdfGenerator: {
|
|
98
|
+
generateInvoicePdf: async () => Buffer.from('%PDF-1.4 fake'),
|
|
99
|
+
COMPANY_DETAILS: {},
|
|
100
|
+
},
|
|
101
|
+
})
|
|
102
|
+
)
|
|
103
|
+
let status = 0
|
|
104
|
+
const res = {
|
|
105
|
+
status(statusCode: number) {
|
|
106
|
+
status = statusCode
|
|
107
|
+
return this
|
|
108
|
+
},
|
|
109
|
+
json() {
|
|
110
|
+
return this
|
|
111
|
+
},
|
|
112
|
+
setHeader() {
|
|
113
|
+
/* unused */
|
|
114
|
+
},
|
|
115
|
+
end() {
|
|
116
|
+
/* unused */
|
|
117
|
+
},
|
|
118
|
+
}
|
|
119
|
+
await handler({ method: 'POST', headers: serverCallHeaders(), body }, res)
|
|
120
|
+
return { status, invoice }
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
interface PdfGeneratorModule {
|
|
124
|
+
buildInvoiceDataScope: (invoiceData: Row) => { invoiceData: Record<string, Row> }
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function loadPdfGenerator(
|
|
128
|
+
settings: UIDLInvoiceSettings,
|
|
129
|
+
templateUidl: unknown
|
|
130
|
+
): PdfGeneratorModule {
|
|
131
|
+
const code = generatePdfGeneratorCode(settings, templateUidl, {})
|
|
132
|
+
const moduleObject = { exports: {} as Record<string, unknown> }
|
|
133
|
+
new Function('require', 'module', 'exports', `${code}; return module.exports;`)(
|
|
134
|
+
require,
|
|
135
|
+
moduleObject,
|
|
136
|
+
moduleObject.exports
|
|
137
|
+
)
|
|
138
|
+
return moduleObject.exports as unknown as PdfGeneratorModule
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
describe('/api/invoices/generate — the buyer’s own invoice details', () => {
|
|
142
|
+
const originalFetch = globalThis.fetch
|
|
143
|
+
const originalConnString = process.env.TELEPORT_DB_CONNECTION_STRING
|
|
144
|
+
const originalDatabaseUrl = process.env.DATABASE_URL
|
|
145
|
+
const originalAppSecret = process.env.NEXTAUTH_SECRET
|
|
146
|
+
|
|
147
|
+
beforeAll(() => {
|
|
148
|
+
globalThis.fetch = (async () => {
|
|
149
|
+
throw new Error('runtime storage not configured in tests')
|
|
150
|
+
}) as unknown as typeof fetch
|
|
151
|
+
delete process.env.TELEPORT_DB_CONNECTION_STRING
|
|
152
|
+
delete process.env.DATABASE_URL
|
|
153
|
+
process.env.NEXTAUTH_SECRET = INVOICE_TEST_APP_SECRET
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
afterAll(() => {
|
|
157
|
+
globalThis.fetch = originalFetch
|
|
158
|
+
if (originalAppSecret === undefined) {
|
|
159
|
+
delete process.env.NEXTAUTH_SECRET
|
|
160
|
+
} else {
|
|
161
|
+
process.env.NEXTAUTH_SECRET = originalAppSecret
|
|
162
|
+
}
|
|
163
|
+
if (originalConnString !== undefined) {
|
|
164
|
+
process.env.TELEPORT_DB_CONNECTION_STRING = originalConnString
|
|
165
|
+
}
|
|
166
|
+
if (originalDatabaseUrl !== undefined) {
|
|
167
|
+
process.env.DATABASE_URL = originalDatabaseUrl
|
|
168
|
+
}
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
it('makes the invoice out to the address parts and VAT number the order carries', async () => {
|
|
172
|
+
const run = await runHandler(settingsWith({}), BOOKING_ORDER, BOOKING_ITEMS, {
|
|
173
|
+
orderId: 'order-bk',
|
|
174
|
+
})
|
|
175
|
+
expect(run.status).toBe(200)
|
|
176
|
+
expect(run.invoice).toMatchObject({
|
|
177
|
+
customerName: 'Acme SRL',
|
|
178
|
+
customerAddress: 'Str. Lunga 1',
|
|
179
|
+
customerCity: 'Cluj',
|
|
180
|
+
customerZip: '400000',
|
|
181
|
+
customerState: 'Cluj',
|
|
182
|
+
customerCountry: 'Romania',
|
|
183
|
+
customerVat: 'RO12345678',
|
|
184
|
+
})
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
it('extracts the tax a booking price includes, so the invoice totals what was charged', async () => {
|
|
188
|
+
// The store adds 19% on top of its products; the booking's price already holds it.
|
|
189
|
+
const run = await runHandler(settingsWith({}), BOOKING_ORDER, BOOKING_ITEMS, {
|
|
190
|
+
orderId: 'order-bk',
|
|
191
|
+
})
|
|
192
|
+
expect(run.invoice.total).toBe(238)
|
|
193
|
+
expect(run.invoice.taxAmount).toBeCloseTo(38, 2)
|
|
194
|
+
expect(run.invoice.subtotal).toBeCloseTo(200, 2)
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
it('keeps the order’s own billing and shipping address when it carries no parts', async () => {
|
|
198
|
+
const order = {
|
|
199
|
+
...BOOKING_ORDER,
|
|
200
|
+
tax_breakdown: null,
|
|
201
|
+
billing_address: 'Main St 5',
|
|
202
|
+
shipping_city: 'Springfield',
|
|
203
|
+
shipping_country: 'United States',
|
|
204
|
+
metadata: JSON.stringify({ billingTaxId: 'US-99' }),
|
|
205
|
+
}
|
|
206
|
+
const run = await runHandler(settingsWith({ defaultTaxRate: 0 }), order, BOOKING_ITEMS, {
|
|
207
|
+
orderId: 'order-bk',
|
|
208
|
+
})
|
|
209
|
+
expect(run.invoice).toMatchObject({
|
|
210
|
+
customerAddress: 'Main St 5',
|
|
211
|
+
customerCity: 'Springfield',
|
|
212
|
+
customerCountry: 'United States',
|
|
213
|
+
customerVat: 'US-99',
|
|
214
|
+
})
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
it('reads no metadata that is not a JSON object, and lets the caller’s VAT win', async () => {
|
|
218
|
+
for (const metadata of ['not json', '[1,2]', null]) {
|
|
219
|
+
const run = await runHandler(
|
|
220
|
+
settingsWith({ defaultTaxRate: 0 }),
|
|
221
|
+
{ ...BOOKING_ORDER, metadata, tax_breakdown: null },
|
|
222
|
+
BOOKING_ITEMS,
|
|
223
|
+
{ orderId: 'order-bk' }
|
|
224
|
+
)
|
|
225
|
+
expect([metadata, run.status]).toEqual([metadata, 200])
|
|
226
|
+
expect([metadata, run.invoice.customerVat, run.invoice.customerCity]).toEqual([
|
|
227
|
+
metadata,
|
|
228
|
+
'',
|
|
229
|
+
'Shipped-to city',
|
|
230
|
+
])
|
|
231
|
+
}
|
|
232
|
+
const stated = await runHandler(settingsWith({}), BOOKING_ORDER, BOOKING_ITEMS, {
|
|
233
|
+
orderId: 'order-bk',
|
|
234
|
+
customerVat: 'EDITED-1',
|
|
235
|
+
})
|
|
236
|
+
expect(stated.invoice.customerVat).toBe('EDITED-1')
|
|
237
|
+
})
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
describe('invoice PDF renderer — the buyer, never the template’s sample', () => {
|
|
241
|
+
const settings = settingsWith({})
|
|
242
|
+
const templated = loadPdfGenerator(settings, {
|
|
243
|
+
name: 'invoice-template',
|
|
244
|
+
node: {
|
|
245
|
+
type: 'element',
|
|
246
|
+
content: {
|
|
247
|
+
elementType: 'context',
|
|
248
|
+
attrs: {
|
|
249
|
+
Customer: {
|
|
250
|
+
type: 'static',
|
|
251
|
+
content: { name: 'Jane Smith', city: 'San Francisco', state: 'CA', vat: 'US1' },
|
|
252
|
+
},
|
|
253
|
+
Payment: { type: 'static', content: { method: 'Card', intentId: 'pi_sample' } },
|
|
254
|
+
},
|
|
255
|
+
children: [],
|
|
256
|
+
},
|
|
257
|
+
},
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
it('leaves out what the buyer did not give, and flags the lines that show', () => {
|
|
261
|
+
const { invoiceData } = templated.buildInvoiceDataScope({
|
|
262
|
+
customerName: 'Acme SRL',
|
|
263
|
+
customerVat: 'RO12345678',
|
|
264
|
+
currency: 'EUR',
|
|
265
|
+
items: [],
|
|
266
|
+
})
|
|
267
|
+
expect(invoiceData.Customer).toMatchObject({
|
|
268
|
+
name: 'Acme SRL',
|
|
269
|
+
city: '',
|
|
270
|
+
state: '',
|
|
271
|
+
vat: 'RO12345678',
|
|
272
|
+
hasVat: 'true',
|
|
273
|
+
hasZip: 'false',
|
|
274
|
+
hasCountry: 'false',
|
|
275
|
+
})
|
|
276
|
+
expect(invoiceData.Payment.intentId).toBe('')
|
|
277
|
+
})
|
|
278
|
+
})
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { UIDLAuthentication } from '@teleporthq/teleport-types'
|
|
2
|
+
import { generateMiddlewareFile } from '../src/auth-generator'
|
|
3
|
+
import {
|
|
4
|
+
compileGeneratedMiddleware,
|
|
5
|
+
extractProtectedRoutes,
|
|
6
|
+
} from './_helpers/run-generated-middleware'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* An admin panel whose folder admits admins only, some of whose pages (the
|
|
10
|
+
* dashboard first among them) also admit staff, whose built-in entities stay
|
|
11
|
+
* admin only, and whose project redirects the home page into the dashboard
|
|
12
|
+
* (`homeRedirect`). Two things must follow from the ADMIN FOLDER's protection,
|
|
13
|
+
* never from whichever page happens to come first:
|
|
14
|
+
* - the `/admin` catch-all, which guards `/admin` itself and every admin page
|
|
15
|
+
* the middleware has no entry for (one an owner added without protection);
|
|
16
|
+
* - where a role denial lands: never `/` when `/` leads straight back into a
|
|
17
|
+
* page the visitor may not open — that is a redirect loop.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const page = (route: string, allowedRoles: string[]) => ({
|
|
21
|
+
requiresAuth: true,
|
|
22
|
+
allowedRoles,
|
|
23
|
+
pageName: route,
|
|
24
|
+
route,
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
const panelAuth = (homeRedirect?: string): UIDLAuthentication => ({
|
|
28
|
+
enabled: true,
|
|
29
|
+
dataSourceId: 'ds-1',
|
|
30
|
+
dataSourceType: 'teleport',
|
|
31
|
+
passwordAuthEnabled: true,
|
|
32
|
+
providers: [],
|
|
33
|
+
roles: ['admin', 'staff', 'user'],
|
|
34
|
+
tables: {},
|
|
35
|
+
// The dashboard is the first admin route, as the GUI emits it.
|
|
36
|
+
pageProtection: {
|
|
37
|
+
'p-dashboard': page('/admin/dashboard', ['admin', 'staff']),
|
|
38
|
+
'p-tasks': page('/admin/tasks', ['admin', 'staff']),
|
|
39
|
+
'p-users': page('/admin/users', ['admin']),
|
|
40
|
+
},
|
|
41
|
+
folderProtection: {
|
|
42
|
+
'f-admin': {
|
|
43
|
+
requiresAuth: true,
|
|
44
|
+
allowedRoles: ['admin'],
|
|
45
|
+
folderName: 'admin-panel',
|
|
46
|
+
parentId: 'pages',
|
|
47
|
+
children: { 'p-dashboard': 'page', 'f-tasks': 'folder', 'f-users': 'folder' },
|
|
48
|
+
},
|
|
49
|
+
'f-tasks': {
|
|
50
|
+
requiresAuth: true,
|
|
51
|
+
allowedRoles: ['admin', 'staff'],
|
|
52
|
+
folderName: 'tasks',
|
|
53
|
+
parentId: 'f-admin',
|
|
54
|
+
children: { 'p-tasks': 'page' },
|
|
55
|
+
},
|
|
56
|
+
'f-users': {
|
|
57
|
+
requiresAuth: true,
|
|
58
|
+
allowedRoles: ['admin'],
|
|
59
|
+
folderName: 'users',
|
|
60
|
+
parentId: 'f-admin',
|
|
61
|
+
children: { 'p-users': 'page' },
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
authPages: {
|
|
65
|
+
signIn: { pageId: 'p-sign-in', pageName: 'sign-in', route: '/auth/sign-in' },
|
|
66
|
+
},
|
|
67
|
+
callbackBaseUrl: '/api/auth/callback',
|
|
68
|
+
envKeys: {},
|
|
69
|
+
customUserProperties: [],
|
|
70
|
+
...(homeRedirect ? { homeRedirect } : {}),
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
const ORIGIN = 'https://panel.test'
|
|
74
|
+
|
|
75
|
+
describe('the /admin catch-all', () => {
|
|
76
|
+
it("admits the admin folder's roles, not the first admin page's", () => {
|
|
77
|
+
const routes = extractProtectedRoutes(generateMiddlewareFile(panelAuth('/admin/dashboard')))
|
|
78
|
+
expect(routes['/admin']).toEqual({ requiresAuth: true, allowedRoles: ['admin'] })
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('keeps staff out of an admin page the middleware has no entry for', async () => {
|
|
82
|
+
const run = compileGeneratedMiddleware(panelAuth('/admin/dashboard'))
|
|
83
|
+
const staff = { id: 's1', role: 'staff' }
|
|
84
|
+
expect(await run('/admin/reports', { sessionUser: staff, origin: ORIGIN })).toMatchObject({
|
|
85
|
+
kind: 'redirect',
|
|
86
|
+
})
|
|
87
|
+
expect(await run('/admin/tasks', { sessionUser: staff, origin: ORIGIN })).toEqual({
|
|
88
|
+
kind: 'next',
|
|
89
|
+
})
|
|
90
|
+
expect(
|
|
91
|
+
await run('/admin/reports', { sessionUser: { id: 'a1', role: 'admin' }, origin: ORIGIN })
|
|
92
|
+
).toEqual({ kind: 'next' })
|
|
93
|
+
})
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
describe('a role denial when the home page redirects into the admin panel', () => {
|
|
97
|
+
const run = compileGeneratedMiddleware(panelAuth('/admin/dashboard'))
|
|
98
|
+
const signInLocation = (callbackUrl: string) =>
|
|
99
|
+
`${ORIGIN}/auth/sign-in?callbackUrl=${encodeURIComponent(callbackUrl)}&error=forbidden`
|
|
100
|
+
|
|
101
|
+
it('sends a signed-in account the panel does not admit to sign-in, explained — never to "/"', async () => {
|
|
102
|
+
const user = { id: 'u1', role: 'user' }
|
|
103
|
+
// "/" would redirect to the dashboard, which denies the role again: a loop.
|
|
104
|
+
expect(await run('/admin/dashboard', { sessionUser: user, origin: ORIGIN })).toEqual({
|
|
105
|
+
kind: 'redirect',
|
|
106
|
+
location: signInLocation('/admin/dashboard'),
|
|
107
|
+
})
|
|
108
|
+
expect(await run('/admin/users', { sessionUser: user, origin: ORIGIN })).toEqual({
|
|
109
|
+
kind: 'redirect',
|
|
110
|
+
location: signInLocation('/admin/users'),
|
|
111
|
+
})
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it('still sends staff denied an admin-only page home, where the dashboard admits them', async () => {
|
|
115
|
+
expect(
|
|
116
|
+
await run('/admin/users', { sessionUser: { id: 's1', role: 'staff' }, origin: ORIGIN })
|
|
117
|
+
).toEqual({ kind: 'redirect', location: `${ORIGIN}/` })
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
it('leaves a project whose home is not redirected as it was', () => {
|
|
121
|
+
const code = generateMiddlewareFile(panelAuth())
|
|
122
|
+
expect(code).toContain('function roleDeniedRedirect(request, pathname) {')
|
|
123
|
+
expect(code).toContain('return roleDeniedRedirect(request, pathname);')
|
|
124
|
+
expect(code).not.toContain('homeRedirect')
|
|
125
|
+
expect(code).not.toContain("'forbidden'")
|
|
126
|
+
})
|
|
127
|
+
})
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
// A page keeps its instance when a navigation lands on the page already on
|
|
2
|
+
// screen (a link to the current URL, another record of the same dynamic route,
|
|
3
|
+
// another query string), so a trigger that fires when the page appears —
|
|
4
|
+
// page-loaded, element-visible, component-loaded — ran only on the first
|
|
5
|
+
// arrival. Such pages are wrapped in `remountOnPageArrival`, which starts them
|
|
6
|
+
// fresh on every arrival; pages whose workflows only answer interactions keep
|
|
7
|
+
// their plain export.
|
|
8
|
+
|
|
9
|
+
import generator from '@babel/generator'
|
|
10
|
+
import * as types from '@babel/types'
|
|
11
|
+
import { createNextWorkflowPlugin } from '../src/workflow-component-plugin'
|
|
12
|
+
|
|
13
|
+
const PAGE_ID = 'page-dashboard'
|
|
14
|
+
const ROOT_ID = 'thq_container_root'
|
|
15
|
+
|
|
16
|
+
const workflow = (type: string, scope: string, config: Record<string, unknown>) => ({
|
|
17
|
+
id: `wf-${type}`,
|
|
18
|
+
name: type,
|
|
19
|
+
trigger: { type, nodeId: 'trigger-1', scope, config },
|
|
20
|
+
nodes: [
|
|
21
|
+
{
|
|
22
|
+
id: 'update-1',
|
|
23
|
+
type: 'state-update-local-state',
|
|
24
|
+
config: { property: 'isLoading', value: false },
|
|
25
|
+
stepNumber: 1,
|
|
26
|
+
label: 'Update',
|
|
27
|
+
},
|
|
28
|
+
],
|
|
29
|
+
edges: [{ id: 'e1', source: 'trigger-1', target: 'update-1' }],
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
const jsxComponentChunk = (rootId: string) => ({
|
|
33
|
+
type: 'AST',
|
|
34
|
+
name: 'jsx-component',
|
|
35
|
+
content: types.variableDeclaration('const', [
|
|
36
|
+
types.variableDeclarator(
|
|
37
|
+
types.identifier('Dashboard'),
|
|
38
|
+
types.arrowFunctionExpression(
|
|
39
|
+
[types.identifier('props')],
|
|
40
|
+
types.blockStatement([
|
|
41
|
+
types.returnStatement(
|
|
42
|
+
types.jsxElement(
|
|
43
|
+
types.jsxOpeningElement(types.jsxIdentifier('div'), [
|
|
44
|
+
types.jsxAttribute(types.jsxIdentifier('id'), types.stringLiteral(rootId)),
|
|
45
|
+
]),
|
|
46
|
+
types.jsxClosingElement(types.jsxIdentifier('div')),
|
|
47
|
+
[]
|
|
48
|
+
)
|
|
49
|
+
),
|
|
50
|
+
])
|
|
51
|
+
)
|
|
52
|
+
),
|
|
53
|
+
]),
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
interface RunOptions {
|
|
57
|
+
isPage?: boolean
|
|
58
|
+
rootId?: string
|
|
59
|
+
pageNode?: unknown
|
|
60
|
+
projectComponents?: Record<string, unknown>
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const run = async (
|
|
64
|
+
workflows: Array<ReturnType<typeof workflow>>,
|
|
65
|
+
{
|
|
66
|
+
isPage = true,
|
|
67
|
+
rootId = ROOT_ID,
|
|
68
|
+
pageNode = { type: 'element', content: { elementType: 'container' } },
|
|
69
|
+
projectComponents,
|
|
70
|
+
}: RunOptions = {}
|
|
71
|
+
) => {
|
|
72
|
+
const structure: any = {
|
|
73
|
+
uidl: {
|
|
74
|
+
name: 'Dashboard',
|
|
75
|
+
node: pageNode,
|
|
76
|
+
stateDefinitions: { isLoading: { type: 'boolean', defaultValue: true } },
|
|
77
|
+
outputOptions: { pageId: PAGE_ID, folderPath: [] },
|
|
78
|
+
},
|
|
79
|
+
chunks: [
|
|
80
|
+
jsxComponentChunk(rootId),
|
|
81
|
+
{
|
|
82
|
+
type: 'AST',
|
|
83
|
+
name: 'export',
|
|
84
|
+
content: types.exportDefaultDeclaration(types.identifier('Dashboard')),
|
|
85
|
+
},
|
|
86
|
+
],
|
|
87
|
+
options: {
|
|
88
|
+
workflows: {
|
|
89
|
+
workflows: workflows.reduce(
|
|
90
|
+
(byId: Record<string, unknown>, wf) => ({ ...byId, [wf.id]: wf }),
|
|
91
|
+
{}
|
|
92
|
+
),
|
|
93
|
+
customNodes: {},
|
|
94
|
+
},
|
|
95
|
+
pagesPath: ['pages'],
|
|
96
|
+
extractedResources: {},
|
|
97
|
+
projectComponents,
|
|
98
|
+
},
|
|
99
|
+
dependencies: {},
|
|
100
|
+
}
|
|
101
|
+
await createNextWorkflowPlugin({ isPage })(structure)
|
|
102
|
+
const exportChunk = structure.chunks.find((chunk: any) => chunk.name === 'export')
|
|
103
|
+
const moduleChunk = structure.chunks.find((chunk: any) => chunk.name === 'workflow-module')
|
|
104
|
+
return {
|
|
105
|
+
exportCode: generator(exportChunk.content).code,
|
|
106
|
+
moduleCode: moduleChunk ? String(moduleChunk.content) : '',
|
|
107
|
+
dependencies: structure.dependencies,
|
|
108
|
+
extractedResources: structure.options.extractedResources,
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const BANNER_ID = 'thq_container_banner'
|
|
113
|
+
|
|
114
|
+
/** An instance of a project component, as local dependencies are resolved for pages. */
|
|
115
|
+
const componentInstance = (name: string) => ({
|
|
116
|
+
type: 'element',
|
|
117
|
+
content: { elementType: 'component', semanticType: name, dependency: { type: 'local' } },
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
const pageRendering = (componentName: string) => ({
|
|
121
|
+
type: 'element',
|
|
122
|
+
content: { elementType: 'container', children: [componentInstance(componentName)] },
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
const bannerComponent = () => ({
|
|
126
|
+
name: 'Banner',
|
|
127
|
+
node: {
|
|
128
|
+
type: 'element',
|
|
129
|
+
content: { elementType: 'container', attrs: { id: { type: 'static', content: BANNER_ID } } },
|
|
130
|
+
},
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
const WRAPPED_EXPORT = 'export default remountOnPageArrival(Dashboard);'
|
|
134
|
+
const PLAIN_EXPORT = 'export default Dashboard;'
|
|
135
|
+
|
|
136
|
+
describe('workflow plugin — remount on page arrival', () => {
|
|
137
|
+
it('wraps a page whose workflow runs when the page loads', async () => {
|
|
138
|
+
const result = await run([workflow('event-page-loaded', 'page', { pageId: PAGE_ID })])
|
|
139
|
+
|
|
140
|
+
expect(result.exportCode).toBe(WRAPPED_EXPORT)
|
|
141
|
+
expect(result.dependencies.remountOnPageArrival).toEqual({
|
|
142
|
+
type: 'local',
|
|
143
|
+
path: '../utils/page-arrival',
|
|
144
|
+
})
|
|
145
|
+
expect(result.extractedResources['utils/page-arrival']).toBeDefined()
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
it('wraps a page whose workflow runs when its root element becomes visible', async () => {
|
|
149
|
+
const result = await run([
|
|
150
|
+
workflow('event-element-visible', 'element', { elementHtmlId: ROOT_ID, once: true }),
|
|
151
|
+
])
|
|
152
|
+
|
|
153
|
+
expect(result.exportCode).toBe(WRAPPED_EXPORT)
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
it('leaves the page alone when the visible element is rendered elsewhere', async () => {
|
|
157
|
+
const result = await run(
|
|
158
|
+
[workflow('event-element-visible', 'element', { elementHtmlId: ROOT_ID, once: true })],
|
|
159
|
+
{ rootId: 'thq_container_other' }
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
expect(result.exportCode).toBe(PLAIN_EXPORT)
|
|
163
|
+
expect(result.dependencies.remountOnPageArrival).toBeUndefined()
|
|
164
|
+
expect(result.extractedResources).toEqual({})
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
it('leaves the page alone when its workflows only answer interactions', async () => {
|
|
168
|
+
const result = await run([
|
|
169
|
+
workflow('event-element-clicked', 'element', { elementHtmlId: ROOT_ID }),
|
|
170
|
+
workflow('event-interval', 'page', { pageId: PAGE_ID, intervalMs: 1000 }),
|
|
171
|
+
])
|
|
172
|
+
|
|
173
|
+
expect(result.moduleCode).not.toBe('')
|
|
174
|
+
expect(result.exportCode).toBe(PLAIN_EXPORT)
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
it('never wraps a component', async () => {
|
|
178
|
+
const result = await run(
|
|
179
|
+
[workflow('event-component-loaded', 'component', { componentName: 'Dashboard' })],
|
|
180
|
+
{ isPage: false }
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
expect(result.moduleCode).toContain('Component loaded')
|
|
184
|
+
expect(result.exportCode).toBe(PLAIN_EXPORT)
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
it('wraps a page that renders a component running a workflow when its element is visible', async () => {
|
|
188
|
+
const result = await run(
|
|
189
|
+
[workflow('event-element-visible', 'element', { elementHtmlId: BANNER_ID, once: true })],
|
|
190
|
+
{ pageNode: pageRendering('Banner'), projectComponents: { Banner: bannerComponent() } }
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
expect(result.moduleCode).toBe('')
|
|
194
|
+
expect(result.exportCode).toBe(WRAPPED_EXPORT)
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
it('finds the component through the components it is nested in', async () => {
|
|
198
|
+
const result = await run(
|
|
199
|
+
[workflow('event-element-visible', 'element', { elementHtmlId: BANNER_ID, once: true })],
|
|
200
|
+
{
|
|
201
|
+
pageNode: pageRendering('Layout'),
|
|
202
|
+
projectComponents: {
|
|
203
|
+
Layout: { name: 'Layout', node: componentInstance('Banner') },
|
|
204
|
+
Banner: bannerComponent(),
|
|
205
|
+
},
|
|
206
|
+
}
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
expect(result.exportCode).toBe(WRAPPED_EXPORT)
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
it('wraps a page that renders a component running a workflow when it loads', async () => {
|
|
213
|
+
const result = await run(
|
|
214
|
+
[workflow('event-component-loaded', 'component', { componentName: 'Banner' })],
|
|
215
|
+
{ pageNode: pageRendering('Banner'), projectComponents: { Banner: bannerComponent() } }
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
expect(result.exportCode).toBe(WRAPPED_EXPORT)
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
it('leaves the page alone when no rendered component observes the element', async () => {
|
|
222
|
+
const result = await run(
|
|
223
|
+
[
|
|
224
|
+
workflow('event-element-visible', 'element', {
|
|
225
|
+
elementHtmlId: 'thq_elsewhere',
|
|
226
|
+
once: true,
|
|
227
|
+
}),
|
|
228
|
+
],
|
|
229
|
+
{ pageNode: pageRendering('Banner'), projectComponents: { Banner: bannerComponent() } }
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
expect(result.exportCode).toBe(PLAIN_EXPORT)
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
it('cancels a delayed page-loaded run together with the other triggers', async () => {
|
|
236
|
+
const result = await run([
|
|
237
|
+
workflow('event-page-loaded', 'page', { pageId: PAGE_ID, delay: 500 }),
|
|
238
|
+
])
|
|
239
|
+
|
|
240
|
+
expect(result.moduleCode).toContain(
|
|
241
|
+
'const __delay_wf_event_page_loaded = setTimeout(function() { __execWf(__wfConfig_wf_event_page_loaded'
|
|
242
|
+
)
|
|
243
|
+
expect(result.moduleCode).toContain(
|
|
244
|
+
'cleanups.push(function() { clearTimeout(__delay_wf_event_page_loaded); });'
|
|
245
|
+
)
|
|
246
|
+
})
|
|
247
|
+
})
|
|
@@ -265,3 +265,26 @@ describe('Stripe driver — settling a checkout from Stripe', () => {
|
|
|
265
265
|
expect(calls).toEqual([])
|
|
266
266
|
})
|
|
267
267
|
})
|
|
268
|
+
|
|
269
|
+
describe('Stripe driver — checkout lifetime', () => {
|
|
270
|
+
it("opens every Checkout page for an hour, not Stripe's default day", async () => {
|
|
271
|
+
const { stripe, calls } = load()
|
|
272
|
+
const before = Math.floor(Date.now() / 1000)
|
|
273
|
+
await withEnv(KEY, async () => {
|
|
274
|
+
await stripe.createCheckout({ amount: 10, currency: 'usd', description: 'Order' })
|
|
275
|
+
await stripe.createSubscriptionCheckout({
|
|
276
|
+
amount: 10,
|
|
277
|
+
currency: 'usd',
|
|
278
|
+
recurringInterval: 'month',
|
|
279
|
+
description: 'Club',
|
|
280
|
+
})
|
|
281
|
+
})
|
|
282
|
+
const after = Math.floor(Date.now() / 1000)
|
|
283
|
+
const sessions = calls.filter((call: any) => call.method === 'checkout.sessions.create')
|
|
284
|
+
expect(sessions).toHaveLength(2)
|
|
285
|
+
for (const session of sessions as any[]) {
|
|
286
|
+
expect(session.params.expires_at).toBeGreaterThanOrEqual(before + 3600)
|
|
287
|
+
expect(session.params.expires_at).toBeLessThanOrEqual(after + 3600)
|
|
288
|
+
}
|
|
289
|
+
})
|
|
290
|
+
})
|