@teleporthq/teleport-plugin-next-workflows 0.43.75 → 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.
Files changed (248) hide show
  1. package/__tests__/_helpers/fake-store-database.ts +428 -0
  2. package/__tests__/_helpers/invoice-store.ts +382 -0
  3. package/__tests__/booking-calendar-feed-route.test.ts +494 -0
  4. package/__tests__/custom-node-result-taken-branch.test.ts +107 -0
  5. package/__tests__/data-api-date-parser.test.ts +45 -0
  6. package/__tests__/email-locale-senders.test.ts +3 -1
  7. package/__tests__/email-sender-name.test.ts +513 -0
  8. package/__tests__/graph-utils-topological-order.test.ts +155 -1
  9. package/__tests__/invoice-billing-details.test.ts +384 -0
  10. package/__tests__/invoice-data-access-replace.test.ts +236 -2
  11. package/__tests__/invoice-order-items-settle.test.ts +1 -1
  12. package/__tests__/invoice-payment-email.test.ts +191 -0
  13. package/__tests__/invoice-regenerate-email.test.ts +288 -0
  14. package/__tests__/invoice-regenerate-route.test.ts +9 -7
  15. package/__tests__/invoice-reservation.test.ts +1 -1
  16. package/__tests__/invoice-tender.test.ts +1 -1
  17. package/__tests__/middleware-admin-panel-roles.test.ts +127 -0
  18. package/__tests__/page-arrival-remount.test.ts +247 -0
  19. package/__tests__/payment-drivers-module.test.ts +19 -0
  20. package/__tests__/raw-query-error-code.test.ts +47 -0
  21. package/__tests__/runtime-template-token-resolution.test.ts +16 -0
  22. package/__tests__/segment-reply-changed-keys.test.ts +31 -0
  23. package/__tests__/segment-route-skipped-branch.test.ts +101 -0
  24. package/__tests__/segment-splitter-checkout-order.test.ts +306 -0
  25. package/__tests__/streaming-on-end-env-filter.test.ts +18 -18
  26. package/__tests__/streaming-on-end-order.test.ts +387 -0
  27. package/dist/cjs/account-delete-route-generator.d.ts +1 -0
  28. package/dist/cjs/account-delete-route-generator.d.ts.map +1 -1
  29. package/dist/cjs/account-delete-route-generator.js +2 -1
  30. package/dist/cjs/account-delete-route-generator.js.map +1 -1
  31. package/dist/cjs/api-route-generator.d.ts.map +1 -1
  32. package/dist/cjs/api-route-generator.js +23 -13
  33. package/dist/cjs/api-route-generator.js.map +1 -1
  34. package/dist/cjs/auth-generator.d.ts +1 -0
  35. package/dist/cjs/auth-generator.d.ts.map +1 -1
  36. package/dist/cjs/auth-generator.js +77 -28
  37. package/dist/cjs/auth-generator.js.map +1 -1
  38. package/dist/cjs/booking-calendar-feed-route-generator.d.ts +28 -0
  39. package/dist/cjs/booking-calendar-feed-route-generator.d.ts.map +1 -0
  40. package/dist/cjs/booking-calendar-feed-route-generator.js +296 -0
  41. package/dist/cjs/booking-calendar-feed-route-generator.js.map +1 -0
  42. package/dist/cjs/data-api-route-generator.d.ts.map +1 -1
  43. package/dist/cjs/data-api-route-generator.js +6 -1
  44. package/dist/cjs/data-api-route-generator.js.map +1 -1
  45. package/dist/cjs/executor-generator.d.ts.map +1 -1
  46. package/dist/cjs/executor-generator.js +39 -28
  47. package/dist/cjs/executor-generator.js.map +1 -1
  48. package/dist/cjs/graph-utils.d.ts +31 -1
  49. package/dist/cjs/graph-utils.d.ts.map +1 -1
  50. package/dist/cjs/graph-utils.js +112 -10
  51. package/dist/cjs/graph-utils.js.map +1 -1
  52. package/dist/cjs/index.d.ts +1 -1
  53. package/dist/cjs/index.d.ts.map +1 -1
  54. package/dist/cjs/index.js +2 -1
  55. package/dist/cjs/index.js.map +1 -1
  56. package/dist/cjs/invoice/api-routes-code.d.ts +12 -3
  57. package/dist/cjs/invoice/api-routes-code.d.ts.map +1 -1
  58. package/dist/cjs/invoice/api-routes-code.js +152 -21
  59. package/dist/cjs/invoice/api-routes-code.js.map +1 -1
  60. package/dist/cjs/invoice/data-access-code.d.ts.map +1 -1
  61. package/dist/cjs/invoice/data-access-code.js +133 -21
  62. package/dist/cjs/invoice/data-access-code.js.map +1 -1
  63. package/dist/cjs/invoice/email-sender-code.d.ts.map +1 -1
  64. package/dist/cjs/invoice/email-sender-code.js +15 -25
  65. package/dist/cjs/invoice/email-sender-code.js.map +1 -1
  66. package/dist/cjs/invoice/index.js +1 -1
  67. package/dist/cjs/invoice/index.js.map +1 -1
  68. package/dist/cjs/invoice/invoice-assembly-code.d.ts.map +1 -1
  69. package/dist/cjs/invoice/invoice-assembly-code.js +50 -6
  70. package/dist/cjs/invoice/invoice-assembly-code.js.map +1 -1
  71. package/dist/cjs/invoice/invoice-html-code.d.ts.map +1 -1
  72. package/dist/cjs/invoice/invoice-html-code.js +13 -2
  73. package/dist/cjs/invoice/invoice-html-code.js.map +1 -1
  74. package/dist/cjs/nodes/data/data-raw-query.d.ts.map +1 -1
  75. package/dist/cjs/nodes/data/data-raw-query.js +11 -1
  76. package/dist/cjs/nodes/data/data-raw-query.js.map +1 -1
  77. package/dist/cjs/nodes/email/email-mailgun.d.ts.map +1 -1
  78. package/dist/cjs/nodes/email/email-mailgun.js +4 -2
  79. package/dist/cjs/nodes/email/email-mailgun.js.map +1 -1
  80. package/dist/cjs/nodes/email/email-postmark.d.ts.map +1 -1
  81. package/dist/cjs/nodes/email/email-postmark.js +5 -2
  82. package/dist/cjs/nodes/email/email-postmark.js.map +1 -1
  83. package/dist/cjs/nodes/email/email-resend.d.ts.map +1 -1
  84. package/dist/cjs/nodes/email/email-resend.js +5 -2
  85. package/dist/cjs/nodes/email/email-resend.js.map +1 -1
  86. package/dist/cjs/nodes/email/email-sendgrid.d.ts.map +1 -1
  87. package/dist/cjs/nodes/email/email-sendgrid.js +8 -2
  88. package/dist/cjs/nodes/email/email-sendgrid.js.map +1 -1
  89. package/dist/cjs/nodes/general/general-custom-js.d.ts.map +1 -1
  90. package/dist/cjs/nodes/general/general-custom-js.js +11 -0
  91. package/dist/cjs/nodes/general/general-custom-js.js.map +1 -1
  92. package/dist/cjs/nodes/storage/storage-local-remove.d.ts.map +1 -1
  93. package/dist/cjs/nodes/storage/storage-local-remove.js +2 -0
  94. package/dist/cjs/nodes/storage/storage-local-remove.js.map +1 -1
  95. package/dist/cjs/nodes/storage/storage-local-set.d.ts.map +1 -1
  96. package/dist/cjs/nodes/storage/storage-local-set.js +4 -0
  97. package/dist/cjs/nodes/storage/storage-local-set.js.map +1 -1
  98. package/dist/cjs/page-arrival.d.ts +10 -0
  99. package/dist/cjs/page-arrival.d.ts.map +1 -0
  100. package/dist/cjs/page-arrival.js +86 -0
  101. package/dist/cjs/page-arrival.js.map +1 -0
  102. package/dist/cjs/payments/index.d.ts +10 -4
  103. package/dist/cjs/payments/index.d.ts.map +1 -1
  104. package/dist/cjs/payments/index.js +18 -7
  105. package/dist/cjs/payments/index.js.map +1 -1
  106. package/dist/cjs/segment-splitter.d.ts.map +1 -1
  107. package/dist/cjs/segment-splitter.js +65 -27
  108. package/dist/cjs/segment-splitter.js.map +1 -1
  109. package/dist/cjs/transactional-email-code.d.ts.map +1 -1
  110. package/dist/cjs/transactional-email-code.js +32 -18
  111. package/dist/cjs/transactional-email-code.js.map +1 -1
  112. package/dist/cjs/tsconfig.tsbuildinfo +1 -1
  113. package/dist/cjs/workflow-component-plugin.d.ts.map +1 -1
  114. package/dist/cjs/workflow-component-plugin.js +35 -149
  115. package/dist/cjs/workflow-component-plugin.js.map +1 -1
  116. package/dist/cjs/workflow-project-plugin.d.ts.map +1 -1
  117. package/dist/cjs/workflow-project-plugin.js +16 -4
  118. package/dist/cjs/workflow-project-plugin.js.map +1 -1
  119. package/dist/cjs/workflow-scope.d.ts +19 -0
  120. package/dist/cjs/workflow-scope.d.ts.map +1 -0
  121. package/dist/cjs/workflow-scope.js +147 -0
  122. package/dist/cjs/workflow-scope.js.map +1 -0
  123. package/dist/esm/account-delete-route-generator.d.ts +1 -0
  124. package/dist/esm/account-delete-route-generator.d.ts.map +1 -1
  125. package/dist/esm/account-delete-route-generator.js +2 -1
  126. package/dist/esm/account-delete-route-generator.js.map +1 -1
  127. package/dist/esm/api-route-generator.d.ts.map +1 -1
  128. package/dist/esm/api-route-generator.js +23 -13
  129. package/dist/esm/api-route-generator.js.map +1 -1
  130. package/dist/esm/auth-generator.d.ts +1 -0
  131. package/dist/esm/auth-generator.d.ts.map +1 -1
  132. package/dist/esm/auth-generator.js +77 -28
  133. package/dist/esm/auth-generator.js.map +1 -1
  134. package/dist/esm/booking-calendar-feed-route-generator.d.ts +28 -0
  135. package/dist/esm/booking-calendar-feed-route-generator.d.ts.map +1 -0
  136. package/dist/esm/booking-calendar-feed-route-generator.js +291 -0
  137. package/dist/esm/booking-calendar-feed-route-generator.js.map +1 -0
  138. package/dist/esm/data-api-route-generator.d.ts.map +1 -1
  139. package/dist/esm/data-api-route-generator.js +6 -1
  140. package/dist/esm/data-api-route-generator.js.map +1 -1
  141. package/dist/esm/executor-generator.d.ts.map +1 -1
  142. package/dist/esm/executor-generator.js +39 -28
  143. package/dist/esm/executor-generator.js.map +1 -1
  144. package/dist/esm/graph-utils.d.ts +31 -1
  145. package/dist/esm/graph-utils.d.ts.map +1 -1
  146. package/dist/esm/graph-utils.js +109 -9
  147. package/dist/esm/graph-utils.js.map +1 -1
  148. package/dist/esm/index.d.ts +1 -1
  149. package/dist/esm/index.d.ts.map +1 -1
  150. package/dist/esm/index.js +1 -1
  151. package/dist/esm/index.js.map +1 -1
  152. package/dist/esm/invoice/api-routes-code.d.ts +12 -3
  153. package/dist/esm/invoice/api-routes-code.d.ts.map +1 -1
  154. package/dist/esm/invoice/api-routes-code.js +152 -21
  155. package/dist/esm/invoice/api-routes-code.js.map +1 -1
  156. package/dist/esm/invoice/data-access-code.d.ts.map +1 -1
  157. package/dist/esm/invoice/data-access-code.js +133 -21
  158. package/dist/esm/invoice/data-access-code.js.map +1 -1
  159. package/dist/esm/invoice/email-sender-code.d.ts.map +1 -1
  160. package/dist/esm/invoice/email-sender-code.js +15 -25
  161. package/dist/esm/invoice/email-sender-code.js.map +1 -1
  162. package/dist/esm/invoice/index.js +1 -1
  163. package/dist/esm/invoice/index.js.map +1 -1
  164. package/dist/esm/invoice/invoice-assembly-code.d.ts.map +1 -1
  165. package/dist/esm/invoice/invoice-assembly-code.js +50 -6
  166. package/dist/esm/invoice/invoice-assembly-code.js.map +1 -1
  167. package/dist/esm/invoice/invoice-html-code.d.ts.map +1 -1
  168. package/dist/esm/invoice/invoice-html-code.js +13 -2
  169. package/dist/esm/invoice/invoice-html-code.js.map +1 -1
  170. package/dist/esm/nodes/data/data-raw-query.d.ts.map +1 -1
  171. package/dist/esm/nodes/data/data-raw-query.js +11 -1
  172. package/dist/esm/nodes/data/data-raw-query.js.map +1 -1
  173. package/dist/esm/nodes/email/email-mailgun.d.ts.map +1 -1
  174. package/dist/esm/nodes/email/email-mailgun.js +4 -2
  175. package/dist/esm/nodes/email/email-mailgun.js.map +1 -1
  176. package/dist/esm/nodes/email/email-postmark.d.ts.map +1 -1
  177. package/dist/esm/nodes/email/email-postmark.js +5 -2
  178. package/dist/esm/nodes/email/email-postmark.js.map +1 -1
  179. package/dist/esm/nodes/email/email-resend.d.ts.map +1 -1
  180. package/dist/esm/nodes/email/email-resend.js +5 -2
  181. package/dist/esm/nodes/email/email-resend.js.map +1 -1
  182. package/dist/esm/nodes/email/email-sendgrid.d.ts.map +1 -1
  183. package/dist/esm/nodes/email/email-sendgrid.js +8 -2
  184. package/dist/esm/nodes/email/email-sendgrid.js.map +1 -1
  185. package/dist/esm/nodes/general/general-custom-js.d.ts.map +1 -1
  186. package/dist/esm/nodes/general/general-custom-js.js +11 -0
  187. package/dist/esm/nodes/general/general-custom-js.js.map +1 -1
  188. package/dist/esm/nodes/storage/storage-local-remove.d.ts.map +1 -1
  189. package/dist/esm/nodes/storage/storage-local-remove.js +2 -0
  190. package/dist/esm/nodes/storage/storage-local-remove.js.map +1 -1
  191. package/dist/esm/nodes/storage/storage-local-set.d.ts.map +1 -1
  192. package/dist/esm/nodes/storage/storage-local-set.js +4 -0
  193. package/dist/esm/nodes/storage/storage-local-set.js.map +1 -1
  194. package/dist/esm/page-arrival.d.ts +10 -0
  195. package/dist/esm/page-arrival.d.ts.map +1 -0
  196. package/dist/esm/page-arrival.js +81 -0
  197. package/dist/esm/page-arrival.js.map +1 -0
  198. package/dist/esm/payments/index.d.ts +10 -4
  199. package/dist/esm/payments/index.d.ts.map +1 -1
  200. package/dist/esm/payments/index.js +16 -6
  201. package/dist/esm/payments/index.js.map +1 -1
  202. package/dist/esm/segment-splitter.d.ts.map +1 -1
  203. package/dist/esm/segment-splitter.js +66 -28
  204. package/dist/esm/segment-splitter.js.map +1 -1
  205. package/dist/esm/transactional-email-code.d.ts.map +1 -1
  206. package/dist/esm/transactional-email-code.js +32 -18
  207. package/dist/esm/transactional-email-code.js.map +1 -1
  208. package/dist/esm/tsconfig.tsbuildinfo +1 -1
  209. package/dist/esm/workflow-component-plugin.d.ts.map +1 -1
  210. package/dist/esm/workflow-component-plugin.js +32 -146
  211. package/dist/esm/workflow-component-plugin.js.map +1 -1
  212. package/dist/esm/workflow-project-plugin.d.ts.map +1 -1
  213. package/dist/esm/workflow-project-plugin.js +16 -4
  214. package/dist/esm/workflow-project-plugin.js.map +1 -1
  215. package/dist/esm/workflow-scope.d.ts +19 -0
  216. package/dist/esm/workflow-scope.d.ts.map +1 -0
  217. package/dist/esm/workflow-scope.js +142 -0
  218. package/dist/esm/workflow-scope.js.map +1 -0
  219. package/package.json +4 -4
  220. package/src/account-delete-route-generator.ts +3 -1
  221. package/src/api-route-generator.ts +23 -13
  222. package/src/auth-generator.ts +83 -26
  223. package/src/booking-calendar-feed-route-generator.ts +293 -0
  224. package/src/data-api-route-generator.ts +6 -1
  225. package/src/executor-generator.ts +39 -28
  226. package/src/graph-utils.ts +143 -11
  227. package/src/index.ts +1 -0
  228. package/src/invoice/api-routes-code.ts +166 -21
  229. package/src/invoice/data-access-code.ts +139 -21
  230. package/src/invoice/email-sender-code.ts +15 -25
  231. package/src/invoice/index.ts +1 -1
  232. package/src/invoice/invoice-assembly-code.ts +50 -6
  233. package/src/invoice/invoice-html-code.ts +13 -2
  234. package/src/nodes/data/data-raw-query.ts +11 -1
  235. package/src/nodes/email/email-mailgun.ts +11 -2
  236. package/src/nodes/email/email-postmark.ts +12 -2
  237. package/src/nodes/email/email-resend.ts +12 -2
  238. package/src/nodes/email/email-sendgrid.ts +15 -2
  239. package/src/nodes/general/general-custom-js.ts +10 -0
  240. package/src/nodes/storage/storage-local-remove.ts +4 -0
  241. package/src/nodes/storage/storage-local-set.ts +6 -0
  242. package/src/page-arrival.ts +115 -0
  243. package/src/payments/index.ts +18 -5
  244. package/src/segment-splitter.ts +90 -36
  245. package/src/transactional-email-code.ts +32 -21
  246. package/src/workflow-component-plugin.ts +43 -188
  247. package/src/workflow-project-plugin.ts +17 -4
  248. package/src/workflow-scope.ts +184 -0
