@teleporthq/teleport-plugin-next-workflows 0.44.0 → 0.44.1
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__/_helpers/fake-store-database.ts +428 -0
- package/__tests__/_helpers/invoice-store.ts +382 -0
- package/__tests__/email-locale-senders.test.ts +3 -1
- package/__tests__/email-sender-name.test.ts +513 -0
- package/__tests__/graph-utils-topological-order.test.ts +155 -1
- package/__tests__/invoice-billing-details.test.ts +107 -1
- package/__tests__/invoice-data-access-replace.test.ts +236 -2
- package/__tests__/invoice-order-items-settle.test.ts +1 -1
- package/__tests__/invoice-payment-email.test.ts +191 -0
- package/__tests__/invoice-regenerate-email.test.ts +288 -0
- package/__tests__/invoice-regenerate-route.test.ts +9 -7
- package/__tests__/invoice-reservation.test.ts +1 -1
- package/__tests__/invoice-tender.test.ts +1 -1
- package/__tests__/segment-splitter-checkout-order.test.ts +306 -0
- package/__tests__/streaming-on-end-env-filter.test.ts +18 -18
- package/__tests__/streaming-on-end-order.test.ts +387 -0
- package/dist/cjs/account-delete-route-generator.d.ts +1 -0
- package/dist/cjs/account-delete-route-generator.d.ts.map +1 -1
- package/dist/cjs/account-delete-route-generator.js +2 -1
- package/dist/cjs/account-delete-route-generator.js.map +1 -1
- package/dist/cjs/auth-generator.d.ts +1 -0
- package/dist/cjs/auth-generator.d.ts.map +1 -1
- package/dist/cjs/auth-generator.js +2 -1
- 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 +9 -26
- package/dist/cjs/executor-generator.js.map +1 -1
- package/dist/cjs/graph-utils.d.ts +31 -1
- package/dist/cjs/graph-utils.d.ts.map +1 -1
- package/dist/cjs/graph-utils.js +112 -10
- package/dist/cjs/graph-utils.js.map +1 -1
- package/dist/cjs/invoice/api-routes-code.d.ts +12 -3
- package/dist/cjs/invoice/api-routes-code.d.ts.map +1 -1
- package/dist/cjs/invoice/api-routes-code.js +152 -21
- package/dist/cjs/invoice/api-routes-code.js.map +1 -1
- package/dist/cjs/invoice/data-access-code.d.ts.map +1 -1
- package/dist/cjs/invoice/data-access-code.js +133 -21
- package/dist/cjs/invoice/data-access-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 +15 -25
- package/dist/cjs/invoice/email-sender-code.js.map +1 -1
- package/dist/cjs/invoice/index.js +1 -1
- package/dist/cjs/invoice/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 +10 -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 +3 -0
- package/dist/cjs/invoice/invoice-html-code.js.map +1 -1
- package/dist/cjs/nodes/email/email-mailgun.d.ts.map +1 -1
- package/dist/cjs/nodes/email/email-mailgun.js +4 -2
- package/dist/cjs/nodes/email/email-mailgun.js.map +1 -1
- package/dist/cjs/nodes/email/email-postmark.d.ts.map +1 -1
- package/dist/cjs/nodes/email/email-postmark.js +5 -2
- package/dist/cjs/nodes/email/email-postmark.js.map +1 -1
- package/dist/cjs/nodes/email/email-resend.d.ts.map +1 -1
- package/dist/cjs/nodes/email/email-resend.js +5 -2
- package/dist/cjs/nodes/email/email-resend.js.map +1 -1
- package/dist/cjs/nodes/email/email-sendgrid.d.ts.map +1 -1
- package/dist/cjs/nodes/email/email-sendgrid.js +8 -2
- package/dist/cjs/nodes/email/email-sendgrid.js.map +1 -1
- package/dist/cjs/nodes/storage/storage-local-remove.d.ts.map +1 -1
- package/dist/cjs/nodes/storage/storage-local-remove.js +2 -0
- package/dist/cjs/nodes/storage/storage-local-remove.js.map +1 -1
- package/dist/cjs/nodes/storage/storage-local-set.d.ts.map +1 -1
- package/dist/cjs/nodes/storage/storage-local-set.js +4 -0
- package/dist/cjs/nodes/storage/storage-local-set.js.map +1 -1
- package/dist/cjs/segment-splitter.d.ts.map +1 -1
- package/dist/cjs/segment-splitter.js +65 -27
- package/dist/cjs/segment-splitter.js.map +1 -1
- package/dist/cjs/transactional-email-code.d.ts.map +1 -1
- package/dist/cjs/transactional-email-code.js +32 -18
- package/dist/cjs/transactional-email-code.js.map +1 -1
- package/dist/cjs/tsconfig.tsbuildinfo +1 -1
- package/dist/cjs/workflow-project-plugin.d.ts.map +1 -1
- package/dist/cjs/workflow-project-plugin.js +2 -0
- package/dist/cjs/workflow-project-plugin.js.map +1 -1
- package/dist/esm/account-delete-route-generator.d.ts +1 -0
- package/dist/esm/account-delete-route-generator.d.ts.map +1 -1
- package/dist/esm/account-delete-route-generator.js +2 -1
- package/dist/esm/account-delete-route-generator.js.map +1 -1
- package/dist/esm/auth-generator.d.ts +1 -0
- package/dist/esm/auth-generator.d.ts.map +1 -1
- package/dist/esm/auth-generator.js +2 -1
- 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 +9 -26
- package/dist/esm/executor-generator.js.map +1 -1
- package/dist/esm/graph-utils.d.ts +31 -1
- package/dist/esm/graph-utils.d.ts.map +1 -1
- package/dist/esm/graph-utils.js +109 -9
- package/dist/esm/graph-utils.js.map +1 -1
- package/dist/esm/invoice/api-routes-code.d.ts +12 -3
- package/dist/esm/invoice/api-routes-code.d.ts.map +1 -1
- package/dist/esm/invoice/api-routes-code.js +152 -21
- package/dist/esm/invoice/api-routes-code.js.map +1 -1
- package/dist/esm/invoice/data-access-code.d.ts.map +1 -1
- package/dist/esm/invoice/data-access-code.js +133 -21
- package/dist/esm/invoice/data-access-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 +15 -25
- package/dist/esm/invoice/email-sender-code.js.map +1 -1
- package/dist/esm/invoice/index.js +1 -1
- package/dist/esm/invoice/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 +10 -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 +3 -0
- package/dist/esm/invoice/invoice-html-code.js.map +1 -1
- package/dist/esm/nodes/email/email-mailgun.d.ts.map +1 -1
- package/dist/esm/nodes/email/email-mailgun.js +4 -2
- package/dist/esm/nodes/email/email-mailgun.js.map +1 -1
- package/dist/esm/nodes/email/email-postmark.d.ts.map +1 -1
- package/dist/esm/nodes/email/email-postmark.js +5 -2
- package/dist/esm/nodes/email/email-postmark.js.map +1 -1
- package/dist/esm/nodes/email/email-resend.d.ts.map +1 -1
- package/dist/esm/nodes/email/email-resend.js +5 -2
- package/dist/esm/nodes/email/email-resend.js.map +1 -1
- package/dist/esm/nodes/email/email-sendgrid.d.ts.map +1 -1
- package/dist/esm/nodes/email/email-sendgrid.js +8 -2
- package/dist/esm/nodes/email/email-sendgrid.js.map +1 -1
- package/dist/esm/nodes/storage/storage-local-remove.d.ts.map +1 -1
- package/dist/esm/nodes/storage/storage-local-remove.js +2 -0
- package/dist/esm/nodes/storage/storage-local-remove.js.map +1 -1
- package/dist/esm/nodes/storage/storage-local-set.d.ts.map +1 -1
- package/dist/esm/nodes/storage/storage-local-set.js +4 -0
- package/dist/esm/nodes/storage/storage-local-set.js.map +1 -1
- package/dist/esm/segment-splitter.d.ts.map +1 -1
- package/dist/esm/segment-splitter.js +66 -28
- package/dist/esm/segment-splitter.js.map +1 -1
- package/dist/esm/transactional-email-code.d.ts.map +1 -1
- package/dist/esm/transactional-email-code.js +32 -18
- package/dist/esm/transactional-email-code.js.map +1 -1
- package/dist/esm/tsconfig.tsbuildinfo +1 -1
- package/dist/esm/workflow-project-plugin.d.ts.map +1 -1
- package/dist/esm/workflow-project-plugin.js +2 -0
- package/dist/esm/workflow-project-plugin.js.map +1 -1
- package/package.json +3 -3
- package/src/account-delete-route-generator.ts +3 -1
- package/src/auth-generator.ts +3 -1
- package/src/executor-generator.ts +9 -26
- package/src/graph-utils.ts +143 -11
- package/src/invoice/api-routes-code.ts +166 -21
- package/src/invoice/data-access-code.ts +139 -21
- package/src/invoice/email-sender-code.ts +15 -25
- package/src/invoice/index.ts +1 -1
- package/src/invoice/invoice-assembly-code.ts +10 -6
- package/src/invoice/invoice-html-code.ts +3 -0
- package/src/nodes/email/email-mailgun.ts +11 -2
- package/src/nodes/email/email-postmark.ts +12 -2
- package/src/nodes/email/email-resend.ts +12 -2
- package/src/nodes/email/email-sendgrid.ts +15 -2
- package/src/nodes/storage/storage-local-remove.ts +4 -0
- package/src/nodes/storage/storage-local-set.ts +6 -0
- package/src/segment-splitter.ts +90 -36
- package/src/transactional-email-code.ts +32 -21
- package/src/workflow-project-plugin.ts +2 -0
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BUYER,
|
|
3
|
+
ORDER_ID,
|
|
4
|
+
SECOND_BUYER,
|
|
5
|
+
SECOND_ORDER_ID,
|
|
6
|
+
PostmarkMessage,
|
|
7
|
+
historyLines,
|
|
8
|
+
invoiceOf,
|
|
9
|
+
ledgerRows,
|
|
10
|
+
openStore,
|
|
11
|
+
payWhilePdfServiceIsDown,
|
|
12
|
+
runsAsTheStore,
|
|
13
|
+
seedPaidOrder,
|
|
14
|
+
} from './_helpers/invoice-store'
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Regenerate emails the buyer the invoice their payment could not.
|
|
18
|
+
*
|
|
19
|
+
* When the PDF service is down at payment, the order is still paid and its
|
|
20
|
+
* invoice row stays reserved without a PDF, so the payment's invoice email
|
|
21
|
+
* never goes out. The merchant then gets the PDF with the admin panel's
|
|
22
|
+
* Regenerate — and that regeneration, the FIRST to give the invoice a PDF,
|
|
23
|
+
* emails the buyer, once, when the store emails invoices.
|
|
24
|
+
*
|
|
25
|
+
* Everything here is the code the store runs, as emitted: the payment's
|
|
26
|
+
* `/api/invoices/generate`, the admin panel's `/api/invoices/[id]/regenerate`,
|
|
27
|
+
* the data access, the assembly, the PDF client, the invoice sender, the
|
|
28
|
+
* sent-email ledger and the locale module. Only what lies outside the store is
|
|
29
|
+
* faked: its Postgres database, the PDF service and the email provider.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
describe('Regenerate emails the buyer the invoice their payment could not', () => {
|
|
33
|
+
runsAsTheStore()
|
|
34
|
+
|
|
35
|
+
it('no PDF before: the regeneration that gives the invoice its PDF emails the buyer once', async () => {
|
|
36
|
+
const store = openStore()
|
|
37
|
+
seedPaidOrder(store.db, ORDER_ID)
|
|
38
|
+
const invoice = await payWhilePdfServiceIsDown(store, ORDER_ID)
|
|
39
|
+
expect(store.emails).toHaveLength(0)
|
|
40
|
+
|
|
41
|
+
const regenerated = await store.regenerate(String(invoice.id))
|
|
42
|
+
|
|
43
|
+
expect(regenerated.status).toBe(200)
|
|
44
|
+
expect(Number(invoice.pdf_size_bytes)).toBeGreaterThan(0)
|
|
45
|
+
expect(store.emails).toHaveLength(1)
|
|
46
|
+
const [email] = store.emails
|
|
47
|
+
expect(email.To).toBe(BUYER)
|
|
48
|
+
expect(email.From).toBe('Lumen Pottery <shop@lumen.test>')
|
|
49
|
+
expect(email.Attachments).toEqual([
|
|
50
|
+
{
|
|
51
|
+
Name: 'INV-0001.pdf',
|
|
52
|
+
Content: Buffer.from('%PDF-1.4 INV-0001').toString('base64'),
|
|
53
|
+
ContentType: 'application/pdf',
|
|
54
|
+
},
|
|
55
|
+
])
|
|
56
|
+
|
|
57
|
+
// The sent-email ledger: the row the payment's email writes, naming the
|
|
58
|
+
// route that sent it.
|
|
59
|
+
expect(ledgerRows(store)).toEqual([
|
|
60
|
+
expect.objectContaining({
|
|
61
|
+
email_type: 'invoice-email',
|
|
62
|
+
audience: 'customer',
|
|
63
|
+
recipient_email: BUYER,
|
|
64
|
+
from_email: 'shop@lumen.test',
|
|
65
|
+
from_name: 'Lumen Pottery',
|
|
66
|
+
status: 'sent',
|
|
67
|
+
provider: 'postmark',
|
|
68
|
+
source: 'invoice',
|
|
69
|
+
source_ref: 'api/invoices/[id]/regenerate',
|
|
70
|
+
order_id: ORDER_ID,
|
|
71
|
+
reference_type: 'invoice',
|
|
72
|
+
reference_id: invoice.id,
|
|
73
|
+
}),
|
|
74
|
+
])
|
|
75
|
+
// The order's history: one "email sent" line, by the store admin.
|
|
76
|
+
expect(historyLines(store)).toEqual([
|
|
77
|
+
expect.objectContaining({
|
|
78
|
+
event_type: 'order-email-sent',
|
|
79
|
+
dedupe_key: 'order-email-sent:' + String(invoice.id) + ':invoice-email',
|
|
80
|
+
order_id: ORDER_ID,
|
|
81
|
+
payload: JSON.stringify({ purpose: 'invoice-email', to: BUYER }),
|
|
82
|
+
actor: 'Store admin',
|
|
83
|
+
actor_type: 'merchant',
|
|
84
|
+
status: 'recorded',
|
|
85
|
+
}),
|
|
86
|
+
])
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it.each([
|
|
90
|
+
[
|
|
91
|
+
'served from the database',
|
|
92
|
+
false,
|
|
93
|
+
(invoiceId: string) => '/api/invoices/' + invoiceId + '/pdf',
|
|
94
|
+
],
|
|
95
|
+
['in runtime storage', true, () => 'https://cdn.test/INV-0001.pdf'],
|
|
96
|
+
])(
|
|
97
|
+
'sends exactly the email the payment would have sent — sender, template, attachment, language, and the link of a PDF %s',
|
|
98
|
+
async (_, storageConfigured, expectedLink) => {
|
|
99
|
+
// A payment the PDF service served: the invoice email it sends.
|
|
100
|
+
const paidOk = openStore()
|
|
101
|
+
paidOk.storage.configured = storageConfigured
|
|
102
|
+
seedPaidOrder(paidOk.db, ORDER_ID)
|
|
103
|
+
expect((await paidOk.pay(ORDER_ID)).status).toBe(200)
|
|
104
|
+
expect(paidOk.emails).toHaveLength(1)
|
|
105
|
+
const atPayment = paidOk.emails[0]
|
|
106
|
+
const paidInvoiceId = String(invoiceOf(paidOk, ORDER_ID).id)
|
|
107
|
+
|
|
108
|
+
// The same order, paid while the PDF service was down, then regenerated.
|
|
109
|
+
const paidDown = openStore()
|
|
110
|
+
paidDown.storage.configured = storageConfigured
|
|
111
|
+
seedPaidOrder(paidDown.db, ORDER_ID)
|
|
112
|
+
const invoice = await payWhilePdfServiceIsDown(paidDown, ORDER_ID)
|
|
113
|
+
expect((await paidDown.regenerate(String(invoice.id))).status).toBe(200)
|
|
114
|
+
expect(paidDown.emails).toHaveLength(1)
|
|
115
|
+
const atRegeneration = paidDown.emails[0]
|
|
116
|
+
|
|
117
|
+
// Nothing differs but the invoice's own id, inside a database link.
|
|
118
|
+
const sameLink = (message: PostmarkMessage, id: string): PostmarkMessage => ({
|
|
119
|
+
...message,
|
|
120
|
+
HtmlBody: message.HtmlBody.split(id).join('<invoice id>'),
|
|
121
|
+
})
|
|
122
|
+
expect(sameLink(atRegeneration, String(invoice.id))).toEqual(
|
|
123
|
+
sameLink(atPayment, paidInvoiceId)
|
|
124
|
+
)
|
|
125
|
+
// The buyer's language (the order is Spanish), not the merchant's German admin.
|
|
126
|
+
expect(atRegeneration.Subject).toBe('Factura INV-0001')
|
|
127
|
+
expect(atRegeneration.HtmlBody).toBe(
|
|
128
|
+
'<p>Hola Ana Pop, factura INV-0001 por €100.00: ' +
|
|
129
|
+
expectedLink(String(invoice.id)) +
|
|
130
|
+
'</p>'
|
|
131
|
+
)
|
|
132
|
+
}
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
it("an order with no language of its own gets the store's main language, never the merchant's", async () => {
|
|
136
|
+
const store = openStore()
|
|
137
|
+
seedPaidOrder(store.db, ORDER_ID, { locale: null })
|
|
138
|
+
const invoice = await payWhilePdfServiceIsDown(store, ORDER_ID)
|
|
139
|
+
|
|
140
|
+
await store.regenerate(String(invoice.id))
|
|
141
|
+
|
|
142
|
+
expect(store.emails.map((email) => email.Subject)).toEqual(['Invoice INV-0001'])
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
it('had a PDF: only the invoice that never had one is emailed', async () => {
|
|
146
|
+
const store = openStore()
|
|
147
|
+
seedPaidOrder(store.db, ORDER_ID)
|
|
148
|
+
seedPaidOrder(store.db, SECOND_ORDER_ID, {}, SECOND_BUYER)
|
|
149
|
+
// The first order's invoice was issued and emailed at payment.
|
|
150
|
+
expect((await store.pay(ORDER_ID)).status).toBe(200)
|
|
151
|
+
const issued = invoiceOf(store, ORDER_ID)
|
|
152
|
+
expect(store.emails.map((email) => email.To)).toEqual([BUYER])
|
|
153
|
+
// The second order's was not: the PDF service was down.
|
|
154
|
+
const unfinished = await payWhilePdfServiceIsDown(store, SECOND_ORDER_ID)
|
|
155
|
+
|
|
156
|
+
expect((await store.regenerate(String(issued.id))).status).toBe(200)
|
|
157
|
+
expect((await store.regenerate(String(unfinished.id))).status).toBe(200)
|
|
158
|
+
|
|
159
|
+
// The payment's email, then the unfinished invoice's — nothing for the
|
|
160
|
+
// invoice that already had its PDF.
|
|
161
|
+
expect(store.emails.map((email) => email.To)).toEqual([BUYER, SECOND_BUYER])
|
|
162
|
+
// Each email is on its own order's history once: the payment's, by the
|
|
163
|
+
// store, and the regeneration's, by the admin.
|
|
164
|
+
expect(historyLines(store).map((line) => [line.order_id, line.actor_type])).toEqual([
|
|
165
|
+
[ORDER_ID, 'system'],
|
|
166
|
+
[SECOND_ORDER_ID, 'merchant'],
|
|
167
|
+
])
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
it('a second regeneration never emails again', async () => {
|
|
171
|
+
const store = openStore()
|
|
172
|
+
seedPaidOrder(store.db, ORDER_ID)
|
|
173
|
+
const invoice = await payWhilePdfServiceIsDown(store, ORDER_ID)
|
|
174
|
+
|
|
175
|
+
expect((await store.regenerate(String(invoice.id))).status).toBe(200)
|
|
176
|
+
expect(store.emails).toHaveLength(1)
|
|
177
|
+
expect((await store.regenerate(String(invoice.id))).status).toBe(200)
|
|
178
|
+
expect((await store.regenerate(String(invoice.id))).status).toBe(200)
|
|
179
|
+
|
|
180
|
+
expect(store.emails).toHaveLength(1)
|
|
181
|
+
expect(ledgerRows(store)).toHaveLength(1)
|
|
182
|
+
expect(historyLines(store)).toHaveLength(1)
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
it('two overlapping regenerations (a double click) email once', async () => {
|
|
186
|
+
const store = openStore()
|
|
187
|
+
seedPaidOrder(store.db, ORDER_ID)
|
|
188
|
+
const invoice = await payWhilePdfServiceIsDown(store, ORDER_ID)
|
|
189
|
+
|
|
190
|
+
// Both requests read the invoice without a PDF and render before either
|
|
191
|
+
// writes: the PDF service answers neither until it has both.
|
|
192
|
+
let releaseRenders: () => void = () => undefined
|
|
193
|
+
store.pdfService.hold = new Promise((resolve) => {
|
|
194
|
+
releaseRenders = resolve
|
|
195
|
+
})
|
|
196
|
+
const first = store.regenerate(String(invoice.id))
|
|
197
|
+
const second = store.regenerate(String(invoice.id))
|
|
198
|
+
while (store.pdfService.renders < 2) {
|
|
199
|
+
await new Promise((resolve) => setImmediate(resolve))
|
|
200
|
+
}
|
|
201
|
+
releaseRenders()
|
|
202
|
+
const answers = await Promise.all([first, second])
|
|
203
|
+
|
|
204
|
+
expect(answers.map((answer) => answer.status)).toEqual([200, 200])
|
|
205
|
+
expect(store.emails).toHaveLength(1)
|
|
206
|
+
expect(ledgerRows(store)).toHaveLength(1)
|
|
207
|
+
expect(historyLines(store)).toHaveLength(1)
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
it('Invoice Email Delivery off: the same regeneration emails nobody', async () => {
|
|
211
|
+
const delivering = openStore()
|
|
212
|
+
seedPaidOrder(delivering.db, ORDER_ID)
|
|
213
|
+
const deliveredInvoice = await payWhilePdfServiceIsDown(delivering, ORDER_ID)
|
|
214
|
+
await delivering.regenerate(String(deliveredInvoice.id))
|
|
215
|
+
expect(delivering.emails).toHaveLength(1)
|
|
216
|
+
|
|
217
|
+
const silent = openStore({ emailDelivery: false })
|
|
218
|
+
seedPaidOrder(silent.db, ORDER_ID)
|
|
219
|
+
const invoice = await payWhilePdfServiceIsDown(silent, ORDER_ID)
|
|
220
|
+
const regenerated = await silent.regenerate(String(invoice.id))
|
|
221
|
+
|
|
222
|
+
expect(regenerated.status).toBe(200)
|
|
223
|
+
expect(Number(invoice.pdf_size_bytes)).toBeGreaterThan(0)
|
|
224
|
+
expect(silent.emails).toHaveLength(0)
|
|
225
|
+
expect(ledgerRows(silent)).toHaveLength(0)
|
|
226
|
+
expect(historyLines(silent)).toHaveLength(0)
|
|
227
|
+
// The route does not even load the sender.
|
|
228
|
+
expect(silent.routeCode).not.toContain('email-sender')
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
it('a refused email still regenerates the invoice, is logged, and is never sent again', async () => {
|
|
232
|
+
const store = openStore()
|
|
233
|
+
seedPaidOrder(store.db, ORDER_ID)
|
|
234
|
+
const invoice = await payWhilePdfServiceIsDown(store, ORDER_ID)
|
|
235
|
+
store.provider.refuses = true
|
|
236
|
+
|
|
237
|
+
const regenerated = await store.regenerate(String(invoice.id))
|
|
238
|
+
|
|
239
|
+
// The merchant has the PDF, and the order links to it.
|
|
240
|
+
expect(regenerated.status).toBe(200)
|
|
241
|
+
expect(regenerated.body).toMatchObject({ success: true, invoiceNumber: 'INV-0001' })
|
|
242
|
+
expect(Number(invoice.pdf_size_bytes)).toBeGreaterThan(0)
|
|
243
|
+
expect(store.db.row('teleport_orders', ORDER_ID)).toMatchObject({
|
|
244
|
+
invoice_id: invoice.id,
|
|
245
|
+
invoice_number: 'INV-0001',
|
|
246
|
+
invoice_pdf_url: '/api/invoices/' + String(invoice.id) + '/pdf',
|
|
247
|
+
})
|
|
248
|
+
// The provider was asked once and refused: the ledger says so, the
|
|
249
|
+
// order's history does not claim an email went out, the log names it.
|
|
250
|
+
expect(store.emails).toHaveLength(1)
|
|
251
|
+
expect(ledgerRows(store)).toEqual([
|
|
252
|
+
expect.objectContaining({
|
|
253
|
+
status: 'failed',
|
|
254
|
+
error_message: '422 The From address is not a confirmed sender signature',
|
|
255
|
+
}),
|
|
256
|
+
])
|
|
257
|
+
expect(historyLines(store)).toHaveLength(0)
|
|
258
|
+
expect(store.errors).toContainEqual(
|
|
259
|
+
expect.stringContaining('invoice INV-0001 could not be emailed to its customer')
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
store.provider.refuses = false
|
|
263
|
+
expect((await store.regenerate(String(invoice.id))).status).toBe(200)
|
|
264
|
+
expect(store.emails).toHaveLength(1)
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
it('an order that cannot be updated still gets its invoice emailed, once', async () => {
|
|
268
|
+
const store = openStore()
|
|
269
|
+
seedPaidOrder(store.db, ORDER_ID)
|
|
270
|
+
const invoice = await payWhilePdfServiceIsDown(store, ORDER_ID)
|
|
271
|
+
store.db.failNext(/^UPDATE teleport_orders SET invoice_id = \$1/, 'Connection terminated')
|
|
272
|
+
|
|
273
|
+
const failed = await store.regenerate(String(invoice.id))
|
|
274
|
+
|
|
275
|
+
// The invoice has its PDF, so its buyer is emailed it, with the link the
|
|
276
|
+
// database serves it from.
|
|
277
|
+
expect(failed.status).toBe(500)
|
|
278
|
+
expect(store.emails).toHaveLength(1)
|
|
279
|
+
expect(store.emails[0].HtmlBody).toContain('/api/invoices/' + String(invoice.id) + '/pdf')
|
|
280
|
+
expect(ledgerRows(store)).toHaveLength(1)
|
|
281
|
+
|
|
282
|
+
// Regenerating again links the order, and emails nobody.
|
|
283
|
+
expect((await store.regenerate(String(invoice.id))).status).toBe(200)
|
|
284
|
+
expect(store.db.row('teleport_orders', ORDER_ID)).toMatchObject({ invoice_id: invoice.id })
|
|
285
|
+
expect(store.emails).toHaveLength(1)
|
|
286
|
+
expect(historyLines(store)).toHaveLength(1)
|
|
287
|
+
})
|
|
288
|
+
})
|
|
@@ -16,8 +16,10 @@ import {
|
|
|
16
16
|
*
|
|
17
17
|
* Every test runs the EMITTED route against the REAL emitted assembly, with
|
|
18
18
|
* the database, the PDF service and the session stubbed: a regenerated invoice
|
|
19
|
-
* must be priced by exactly the code that issued it, keep its identity
|
|
20
|
-
* the merchant's corrections over the order
|
|
19
|
+
* must be priced by exactly the code that issued it, keep its identity and
|
|
20
|
+
* take the merchant's corrections over the order. These run a store that does
|
|
21
|
+
* not email invoices; the one email a regeneration can send is proved in
|
|
22
|
+
* `invoice-regenerate-email.test.ts`.
|
|
21
23
|
*/
|
|
22
24
|
|
|
23
25
|
const SETTINGS = {
|
|
@@ -27,7 +29,7 @@ const SETTINGS = {
|
|
|
27
29
|
companyDetails: {},
|
|
28
30
|
template: { document: null as unknown },
|
|
29
31
|
tables: { invoicesTable: 'teleport_invoices', invoiceItemsTable: 'teleport_invoice_items' },
|
|
30
|
-
emailDelivery: { enabled:
|
|
32
|
+
emailDelivery: { enabled: false },
|
|
31
33
|
} as unknown as UIDLInvoiceSettings
|
|
32
34
|
|
|
33
35
|
type Row = Record<string, unknown>
|
|
@@ -190,7 +192,7 @@ async function run(options: RunOptions = {}): Promise<Harness> {
|
|
|
190
192
|
|
|
191
193
|
const token = options.token === undefined ? { role: 'admin', id: 'u1' } : options.token
|
|
192
194
|
const handler = evaluateEmittedModule<(req: unknown, res: unknown) => Promise<void>>(
|
|
193
|
-
generateInvoiceRegenerateRouteCode(),
|
|
195
|
+
generateInvoiceRegenerateRouteCode(SETTINGS),
|
|
194
196
|
createInvoiceRouteRequire(SETTINGS, {
|
|
195
197
|
dataAccess,
|
|
196
198
|
pdfGenerator,
|
|
@@ -333,9 +335,9 @@ describe('/api/invoices/[id]/regenerate', () => {
|
|
|
333
335
|
})
|
|
334
336
|
})
|
|
335
337
|
|
|
336
|
-
it('never emails the customer', async () => {
|
|
338
|
+
it('never emails the customer of a store that does not email invoices', async () => {
|
|
337
339
|
// The route does not even load the sender: a require of it would throw here.
|
|
338
|
-
const code = generateInvoiceRegenerateRouteCode()
|
|
340
|
+
const code = generateInvoiceRegenerateRouteCode(SETTINGS)
|
|
339
341
|
expect(code).not.toContain('email-sender')
|
|
340
342
|
expect(code).not.toContain('sendInvoiceEmail')
|
|
341
343
|
})
|
|
@@ -402,7 +404,7 @@ describe('/api/invoices/generate — remembers where its PDF is stored', () => {
|
|
|
402
404
|
invoiceData.invoiceNumber = 'INV-0009'
|
|
403
405
|
return { invoice: invoiceData, created: true }
|
|
404
406
|
},
|
|
405
|
-
storeInvoicePdf: async () => ({}),
|
|
407
|
+
storeInvoicePdf: async () => ({ firstPdf: true }),
|
|
406
408
|
updateInvoice: async (id: string, fields: Row) => {
|
|
407
409
|
updates.push({ id, fields })
|
|
408
410
|
return fields
|
|
@@ -86,7 +86,7 @@ async function runHandler(order: Row, items: Row[], body: Row): Promise<HandlerR
|
|
|
86
86
|
invoice = invoiceData
|
|
87
87
|
return { invoice: invoiceData, created: true }
|
|
88
88
|
},
|
|
89
|
-
storeInvoicePdf: async () => ({}),
|
|
89
|
+
storeInvoicePdf: async () => ({ firstPdf: true }),
|
|
90
90
|
updateInvoice: async () => ({}),
|
|
91
91
|
}
|
|
92
92
|
const pdfGeneratorStub = {
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
import type { UIDLWorkflow, UIDLWorkflowEdge, UIDLWorkflowNode } from '@teleporthq/teleport-types'
|
|
2
|
+
import { splitIntoSegments } from '../src/segment-splitter'
|
|
3
|
+
import type { WorkflowSegment } from '../src/types'
|
|
4
|
+
|
|
5
|
+
// Every server segment the browser reaches is one awaited round trip. A
|
|
6
|
+
// checkout is one long chain of server steps with a refusal branch (a toast, a
|
|
7
|
+
// reset) at almost every gate, all at the same depth as the chain's next step.
|
|
8
|
+
// Ordered by depth alone, each refusal step sat between two server steps of the
|
|
9
|
+
// chain and cut it into one more round trip; the loop over the cart lines held
|
|
10
|
+
// everything after it in a depth-only order. Built here the way the editor's
|
|
11
|
+
// checkout builds Place Order, with synthetic ids and labels.
|
|
12
|
+
|
|
13
|
+
type Kind = 'client' | 'server' | 'gate' | 'loop' | 'custom-node' | 'navigate'
|
|
14
|
+
|
|
15
|
+
const TYPE: Record<Kind, string> = {
|
|
16
|
+
client: 'state-update-local-state',
|
|
17
|
+
server: 'data-raw-query',
|
|
18
|
+
gate: 'general-if-statement',
|
|
19
|
+
loop: 'general-loop',
|
|
20
|
+
'custom-node': 'general-custom-node',
|
|
21
|
+
navigate: 'navigation-go-to-page',
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const ENV: Record<Kind, UIDLWorkflowNode['executionEnv']> = {
|
|
25
|
+
client: 'client',
|
|
26
|
+
server: 'server',
|
|
27
|
+
gate: 'universal',
|
|
28
|
+
loop: 'universal',
|
|
29
|
+
'custom-node': 'universal',
|
|
30
|
+
navigate: 'client',
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const buildWorkflow = (
|
|
34
|
+
steps: Array<[string, Kind]>,
|
|
35
|
+
links: Array<[string, string, string?, string?]>
|
|
36
|
+
): UIDLWorkflow => {
|
|
37
|
+
const edges: UIDLWorkflowEdge[] = links.map(([source, target, sourceHandle, targetHandle]) => ({
|
|
38
|
+
id: `${source}->${target}`,
|
|
39
|
+
source,
|
|
40
|
+
target,
|
|
41
|
+
...(sourceHandle ? { sourceHandle } : {}),
|
|
42
|
+
...(targetHandle ? { targetHandle } : {}),
|
|
43
|
+
}))
|
|
44
|
+
// The editor's step numbers: breadth-first depth from the trigger.
|
|
45
|
+
const depth = new Map<string, number>()
|
|
46
|
+
const queue: Array<[string, number]> = [['trigger', 0]]
|
|
47
|
+
while (queue.length > 0) {
|
|
48
|
+
const [id, d] = queue.shift() as [string, number]
|
|
49
|
+
if (depth.has(id)) {
|
|
50
|
+
continue
|
|
51
|
+
}
|
|
52
|
+
depth.set(id, d)
|
|
53
|
+
edges.filter((e) => e.source === id).forEach((e) => queue.push([e.target, d + 1]))
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
id: 'place-order',
|
|
57
|
+
name: 'Place Order',
|
|
58
|
+
trigger: { type: 'event-form-submitted', nodeId: 'trigger' },
|
|
59
|
+
nodes: steps.map(([id, kind]) => ({
|
|
60
|
+
id,
|
|
61
|
+
type: TYPE[kind],
|
|
62
|
+
label: id,
|
|
63
|
+
config: {},
|
|
64
|
+
executionEnv: ENV[kind],
|
|
65
|
+
stepNumber: depth.get(id) || 0,
|
|
66
|
+
})),
|
|
67
|
+
edges,
|
|
68
|
+
} as unknown as UIDLWorkflow
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// A gate whose condition reads a server step, its refusal branch three or four
|
|
72
|
+
// browser steps long, its accepted branch the rest of the chain.
|
|
73
|
+
const placeOrder = buildWorkflow(
|
|
74
|
+
[
|
|
75
|
+
['setBusy', 'client'],
|
|
76
|
+
['formValid', 'gate'],
|
|
77
|
+
['formToast', 'client'],
|
|
78
|
+
['fetchStock', 'server'],
|
|
79
|
+
['checkStock', 'server'],
|
|
80
|
+
['inStock', 'gate'],
|
|
81
|
+
['stockToast', 'client'],
|
|
82
|
+
['stockReset', 'client'],
|
|
83
|
+
['resolveUser', 'custom-node'],
|
|
84
|
+
['loadDiscounts', 'server'],
|
|
85
|
+
['countOrders', 'server'],
|
|
86
|
+
['recheckVoucher', 'server'],
|
|
87
|
+
['voucherValid', 'gate'],
|
|
88
|
+
['forgetVoucher', 'client'],
|
|
89
|
+
['voucherToast', 'client'],
|
|
90
|
+
['voucherReset', 'client'],
|
|
91
|
+
['voucherClear', 'client'],
|
|
92
|
+
['priceOrder', 'server'],
|
|
93
|
+
['assemble', 'server'],
|
|
94
|
+
['checkAddress', 'server'],
|
|
95
|
+
['canBill', 'gate'],
|
|
96
|
+
['billToast', 'client'],
|
|
97
|
+
['billReset', 'client'],
|
|
98
|
+
['createOrder', 'server'],
|
|
99
|
+
['redeem', 'server'],
|
|
100
|
+
['redeemed', 'gate'],
|
|
101
|
+
['cancelOrder', 'server'],
|
|
102
|
+
['redeemToast', 'client'],
|
|
103
|
+
['orderNumber', 'server'],
|
|
104
|
+
['eachLine', 'loop'],
|
|
105
|
+
['buildLine', 'server'],
|
|
106
|
+
['saveLine', 'server'],
|
|
107
|
+
['decrementStock', 'server'],
|
|
108
|
+
['recordPlaced', 'custom-node'],
|
|
109
|
+
['saveDetails', 'server'],
|
|
110
|
+
['settled', 'gate'],
|
|
111
|
+
['markSettled', 'server'],
|
|
112
|
+
['clearCart', 'client'],
|
|
113
|
+
['emailPayload', 'server'],
|
|
114
|
+
['sendEmail', 'server'],
|
|
115
|
+
['goToOrder', 'navigate'],
|
|
116
|
+
['redirectUrls', 'server'],
|
|
117
|
+
['charge', 'server'],
|
|
118
|
+
['opened', 'gate'],
|
|
119
|
+
['cancelRefused', 'server'],
|
|
120
|
+
['refusedToast', 'client'],
|
|
121
|
+
['recordCheckout', 'server'],
|
|
122
|
+
['sendToCheckout', 'navigate'],
|
|
123
|
+
],
|
|
124
|
+
[
|
|
125
|
+
['trigger', 'setBusy'],
|
|
126
|
+
['setBusy', 'formValid'],
|
|
127
|
+
['formValid', 'formToast', 'false'],
|
|
128
|
+
['formValid', 'fetchStock', 'true'],
|
|
129
|
+
['fetchStock', 'checkStock'],
|
|
130
|
+
['checkStock', 'inStock'],
|
|
131
|
+
['inStock', 'stockToast', 'false'],
|
|
132
|
+
['stockToast', 'stockReset'],
|
|
133
|
+
['inStock', 'resolveUser', 'true'],
|
|
134
|
+
['resolveUser', 'loadDiscounts'],
|
|
135
|
+
['loadDiscounts', 'countOrders'],
|
|
136
|
+
['countOrders', 'recheckVoucher'],
|
|
137
|
+
['recheckVoucher', 'voucherValid'],
|
|
138
|
+
['voucherValid', 'forgetVoucher', 'false'],
|
|
139
|
+
['forgetVoucher', 'voucherToast'],
|
|
140
|
+
['voucherToast', 'voucherReset'],
|
|
141
|
+
['voucherReset', 'voucherClear'],
|
|
142
|
+
['voucherValid', 'priceOrder', 'true'],
|
|
143
|
+
['priceOrder', 'assemble'],
|
|
144
|
+
['assemble', 'checkAddress'],
|
|
145
|
+
['checkAddress', 'canBill'],
|
|
146
|
+
['canBill', 'billToast', 'false'],
|
|
147
|
+
['billToast', 'billReset'],
|
|
148
|
+
['canBill', 'createOrder', 'true'],
|
|
149
|
+
['createOrder', 'redeem'],
|
|
150
|
+
['redeem', 'redeemed'],
|
|
151
|
+
['redeemed', 'cancelOrder', 'false'],
|
|
152
|
+
['cancelOrder', 'redeemToast'],
|
|
153
|
+
['redeemed', 'orderNumber', 'true'],
|
|
154
|
+
['orderNumber', 'eachLine'],
|
|
155
|
+
['eachLine', 'buildLine', 'loop', 'loop-body-in'],
|
|
156
|
+
['buildLine', 'saveLine'],
|
|
157
|
+
['saveLine', 'eachLine', 'loop-body-out', 'loop-back'],
|
|
158
|
+
['eachLine', 'decrementStock', 'exit'],
|
|
159
|
+
['decrementStock', 'recordPlaced'],
|
|
160
|
+
['recordPlaced', 'saveDetails'],
|
|
161
|
+
['saveDetails', 'settled'],
|
|
162
|
+
['settled', 'markSettled', 'true'],
|
|
163
|
+
['markSettled', 'clearCart'],
|
|
164
|
+
['clearCart', 'emailPayload'],
|
|
165
|
+
['emailPayload', 'sendEmail'],
|
|
166
|
+
['sendEmail', 'goToOrder'],
|
|
167
|
+
['settled', 'redirectUrls', 'false'],
|
|
168
|
+
['redirectUrls', 'charge'],
|
|
169
|
+
['charge', 'opened'],
|
|
170
|
+
['opened', 'cancelRefused', 'false'],
|
|
171
|
+
['cancelRefused', 'refusedToast'],
|
|
172
|
+
['opened', 'recordCheckout', 'true'],
|
|
173
|
+
['recordCheckout', 'sendToCheckout'],
|
|
174
|
+
]
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
const REFUSALS = [
|
|
178
|
+
'formToast',
|
|
179
|
+
'stockToast',
|
|
180
|
+
'stockReset',
|
|
181
|
+
'forgetVoucher',
|
|
182
|
+
'voucherToast',
|
|
183
|
+
'voucherReset',
|
|
184
|
+
'voucherClear',
|
|
185
|
+
'billToast',
|
|
186
|
+
'billReset',
|
|
187
|
+
'cancelOrder',
|
|
188
|
+
'redeemToast',
|
|
189
|
+
'cancelRefused',
|
|
190
|
+
'refusedToast',
|
|
191
|
+
]
|
|
192
|
+
const CASH_ONLY = ['markSettled', 'clearCart', 'emailPayload', 'sendEmail', 'goToOrder']
|
|
193
|
+
const ONLINE_ONLY = ['redirectUrls', 'charge', 'opened', 'recordCheckout', 'sendToCheckout']
|
|
194
|
+
|
|
195
|
+
// The server segments an order that every gate accepts calls, in order.
|
|
196
|
+
const roundTrips = (segments: WorkflowSegment[], skipped: string[]): string[][] =>
|
|
197
|
+
segments
|
|
198
|
+
.filter((s) => s.env === 'server')
|
|
199
|
+
.map((s) => s.nodes.map((n) => n.id).filter((id) => !skipped.includes(id)))
|
|
200
|
+
.filter((ids) => ids.length > 0)
|
|
201
|
+
|
|
202
|
+
const segmentOf = (segments: WorkflowSegment[], id: string): WorkflowSegment =>
|
|
203
|
+
segments.find((s) => s.nodes.some((n) => n.id === id)) as WorkflowSegment
|
|
204
|
+
|
|
205
|
+
describe('splitIntoSegments — a checkout waits on as few server round trips as it can', () => {
|
|
206
|
+
const segments = splitIntoSegments(placeOrder)
|
|
207
|
+
|
|
208
|
+
it('calls the server once between each pair of steps that need the browser', () => {
|
|
209
|
+
// Browser steps on the accepted path: resolveUser (custom node) and
|
|
210
|
+
// recordPlaced (custom node) — plus clearCart for a cash order.
|
|
211
|
+
expect(roundTrips(segments, [...REFUSALS, ...ONLINE_ONLY])).toEqual([
|
|
212
|
+
['fetchStock', 'checkStock', 'inStock'],
|
|
213
|
+
[
|
|
214
|
+
'loadDiscounts',
|
|
215
|
+
'countOrders',
|
|
216
|
+
'recheckVoucher',
|
|
217
|
+
'voucherValid',
|
|
218
|
+
'priceOrder',
|
|
219
|
+
'assemble',
|
|
220
|
+
'checkAddress',
|
|
221
|
+
'canBill',
|
|
222
|
+
'createOrder',
|
|
223
|
+
'redeem',
|
|
224
|
+
'redeemed',
|
|
225
|
+
'orderNumber',
|
|
226
|
+
'eachLine',
|
|
227
|
+
'buildLine',
|
|
228
|
+
'saveLine',
|
|
229
|
+
'decrementStock',
|
|
230
|
+
],
|
|
231
|
+
['saveDetails', 'settled', 'markSettled'],
|
|
232
|
+
['emailPayload', 'sendEmail'],
|
|
233
|
+
])
|
|
234
|
+
expect(roundTrips(segments, [...REFUSALS, ...CASH_ONLY])).toHaveLength(3)
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
it('decides every gate that reads a server step inside the server route', () => {
|
|
238
|
+
// A gate the browser decides is one a crafted request can answer: the
|
|
239
|
+
// route then takes the browser's word for which branch was skipped.
|
|
240
|
+
for (const gate of ['inStock', 'voucherValid', 'canBill', 'redeemed', 'settled', 'opened']) {
|
|
241
|
+
expect(segmentOf(segments, gate).env).toBe('server')
|
|
242
|
+
}
|
|
243
|
+
// A refusal step on the server shares its gate's route, which skips it itself.
|
|
244
|
+
expect(segmentOf(segments, 'cancelOrder')).toBe(segmentOf(segments, 'redeemed'))
|
|
245
|
+
expect(segmentOf(segments, 'cancelRefused')).toBe(segmentOf(segments, 'opened'))
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
it('keeps the loop and its body in one segment, in order', () => {
|
|
249
|
+
const loopSegment = segmentOf(segments, 'eachLine')
|
|
250
|
+
const ids = loopSegment.nodes.map((n) => n.id)
|
|
251
|
+
expect(ids.slice(ids.indexOf('eachLine'), ids.indexOf('eachLine') + 3)).toEqual([
|
|
252
|
+
'eachLine',
|
|
253
|
+
'buildLine',
|
|
254
|
+
'saveLine',
|
|
255
|
+
])
|
|
256
|
+
expect(ids.indexOf('decrementStock')).toBeGreaterThan(ids.indexOf('saveLine'))
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
it('runs every step after everything it depends on', () => {
|
|
260
|
+
const order = segments.flatMap((s) => s.nodes.map((n) => n.id))
|
|
261
|
+
for (const e of placeOrder.edges) {
|
|
262
|
+
if (e.targetHandle === 'loop-back' || e.source === 'trigger') {
|
|
263
|
+
continue
|
|
264
|
+
}
|
|
265
|
+
expect([e.source, order.indexOf(e.source) < order.indexOf(e.target)]).toEqual([
|
|
266
|
+
e.source,
|
|
267
|
+
true,
|
|
268
|
+
])
|
|
269
|
+
}
|
|
270
|
+
})
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
describe('splitIntoSegments — a server route runs its steps by step number', () => {
|
|
274
|
+
it('never puts a step in the segment of an input with a later step number', () => {
|
|
275
|
+
// `total` hangs off the trigger (step 1) but reads `line3` (step 3): the
|
|
276
|
+
// route, which sorts its segment by step, would run it before that value.
|
|
277
|
+
const workflow = {
|
|
278
|
+
id: 'w',
|
|
279
|
+
name: 'w',
|
|
280
|
+
trigger: { type: 'event-element-clicked', nodeId: 'trigger' },
|
|
281
|
+
nodes: [
|
|
282
|
+
{ id: 'line1', type: 'data-select', stepNumber: 1, executionEnv: 'server', config: {} },
|
|
283
|
+
{ id: 'line2', type: 'data-select', stepNumber: 2, executionEnv: 'server', config: {} },
|
|
284
|
+
{ id: 'line3', type: 'data-select', stepNumber: 3, executionEnv: 'server', config: {} },
|
|
285
|
+
{
|
|
286
|
+
id: 'total',
|
|
287
|
+
type: 'data-create-item',
|
|
288
|
+
stepNumber: 1,
|
|
289
|
+
executionEnv: 'server',
|
|
290
|
+
config: { columnMappings: [{ value: { type: 'workflowContext', nodeId: 'line3' } }] },
|
|
291
|
+
},
|
|
292
|
+
],
|
|
293
|
+
edges: [
|
|
294
|
+
{ id: 'e0', source: 'trigger', target: 'line1' },
|
|
295
|
+
{ id: 'e1', source: 'line1', target: 'line2' },
|
|
296
|
+
{ id: 'e2', source: 'line2', target: 'line3' },
|
|
297
|
+
{ id: 'e3', source: 'trigger', target: 'total' },
|
|
298
|
+
],
|
|
299
|
+
} as unknown as UIDLWorkflow
|
|
300
|
+
const segments = splitIntoSegments(workflow)
|
|
301
|
+
expect(segments.map((s) => [s.env, s.nodes.map((n) => n.id)])).toEqual([
|
|
302
|
+
['server', ['line1', 'line2', 'line3']],
|
|
303
|
+
['server', ['total']],
|
|
304
|
+
])
|
|
305
|
+
})
|
|
306
|
+
})
|