@@ -44,24 +44,40 @@ export const collectWorkflowContextNodeIds = (value: unknown, out: Set<string>):
44
44
  Object.values(record).forEach((child) => collectWorkflowContextNodeIds(child, out))
45
45
  }
46
46
 
47
- export const getTopologicalOrder = (
47
+ /**
48
+ * The edge that sends a loop body back to its loop for the next item. The loop
49
+ * does not wait for it: counted as a dependency it left the loop, and every
50
+ * node after it, waiting forever.
51
+ */
52
+ const isLoopBackEdge = (edge: UIDLWorkflowEdge): boolean => edge.targetHandle === 'loop-back'
53
+
54
+ /**
55
+ * Where a node runs once the segment splitter places it, or `null` for a node
56
+ * that runs wherever the node before it ran (an if-statement), so it never
57
+ * forces a switch between browser and server.
58
+ */
59
+ export type ExecutionSide = 'client' | 'server'
60
+ export type ExecutionSideOf = (node: UIDLWorkflowNode) => ExecutionSide | null
61
+
62
+ /**
63
+ * Every dependency each node waits for: the source of each edge into it (but a
64
+ * loop's back edge) and every node whose output it reads through a
65
+ * `workflowContext` reference. Listed per SOURCE, in edge order then reference
66
+ * order — the order the topological sort below releases them in.
67
+ */
68
+ export const getDependents = (
48
69
  nodes: UIDLWorkflowNode[],
49
70
  edges: UIDLWorkflowEdge[]
50
- ): string[] => {
71
+ ): Record<string, string[]> => {
51
72
  const nodeIds = new Set(nodes.map((n) => n.id))
52
- const inDegree: Record<string, number> = {}
53
73
  const adjacency: Record<string, string[]> = {}
54
-
55
- const nodeIdArr = Array.from(nodeIds)
56
- nodeIdArr.forEach((id) => {
57
- inDegree[id] = 0
74
+ nodeIds.forEach((id) => {
58
75
  adjacency[id] = []
59
76
  })
60
77
 
61
78
  edges.forEach((edge) => {
62
- if (nodeIds.has(edge.source) && nodeIds.has(edge.target)) {
79
+ if (nodeIds.has(edge.source) && nodeIds.has(edge.target) && !isLoopBackEdge(edge)) {
63
80
  adjacency[edge.source].push(edge.target)
64
- inDegree[edge.target]++
65
81
  }
66
82
  })
67
83
 
@@ -92,10 +108,105 @@ export const getTopologicalOrder = (
92
108
  return
93
109
  }
94
110
  adjacency[refId].push(node.id)
95
- inDegree[node.id]++
96
111
  })
97
112
  })
98
113
 
114
+ return adjacency
115
+ }
116
+
117
+ /**
118
+ * Every node reachable from `startId` without passing through `parentId` or a
119
+ * loop's back edge: what the runtime skips when `parentId` does not take the
120
+ * branch starting at `startId`.
121
+ */
122
+ const collectBranchNodeIds = (
123
+ startId: string,
124
+ targetsBySource: Map<string, string[]>,
125
+ parentId: string
126
+ ): Set<string> => {
127
+ const reached = new Set<string>()
128
+ const queue = [startId]
129
+ while (queue.length > 0) {
130
+ const current = queue.shift()!
131
+ if (reached.has(current) || current === parentId) {
132
+ continue
133
+ }
134
+ reached.add(current)
135
+ queue.push(...(targetsBySource.get(current) || []))
136
+ }
137
+ return reached
138
+ }
139
+
140
+ /**
141
+ * Whether two nodes can never both run: they lie on different branches of the
142
+ * same if-statement or switch, and the runtime skips everything reachable from
143
+ * the branch it did not take.
144
+ */
145
+ export const getNeverRunTogether = (
146
+ nodes: UIDLWorkflowNode[],
147
+ edges: UIDLWorkflowEdge[]
148
+ ): ((a: string, b: string) => boolean) => {
149
+ const targetsBySource = new Map<string, string[]>()
150
+ edges.forEach((edge) => {
151
+ if (!isLoopBackEdge(edge)) {
152
+ targetsBySource.set(edge.source, [...(targetsBySource.get(edge.source) || []), edge.target])
153
+ }
154
+ })
155
+ const branchesByNodeId = new Map<string, Array<{ gateId: string; branch: string }>>()
156
+ nodes.forEach((gate) => {
157
+ const branchTargets =
158
+ gate.type === 'general-if-statement'
159
+ ? Object.entries(getIfBranches(gate.id, edges))
160
+ : gate.type === 'general-switch'
161
+ ? getSwitchCases(gate.id, edges).map(({ caseId, targetId }) => [caseId, targetId])
162
+ : []
163
+ branchTargets.forEach(([branch, targetId]) => {
164
+ if (!targetId) {
165
+ return
166
+ }
167
+ collectBranchNodeIds(targetId, targetsBySource, gate.id).forEach((id) => {
168
+ const branches = branchesByNodeId.get(id) || []
169
+ branches.push({ gateId: gate.id, branch })
170
+ branchesByNodeId.set(id, branches)
171
+ })
172
+ })
173
+ })
174
+ return (a, b) => {
175
+ const branchesOfB = branchesByNodeId.get(b) || []
176
+ return (branchesByNodeId.get(a) || []).some((x) =>
177
+ branchesOfB.some((y) => x.gateId === y.gateId && x.branch !== y.branch)
178
+ )
179
+ }
180
+ }
181
+
182
+ /**
183
+ * Kahn's order, the lowest `stepNumber` first among the nodes ready to run.
184
+ *
185
+ * With `sideOf` (the segment splitter), a ready node that stays on the side
186
+ * the previous node ran on may go ahead of lower-step nodes on the other side —
187
+ * one client/server switch fewer, so one server round trip fewer — but only of
188
+ * nodes it never runs together with. A refusal branch's toast waits for the
189
+ * main chain's server steps; two steps that both run keep their order, which
190
+ * custom JavaScript reads `params` by.
191
+ */
192
+ export const getTopologicalOrder = (
193
+ nodes: UIDLWorkflowNode[],
194
+ edges: UIDLWorkflowEdge[],
195
+ sideOf?: ExecutionSideOf
196
+ ): string[] => {
197
+ const nodeIds = new Set(nodes.map((n) => n.id))
198
+ const adjacency = getDependents(nodes, edges)
199
+ const inDegree: Record<string, number> = {}
200
+ const nodeIdArr = Array.from(nodeIds)
201
+ nodeIdArr.forEach((id) => {
202
+ inDegree[id] = 0
203
+ })
204
+ nodeIdArr.forEach((id) =>
205
+ adjacency[id].forEach((dependent) => {
206
+ inDegree[dependent]++
207
+ })
208
+ )
209
+
99
210
  const queue: string[] = []
100
211
  nodeIdArr.forEach((id) => {
101
212
  if (inDegree[id] === 0) {
@@ -103,6 +214,26 @@ export const getTopologicalOrder = (
103
214
  }
104
215
  })
105
216
 
217
+ const nodeById = new Map(nodes.map((n) => [n.id, n]))
218
+ const neverRunTogether = sideOf ? getNeverRunTogether(nodes, edges) : null
219
+ let currentSide: ExecutionSide | null = null
220
+ const staysOnCurrentSide = (id: string): boolean => {
221
+ const side = sideOf(nodeById.get(id))
222
+ return side === null || side === currentSide
223
+ }
224
+ const nextIndex = (): number => {
225
+ if (!sideOf || currentSide === null || staysOnCurrentSide(queue[0])) {
226
+ return 0
227
+ }
228
+ const index = queue.findIndex(
229
+ (id, i) =>
230
+ i > 0 &&
231
+ staysOnCurrentSide(id) &&
232
+ queue.slice(0, i).every((ahead) => neverRunTogether(id, ahead))
233
+ )
234
+ return Math.max(index, 0)
235
+ }
236
+
106
237
  const order: string[] = []
107
238
  while (queue.length > 0) {
108
239
  queue.sort((a, b) => {
@@ -110,7 +241,8 @@ export const getTopologicalOrder = (
110
241
  const nodeB = nodes.find((n) => n.id === b)
111
242
  return (nodeA?.stepNumber || 0) - (nodeB?.stepNumber || 0)
112
243
  })
113
- const current = queue.shift()!
244
+ const [current] = queue.splice(nextIndex(), 1)
245
+ currentSide = (sideOf && sideOf(nodeById.get(current))) || currentSide
114
246
  order.push(current)
115
247
 
116
248
  for (const neighbor of adjacency[current] || []) {
package/src/index.ts CHANGED
@@ -57,6 +57,7 @@ export { generateWebhookFiles } from './webhook-generator'
57
57
  export {
58
58
  declarePaymentCredentialEnv,
59
59
  ensurePaymentDriversModule,
60
+ readUidlPaymentProviders,
60
61
  resolveStorePaymentDriverIds,
61
62
  } from './payments'
62
63
  export {
@@ -57,6 +57,42 @@ async function invoiceCaller(req) {
57
57
  };
58
58
  }`
59
59
 
60
+ /**
61
+ * The order-history line of an invoice email the provider accepted: an
62
+ * `order-email-sent` line of the email's purpose, keyed on the invoice, so the
63
+ * email is on its order's history once, whichever route sent it. The strings
64
+ * are data in the merchant's database, owned by teleport-gui's
65
+ * `order-event-types.ts` and `EmailTemplatePurpose.INVOICE_EMAIL`.
66
+ */
67
+ const INVOICE_EMAIL_EVENT = { type: 'order-email-sent', purpose: 'invoice-email' }
68
+
69
+ /** Who sent an invoice email, as the order's history names them. */
70
+ const INVOICE_EMAIL_SENDERS = {
71
+ // The store's own code: the payment webhook's invoice step, the checkout.
72
+ store: { actor: '', actorType: 'system' },
73
+ // A signed-in admin: the admin panel's Regenerate, or an invoice issued by hand.
74
+ admin: { actor: 'Store admin', actorType: 'merchant' },
75
+ }
76
+
77
+ /** `recordInvoiceEmailSent(invoiceData, sender)`, as emitted source; needs `dataAccess`. */
78
+ const RECORD_INVOICE_EMAIL_CODE = `
79
+ var INVOICE_EMAIL_EVENT = ${JSON.stringify(INVOICE_EMAIL_EVENT)};
80
+ var INVOICE_EMAIL_SENDERS = ${JSON.stringify(INVOICE_EMAIL_SENDERS)};
81
+
82
+ // Writes the order's history line for an invoice email the provider accepted.
83
+ // The key is the invoice's, so the line is written once per invoice email.
84
+ async function recordInvoiceEmailSent(invoiceData, sender) {
85
+ if (!invoiceData.orderId) return;
86
+ await dataAccess.recordOrderEvent({
87
+ type: INVOICE_EMAIL_EVENT.type,
88
+ dedupeKey: INVOICE_EMAIL_EVENT.type + ':' + invoiceData.id + ':' + INVOICE_EMAIL_EVENT.purpose,
89
+ orderId: invoiceData.orderId,
90
+ payload: { purpose: INVOICE_EMAIL_EVENT.purpose, to: invoiceData.customerEmail },
91
+ actor: sender.actor,
92
+ actorType: sender.actorType,
93
+ });
94
+ }`
95
+
60
96
  export const generateInvoiceGenerateRouteCode = (settings: UIDLInvoiceSettings): string => {
61
97
  const prefix = settings.invoicePrefix || 'INV-'
62
98
  const autoGenerate = settings.autoGenerateOnPayment !== false
@@ -92,6 +128,7 @@ var INVOICE_PREFIX = ${JSON.stringify(prefix)};
92
128
  var UNFINISHED_INVOICE_STALE_SECONDS = 900;
93
129
 
94
130
  ${INVOICE_CALLER_CODE}
131
+ ${emailEnabled ? RECORD_INVOICE_EMAIL_CODE : ''}
95
132
 
96
133
  // ---------------------------------------------------------------------------
97
134
  // Waiting for the order's line items to be fully written.
@@ -127,6 +164,21 @@ function sleepMs(ms) {
127
164
  return new Promise(function (resolve) { setTimeout(resolve, ms); });
128
165
  }
129
166
 
167
+ // The answer for an invoice the order already has: its number and its PDF.
168
+ function issuedInvoiceAnswer(invoice, currency) {
169
+ var pdfUrl = invoice.pdf_url || ('/api/invoices/' + invoice.id + '/pdf');
170
+ return {
171
+ success: true,
172
+ alreadyIssued: true,
173
+ invoiceId: invoice.id,
174
+ invoiceNumber: invoice.invoice_number,
175
+ storageUrl: pdfUrl.indexOf('/api/invoices/') === 0 ? '' : pdfUrl,
176
+ pdfUrl: pdfUrl,
177
+ total: Number(invoice.total) || 0,
178
+ currency: invoice.currency || currency,
179
+ };
180
+ }
181
+
130
182
  async function hydrateOrderWhenSettled(orderId) {
131
183
  var deadline = Date.now() + SETTLE_TIMEOUT_MS;
132
184
  var lastCount = -1;
@@ -267,17 +319,7 @@ module.exports = async function handler(req, res) {
267
319
  if (!reservation.created) {
268
320
  if (Number(held.pdf_size_bytes) > 0) {
269
321
  console.info('[invoice] Order ' + body.orderId + ' already has invoice ' + held.invoice_number + ' — returning it');
270
- var heldPdfUrl = held.pdf_url || ('/api/invoices/' + held.id + '/pdf');
271
- res.status(200).json({
272
- success: true,
273
- alreadyIssued: true,
274
- invoiceId: held.id,
275
- invoiceNumber: held.invoice_number,
276
- storageUrl: heldPdfUrl.indexOf('/api/invoices/') === 0 ? '' : heldPdfUrl,
277
- pdfUrl: heldPdfUrl,
278
- total: Number(held.total) || 0,
279
- currency: held.currency || currency,
280
- });
322
+ res.status(200).json(issuedInvoiceAnswer(held, currency));
281
323
  return;
282
324
  }
283
325
  // Reserved, never finished: another request is rendering it now, or the
@@ -313,7 +355,18 @@ ${
313
355
  console.info('[invoice] PDF generated: ' + pdfBuffer.length + ' bytes for ' + invoiceData.invoiceNumber);
314
356
 
315
357
  invoiceData.pdfUrl = '/api/invoices/' + invoiceData.id + '/pdf';
316
- await dataAccess.storeInvoicePdf(invoiceData.id, pdfBuffer, invoiceData.pdfUrl);
358
+ // The PDF is written only on an invoice that has none, and the invoice's
359
+ // first PDF is what its email is claimed by (see storeInvoicePdf). A
360
+ // regeneration that gave a reserved invoice its PDF while this request
361
+ // rendered it — a retried payment finishing a stale reservation — emailed
362
+ // the buyer itself, so this request answers with that invoice and sends
363
+ // nothing.
364
+ var stored = await dataAccess.storeInvoicePdf(invoiceData.id, pdfBuffer, invoiceData.pdfUrl);
365
+ if (!stored.firstPdf) {
366
+ console.info('[invoice] Invoice ' + invoiceNumber + ' was given its PDF by a regeneration meanwhile — returning it');
367
+ res.status(200).json(issuedInvoiceAnswer((await dataAccess.getInvoiceById(invoiceData.id)) || held, currency));
368
+ return;
369
+ }
317
370
  console.info('[invoice] PDF stored — teleport_invoices.id=' + invoiceData.id + ', number=' + invoiceData.invoiceNumber + ', pdf_size_bytes=' + pdfBuffer.length + ', items=' + items.length + ' (accessible at ' + invoiceData.pdfUrl + ')');
318
371
 
319
372
  var safeFileName = String(invoiceNumber || invoiceData.id).replace(/[^a-zA-Z0-9._-]/g, '_') + '.pdf';
@@ -382,9 +435,11 @@ ${
382
435
  emailEnabled
383
436
  ? `
384
437
  if (invoiceData.customerEmail) {
438
+ var emailed = false;
385
439
  try {
386
440
  var emailResult = await emailSender.sendInvoiceEmail(invoiceData, pdfBuffer);
387
- if (emailResult && emailResult.success) {
441
+ emailed = !!(emailResult && emailResult.success);
442
+ if (emailed) {
388
443
  console.info('[invoice] Email delivery OK — to=' + invoiceData.customerEmail + ', messageId=' + (emailResult.messageId || '(empty)'));
389
444
  } else {
390
445
  console.error('[invoice] Email delivery FAILED — to=' + invoiceData.customerEmail + ', error=' + (emailResult && emailResult.error ? emailResult.error : 'unknown'));
@@ -392,6 +447,13 @@ ${
392
447
  } catch (emailErr) {
393
448
  console.error('[invoice] Email delivery threw — to=' + invoiceData.customerEmail + ', error=' + (emailErr && emailErr.message ? emailErr.message : emailErr));
394
449
  }
450
+ if (emailed) {
451
+ try {
452
+ await recordInvoiceEmailSent(invoiceData, caller.admin ? INVOICE_EMAIL_SENDERS.admin : INVOICE_EMAIL_SENDERS.store);
453
+ } catch (recordErr) {
454
+ console.error('[invoice] Invoice ' + invoiceNumber + ' was emailed, but the order history could not record it — ' + (recordErr && recordErr.message));
455
+ }
456
+ }
395
457
  } else {
396
458
  console.info('[invoice] Skipping email delivery — invoiceData.customerEmail is empty. (Check teleport_orders.billing_email for order ' + (body.orderId || '(no orderId)') + ')');
397
459
  }
@@ -452,6 +514,37 @@ ${
452
514
  /** The roles allowed to rebuild an invoice (the admin panel is the only caller). */
453
515
  const REGENERATE_INVOICE_ROLES = INVOICE_ADMIN_ROLES
454
516
 
517
+ /** The regenerate route's `emailFirstInvoice`, as emitted source. */
518
+ const generateFirstInvoiceEmailCode = (): string => `${RECORD_INVOICE_EMAIL_CODE}
519
+
520
+ var REGENERATE_SOURCE_REF = 'api/invoices/[id]/regenerate';
521
+
522
+ // The invoice email the payment would have sent: the same sender, template,
523
+ // attachment and link, in the buyer's language — the one stamped on the order
524
+ // at checkout, else the store's main language, never the language of this
525
+ // request, which is the merchant's. The sender records it in the sent-email
526
+ // ledger; the order's history records it once the provider accepted it. A
527
+ // failure is logged and goes no further: the merchant has the PDF either way.
528
+ async function emailFirstInvoice(invoiceData, pdfBuffer, order) {
529
+ if (!invoiceData.customerEmail) return;
530
+ invoiceData.locale = emailLocale.normalizeEmailLocale(order && order.locale) || emailLocale.DEFAULT_EMAIL_LOCALE;
531
+ try {
532
+ var sent = await emailSender.sendInvoiceEmail(invoiceData, pdfBuffer, REGENERATE_SOURCE_REF);
533
+ if (!sent || !sent.success) {
534
+ throw new Error((sent && sent.error) || 'the email provider did not accept it');
535
+ }
536
+ } catch (sendErr) {
537
+ console.error('[invoice] Regenerate: invoice ' + invoiceData.invoiceNumber + ' could not be emailed to its customer — ' + (sendErr && sendErr.message));
538
+ return;
539
+ }
540
+ try {
541
+ await recordInvoiceEmailSent(invoiceData, INVOICE_EMAIL_SENDERS.admin);
542
+ } catch (recordErr) {
543
+ console.error('[invoice] Regenerate: invoice ' + invoiceData.invoiceNumber + ' was emailed, but the order history could not record it — ' + (recordErr && recordErr.message));
544
+ }
545
+ }
546
+ `
547
+
455
548
  /**
456
549
  * POST /api/invoices/[id]/regenerate — the admin panel's "Regenerate" action.
457
550
  *
@@ -461,23 +554,47 @@ const REGENERATE_INVOICE_ROLES = INVOICE_ADMIN_ROLES
461
554
  * against it keep pointing at a valid invoice. What a merchant corrected on the
462
555
  * invoice itself (customer details, notes, payment method) wins over the order;
463
556
  * the lines and every figure are recomputed from the order. The old PDF is
464
- * replaced — and its runtime-storage copy deleted — and the customer is never
465
- * emailed.
557
+ * replaced — and its runtime-storage copy deleted.
558
+ *
559
+ * The customer is emailed once: by the regeneration that gives an invoice its
560
+ * FIRST PDF — one whose PDF failed at payment, so the payment's invoice email
561
+ * never went out — and only when the store emails invoices (Invoice Email
562
+ * Delivery). It is the payment's email: the same sender, template, attachment,
563
+ * link and language, recorded in the sent-email ledger and on the order's
564
+ * history. Which write gives the first PDF, the database decides (see
565
+ * `replaceInvoice` and `storeInvoicePdf`), so a second regeneration, a double
566
+ * click, two requests at once or a retried payment finishing the same invoice
567
+ * never send it again; a failed send costs the merchant nothing.
466
568
  *
467
569
  * Unlike `/generate`, which the payment webhook calls server-to-server, this
468
570
  * route is reached from the merchant's browser only, so it checks the session
469
571
  * itself: signed in, with an admin role.
470
572
  */
471
- export const generateInvoiceRegenerateRouteCode = (): string => {
573
+ export const generateInvoiceRegenerateRouteCode = (settings: UIDLInvoiceSettings): string => {
574
+ const emailEnabled = settings.emailDelivery?.enabled === true
575
+
472
576
  return `/**
473
577
  * POST /api/invoices/[id]/regenerate
474
578
  * Rebuilds one invoice from its order, in place, for the admin panel. Same
475
- * number, id and dates; a freshly rendered PDF replaces the old one; no email.
579
+ * number, id and dates; a freshly rendered PDF replaces the old one.${
580
+ emailEnabled
581
+ ? `
582
+ * The first PDF an invoice ever gets is emailed to its customer, once.`
583
+ : `
584
+ * No email: this store does not email invoices.`
585
+ }
476
586
  */
477
587
 
478
588
  var dataAccess = require('../../../../utils/invoices/data-access');
479
589
  var pdfGenerator = require('../../../../utils/invoices/pdf-generator');
480
590
  var invoiceAssembly = require('../../../../utils/invoices/invoice-assembly');
591
+ ${
592
+ emailEnabled
593
+ ? `var emailSender = require('../../../../utils/invoices/email-sender');
594
+ var sentEmailLog = require('../../../../utils/email/sent-email-log');
595
+ var emailLocale = require('../../../../utils/email/email-locale');`
596
+ : ''
597
+ }
481
598
 
482
599
  ${generateCommonJsSessionTokenResolverCode()}
483
600
  ${ROLE_OF_TOKEN_CODE}
@@ -563,7 +680,7 @@ async function findInvoice(invoiceId) {
563
680
  throw err;
564
681
  }
565
682
  }
566
-
683
+ ${emailEnabled ? generateFirstInvoiceEmailCode() : ''}
567
684
  module.exports = async function handler(req, res) {
568
685
  if (req.method !== 'POST') {
569
686
  res.setHeader('Allow', 'POST');
@@ -622,7 +739,14 @@ module.exports = async function handler(req, res) {
622
739
  invoiceData.pdfSizeBytes = pdfBuffer.length;
623
740
  invoiceData.pdfUrl = '/api/invoices/' + invoiceData.id + '/pdf';
624
741
 
625
- await dataAccess.replaceInvoice(invoiceData.id, invoiceData, invoiceData.items);
742
+ ${
743
+ emailEnabled
744
+ ? ` var replaced = await dataAccess.replaceInvoice(invoiceData.id, invoiceData, invoiceData.items);
745
+ // This regeneration gave the invoice its first PDF, so the payment's
746
+ // invoice email never went out: this one sends it.
747
+ var emailsCustomer = !!(replaced && replaced.firstPdf);`
748
+ : ` await dataAccess.replaceInvoice(invoiceData.id, invoiceData, invoiceData.items);`
749
+ }
626
750
  console.info('[invoice] Regenerated ' + invoiceData.invoiceNumber + ' (id=' + invoiceData.id + ', items=' + invoiceData.items.length + ', pdf_size_bytes=' + pdfBuffer.length + ')');
627
751
 
628
752
  // The new PDF's public copy. The row already serves the new bytes through
@@ -645,7 +769,17 @@ module.exports = async function handler(req, res) {
645
769
  if (storageKey) {
646
770
  await invoiceAssembly.deleteStoredInvoicePdf(storageKey);
647
771
  }
648
- res.status(500).json({ success: false, error: MESSAGES.orderNotUpdated });
772
+ ${
773
+ emailEnabled
774
+ ? ` // The invoice has its first PDF all the same — served from the database,
775
+ // its public copy just went — and no later regeneration will send it.
776
+ if (emailsCustomer) {
777
+ await emailFirstInvoice(invoiceData, pdfBuffer, hydrated.order);
778
+ }
779
+ await sentEmailLog.settleSentEmailLog();
780
+ `
781
+ : ''
782
+ } res.status(500).json({ success: false, error: MESSAGES.orderNotUpdated });
649
783
  return;
650
784
  }
651
785
 
@@ -663,7 +797,18 @@ module.exports = async function handler(req, res) {
663
797
  if (previousStorageKey && previousStorageKey !== storageKey) {
664
798
  await invoiceAssembly.deleteStoredInvoicePdf(previousStorageKey);
665
799
  }
666
-
800
+ ${
801
+ emailEnabled
802
+ ? `
803
+ if (emailsCustomer) {
804
+ await emailFirstInvoice(invoiceData, pdfBuffer, hydrated.order);
805
+ }
806
+ // The email's ledger row is written in the background — land it before
807
+ // replying, a serverless function may be frozen the instant it responds.
808
+ await sentEmailLog.settleSentEmailLog();
809
+ `
810
+ : ''
811
+ }
667
812
  res.status(200).json({
668
813
  success: true,
669
814
  invoiceId: invoiceData.id,