pipework 0.212.0 → 0.242.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.
Files changed (312) hide show
  1. package/CHANGELOG.md +136 -0
  2. package/REFERENCE.md +94 -2
  3. package/dist/cli/commands/openapi.d.ts +8 -0
  4. package/dist/cli/commands/openapi.d.ts.map +1 -0
  5. package/dist/cli/commands/openapi.js +49 -0
  6. package/dist/cli/commands/openapi.js.map +1 -0
  7. package/dist/cli/commands/serve.d.ts +8 -0
  8. package/dist/cli/commands/serve.d.ts.map +1 -1
  9. package/dist/cli/commands/serve.js +22 -3
  10. package/dist/cli/commands/serve.js.map +1 -1
  11. package/dist/cli/dev-bootstrap.js +6 -1
  12. package/dist/cli/dev-bootstrap.js.map +1 -1
  13. package/dist/cli/index.d.ts.map +1 -1
  14. package/dist/cli/index.js +15 -1
  15. package/dist/cli/index.js.map +1 -1
  16. package/dist/cli/surface-kinds.d.ts +5 -0
  17. package/dist/cli/surface-kinds.d.ts.map +1 -0
  18. package/dist/cli/surface-kinds.js +15 -0
  19. package/dist/cli/surface-kinds.js.map +1 -0
  20. package/dist/core/config/load.d.ts +1 -0
  21. package/dist/core/config/load.d.ts.map +1 -1
  22. package/dist/core/config/load.js +1 -0
  23. package/dist/core/config/load.js.map +1 -1
  24. package/dist/core/config/namespace.d.ts +6 -0
  25. package/dist/core/config/namespace.d.ts.map +1 -1
  26. package/dist/core/config/schema.d.ts +15 -0
  27. package/dist/core/config/schema.d.ts.map +1 -1
  28. package/dist/core/config/schema.js +11 -2
  29. package/dist/core/config/schema.js.map +1 -1
  30. package/dist/core/lifecycle/health.js +2 -2
  31. package/dist/core/lifecycle/health.js.map +1 -1
  32. package/dist/core/lifecycle/types.d.ts +2 -0
  33. package/dist/core/lifecycle/types.d.ts.map +1 -1
  34. package/dist/core/pipework.d.ts +14 -1
  35. package/dist/core/pipework.d.ts.map +1 -1
  36. package/dist/core/pipework.js +18 -2
  37. package/dist/core/pipework.js.map +1 -1
  38. package/dist/core/surface/http.d.ts +3 -0
  39. package/dist/core/surface/http.d.ts.map +1 -1
  40. package/dist/core/surface/http.js +27 -8
  41. package/dist/core/surface/http.js.map +1 -1
  42. package/dist/core/surface/start.d.ts.map +1 -1
  43. package/dist/core/surface/start.js +19 -3
  44. package/dist/core/surface/start.js.map +1 -1
  45. package/dist/core/surface/types.d.ts +8 -0
  46. package/dist/core/surface/types.d.ts.map +1 -1
  47. package/dist/data/db/filter.d.ts +3 -0
  48. package/dist/data/db/filter.d.ts.map +1 -1
  49. package/dist/data/db/filter.js +3 -0
  50. package/dist/data/db/filter.js.map +1 -1
  51. package/dist/data/db/in-list.d.ts +15 -0
  52. package/dist/data/db/in-list.d.ts.map +1 -0
  53. package/dist/data/db/in-list.js +51 -0
  54. package/dist/data/db/in-list.js.map +1 -0
  55. package/dist/data/db/namespace.d.ts +2 -0
  56. package/dist/data/db/namespace.d.ts.map +1 -1
  57. package/dist/data/db/pool.d.ts +1 -1
  58. package/dist/data/db/pool.d.ts.map +1 -1
  59. package/dist/data/db/pool.js +4 -2
  60. package/dist/data/db/pool.js.map +1 -1
  61. package/dist/data/db/reservation.d.ts +40 -0
  62. package/dist/data/db/reservation.d.ts.map +1 -0
  63. package/dist/data/db/reservation.js +108 -0
  64. package/dist/data/db/reservation.js.map +1 -0
  65. package/dist/data/domain/build-table.js +32 -1
  66. package/dist/data/domain/build-table.js.map +1 -1
  67. package/dist/data/domain/index.d.ts +2 -1
  68. package/dist/data/domain/index.d.ts.map +1 -1
  69. package/dist/data/domain/types.d.ts +9 -1
  70. package/dist/data/domain/types.d.ts.map +1 -1
  71. package/dist/data/domain/types.js.map +1 -1
  72. package/dist/data/migrate/apply.d.ts.map +1 -1
  73. package/dist/data/migrate/apply.js +3 -11
  74. package/dist/data/migrate/apply.js.map +1 -1
  75. package/dist/data/migrate/check.d.ts.map +1 -1
  76. package/dist/data/migrate/check.js +3 -6
  77. package/dist/data/migrate/check.js.map +1 -1
  78. package/dist/data/migrate/diff.d.ts +16 -0
  79. package/dist/data/migrate/diff.d.ts.map +1 -1
  80. package/dist/data/migrate/diff.js +44 -4
  81. package/dist/data/migrate/diff.js.map +1 -1
  82. package/dist/data/migrate/generate.d.ts +1 -1
  83. package/dist/data/migrate/generate.d.ts.map +1 -1
  84. package/dist/data/migrate/generate.js +19 -51
  85. package/dist/data/migrate/generate.js.map +1 -1
  86. package/dist/data/migrate/history.d.ts +36 -0
  87. package/dist/data/migrate/history.d.ts.map +1 -0
  88. package/dist/data/migrate/history.js +185 -0
  89. package/dist/data/migrate/history.js.map +1 -0
  90. package/dist/data/migrate/internal-definitions.d.ts.map +1 -1
  91. package/dist/data/migrate/internal-definitions.js +22 -1
  92. package/dist/data/migrate/internal-definitions.js.map +1 -1
  93. package/dist/data/migrate/provision-framework.d.ts +2 -0
  94. package/dist/data/migrate/provision-framework.d.ts.map +1 -1
  95. package/dist/data/migrate/provision-framework.js +11 -3
  96. package/dist/data/migrate/provision-framework.js.map +1 -1
  97. package/dist/data/migrate/snapshot.d.ts +4 -0
  98. package/dist/data/migrate/snapshot.d.ts.map +1 -1
  99. package/dist/data/migrate/snapshot.js +2 -0
  100. package/dist/data/migrate/snapshot.js.map +1 -1
  101. package/dist/data/migrate/sql-emit.js +66 -12
  102. package/dist/data/migrate/sql-emit.js.map +1 -1
  103. package/dist/data/query/pg-core/foreign-keys.d.ts +5 -0
  104. package/dist/data/query/pg-core/foreign-keys.d.ts.map +1 -1
  105. package/dist/data/query/pg-core/foreign-keys.js +9 -0
  106. package/dist/data/query/pg-core/foreign-keys.js.map +1 -1
  107. package/dist/data/query/pg-core/indexes.d.ts +5 -0
  108. package/dist/data/query/pg-core/indexes.d.ts.map +1 -1
  109. package/dist/data/query/pg-core/indexes.js +7 -0
  110. package/dist/data/query/pg-core/indexes.js.map +1 -1
  111. package/dist/data/schema/json-schema.js +29 -1
  112. package/dist/data/schema/json-schema.js.map +1 -1
  113. package/dist/data/schema/types.d.ts +2 -2
  114. package/dist/data/schema/types.d.ts.map +1 -1
  115. package/dist/index.d.ts +18 -9
  116. package/dist/index.d.ts.map +1 -1
  117. package/dist/index.js +5 -1
  118. package/dist/index.js.map +1 -1
  119. package/dist/infra/alerts/handle.d.ts +33 -5
  120. package/dist/infra/alerts/handle.d.ts.map +1 -1
  121. package/dist/infra/alerts/handle.js +56 -12
  122. package/dist/infra/alerts/handle.js.map +1 -1
  123. package/dist/infra/alerts/http.d.ts +2 -2
  124. package/dist/infra/alerts/http.d.ts.map +1 -1
  125. package/dist/infra/alerts/http.js +21 -3
  126. package/dist/infra/alerts/http.js.map +1 -1
  127. package/dist/infra/alerts/index.d.ts +1 -1
  128. package/dist/infra/alerts/index.d.ts.map +1 -1
  129. package/dist/infra/alerts/schema.d.ts +5 -0
  130. package/dist/infra/alerts/schema.d.ts.map +1 -1
  131. package/dist/infra/alerts/schema.js +1 -0
  132. package/dist/infra/alerts/schema.js.map +1 -1
  133. package/dist/infra/crawl/audit.d.ts +6 -0
  134. package/dist/infra/crawl/audit.d.ts.map +1 -0
  135. package/dist/infra/crawl/audit.js +38 -0
  136. package/dist/infra/crawl/audit.js.map +1 -0
  137. package/dist/infra/crawl/errors.d.ts +14 -0
  138. package/dist/infra/crawl/errors.d.ts.map +1 -0
  139. package/dist/infra/crawl/errors.js +22 -0
  140. package/dist/infra/crawl/errors.js.map +1 -0
  141. package/dist/infra/crawl/handle.d.ts +149 -0
  142. package/dist/infra/crawl/handle.d.ts.map +1 -0
  143. package/dist/infra/crawl/handle.js +550 -0
  144. package/dist/infra/crawl/handle.js.map +1 -0
  145. package/dist/infra/crawl/index.d.ts +7 -0
  146. package/dist/infra/crawl/index.d.ts.map +1 -0
  147. package/dist/infra/crawl/index.js +5 -0
  148. package/dist/infra/crawl/index.js.map +1 -0
  149. package/dist/infra/crawl/links.d.ts +9 -0
  150. package/dist/infra/crawl/links.d.ts.map +1 -0
  151. package/dist/infra/crawl/links.js +194 -0
  152. package/dist/infra/crawl/links.js.map +1 -0
  153. package/dist/infra/crawl/namespace.d.ts +36 -0
  154. package/dist/infra/crawl/namespace.d.ts.map +1 -0
  155. package/dist/infra/crawl/namespace.js +36 -0
  156. package/dist/infra/crawl/namespace.js.map +1 -0
  157. package/dist/infra/crawl/robots.d.ts +16 -0
  158. package/dist/infra/crawl/robots.d.ts.map +1 -0
  159. package/dist/infra/crawl/robots.js +98 -0
  160. package/dist/infra/crawl/robots.js.map +1 -0
  161. package/dist/infra/crawl/schema.d.ts +222 -0
  162. package/dist/infra/crawl/schema.d.ts.map +1 -0
  163. package/dist/infra/crawl/schema.js +84 -0
  164. package/dist/infra/crawl/schema.js.map +1 -0
  165. package/dist/infra/credit-line/handle.d.ts +250 -0
  166. package/dist/infra/credit-line/handle.d.ts.map +1 -0
  167. package/dist/infra/credit-line/handle.js +618 -0
  168. package/dist/infra/credit-line/handle.js.map +1 -0
  169. package/dist/infra/credit-line/index.d.ts +3 -0
  170. package/dist/infra/credit-line/index.d.ts.map +1 -0
  171. package/dist/infra/credit-line/index.js +2 -0
  172. package/dist/infra/credit-line/index.js.map +1 -0
  173. package/dist/infra/credit-line/namespace.d.ts +7 -0
  174. package/dist/infra/credit-line/namespace.d.ts.map +1 -0
  175. package/dist/infra/credit-line/namespace.js +7 -0
  176. package/dist/infra/credit-line/namespace.js.map +1 -0
  177. package/dist/infra/credit-line/schema.d.ts +194 -0
  178. package/dist/infra/credit-line/schema.d.ts.map +1 -0
  179. package/dist/infra/credit-line/schema.js +89 -0
  180. package/dist/infra/credit-line/schema.js.map +1 -0
  181. package/dist/infra/email/inbound.d.ts +14 -2
  182. package/dist/infra/email/inbound.d.ts.map +1 -1
  183. package/dist/infra/email/inbound.js +5 -1
  184. package/dist/infra/email/inbound.js.map +1 -1
  185. package/dist/infra/email/index.d.ts +2 -2
  186. package/dist/infra/email/index.d.ts.map +1 -1
  187. package/dist/infra/email/index.js.map +1 -1
  188. package/dist/infra/email/mail.d.ts +43 -5
  189. package/dist/infra/email/mail.d.ts.map +1 -1
  190. package/dist/infra/email/mail.js +124 -9
  191. package/dist/infra/email/mail.js.map +1 -1
  192. package/dist/infra/email/mime.d.ts +6 -0
  193. package/dist/infra/email/mime.d.ts.map +1 -1
  194. package/dist/infra/email/mime.js +10 -2
  195. package/dist/infra/email/mime.js.map +1 -1
  196. package/dist/infra/email/schema.d.ts +17 -3
  197. package/dist/infra/email/schema.d.ts.map +1 -1
  198. package/dist/infra/email/schema.js +15 -4
  199. package/dist/infra/email/schema.js.map +1 -1
  200. package/dist/infra/email/sns.js +2 -2
  201. package/dist/infra/email/sns.js.map +1 -1
  202. package/dist/infra/email/unsubscribe.d.ts +13 -0
  203. package/dist/infra/email/unsubscribe.d.ts.map +1 -0
  204. package/dist/infra/email/unsubscribe.js +43 -0
  205. package/dist/infra/email/unsubscribe.js.map +1 -0
  206. package/dist/infra/fetch/handle.d.ts +20 -1
  207. package/dist/infra/fetch/handle.d.ts.map +1 -1
  208. package/dist/infra/fetch/handle.js +60 -28
  209. package/dist/infra/fetch/handle.js.map +1 -1
  210. package/dist/infra/fetch/index.d.ts +2 -2
  211. package/dist/infra/fetch/index.d.ts.map +1 -1
  212. package/dist/infra/fetch/index.js +1 -1
  213. package/dist/infra/fetch/index.js.map +1 -1
  214. package/dist/infra/fetch/namespace.d.ts +20 -2
  215. package/dist/infra/fetch/namespace.d.ts.map +1 -1
  216. package/dist/infra/fetch/namespace.js +20 -2
  217. package/dist/infra/fetch/namespace.js.map +1 -1
  218. package/dist/infra/join-requests/handle.d.ts +86 -0
  219. package/dist/infra/join-requests/handle.d.ts.map +1 -0
  220. package/dist/infra/join-requests/handle.js +233 -0
  221. package/dist/infra/join-requests/handle.js.map +1 -0
  222. package/dist/infra/join-requests/index.d.ts +3 -0
  223. package/dist/infra/join-requests/index.d.ts.map +1 -0
  224. package/dist/infra/join-requests/index.js +2 -0
  225. package/dist/infra/join-requests/index.js.map +1 -0
  226. package/dist/infra/join-requests/namespace.d.ts +7 -0
  227. package/dist/infra/join-requests/namespace.d.ts.map +1 -0
  228. package/dist/infra/join-requests/namespace.js +7 -0
  229. package/dist/infra/join-requests/namespace.js.map +1 -0
  230. package/dist/infra/join-requests/schema.d.ts +58 -0
  231. package/dist/infra/join-requests/schema.d.ts.map +1 -0
  232. package/dist/infra/join-requests/schema.js +31 -0
  233. package/dist/infra/join-requests/schema.js.map +1 -0
  234. package/dist/infra/outbox/core.d.ts +9 -0
  235. package/dist/infra/outbox/core.d.ts.map +1 -1
  236. package/dist/infra/outbox/core.js +14 -0
  237. package/dist/infra/outbox/core.js.map +1 -1
  238. package/dist/infra/support/handle.d.ts +43 -0
  239. package/dist/infra/support/handle.d.ts.map +1 -1
  240. package/dist/infra/support/handle.js +35 -7
  241. package/dist/infra/support/handle.js.map +1 -1
  242. package/dist/infra/support/http.d.ts +28 -0
  243. package/dist/infra/support/http.d.ts.map +1 -0
  244. package/dist/infra/support/http.js +217 -0
  245. package/dist/infra/support/http.js.map +1 -0
  246. package/dist/infra/support/index.d.ts +2 -0
  247. package/dist/infra/support/index.d.ts.map +1 -1
  248. package/dist/infra/support/index.js +1 -0
  249. package/dist/infra/support/index.js.map +1 -1
  250. package/dist/infra/support/namespace.d.ts +3 -0
  251. package/dist/infra/support/namespace.d.ts.map +1 -1
  252. package/dist/infra/support/namespace.js +3 -0
  253. package/dist/infra/support/namespace.js.map +1 -1
  254. package/dist/infra/support/schema.d.ts +4 -4
  255. package/dist/infra/webhook/outbound.d.ts +16 -0
  256. package/dist/infra/webhook/outbound.d.ts.map +1 -1
  257. package/dist/infra/webhook/outbound.js +78 -27
  258. package/dist/infra/webhook/outbound.js.map +1 -1
  259. package/dist/request/di/builder.d.ts +24 -1
  260. package/dist/request/di/builder.d.ts.map +1 -1
  261. package/dist/request/di/builder.js +35 -0
  262. package/dist/request/di/builder.js.map +1 -1
  263. package/dist/request/di/types.d.ts +30 -0
  264. package/dist/request/di/types.d.ts.map +1 -1
  265. package/dist/request/http/server.d.ts.map +1 -1
  266. package/dist/request/http/server.js +111 -20
  267. package/dist/request/http/server.js.map +1 -1
  268. package/dist/test/auto-setup.js +2 -2
  269. package/dist/test/auto-setup.js.map +1 -1
  270. package/dist/test/crawl-fixture.d.ts +53 -0
  271. package/dist/test/crawl-fixture.d.ts.map +1 -0
  272. package/dist/test/crawl-fixture.js +130 -0
  273. package/dist/test/crawl-fixture.js.map +1 -0
  274. package/dist/test/index.d.ts +2 -1
  275. package/dist/test/index.d.ts.map +1 -1
  276. package/dist/test/index.js +2 -1
  277. package/dist/test/index.js.map +1 -1
  278. package/dist/test/progress.d.ts +7 -1
  279. package/dist/test/progress.d.ts.map +1 -1
  280. package/dist/test/progress.js +6 -0
  281. package/dist/test/progress.js.map +1 -1
  282. package/dist/test/reap-after-run.d.ts +3 -0
  283. package/dist/test/reap-after-run.d.ts.map +1 -0
  284. package/dist/test/reap-after-run.js +29 -0
  285. package/dist/test/reap-after-run.js.map +1 -0
  286. package/dist/test/reap.d.ts +11 -2
  287. package/dist/test/reap.d.ts.map +1 -1
  288. package/dist/test/reap.js +19 -12
  289. package/dist/test/reap.js.map +1 -1
  290. package/dist/test/setup.d.ts +2 -0
  291. package/dist/test/setup.d.ts.map +1 -1
  292. package/dist/test/setup.js +87 -30
  293. package/dist/test/setup.js.map +1 -1
  294. package/dist/test/vitest-plugin.d.ts.map +1 -1
  295. package/dist/test/vitest-plugin.js +8 -1
  296. package/dist/test/vitest-plugin.js.map +1 -1
  297. package/dist/test/vitest-workspace.d.ts +1 -0
  298. package/dist/test/vitest-workspace.d.ts.map +1 -1
  299. package/dist/test/vitest-workspace.js +1 -1
  300. package/dist/test/vitest-workspace.js.map +1 -1
  301. package/dist/test/vitest.d.ts +20 -0
  302. package/dist/test/vitest.d.ts.map +1 -1
  303. package/dist/test/vitest.js +41 -1
  304. package/dist/test/vitest.js.map +1 -1
  305. package/dist/trace/partition/sync.d.ts +8 -2
  306. package/dist/trace/partition/sync.d.ts.map +1 -1
  307. package/dist/trace/partition/sync.js +6 -3
  308. package/dist/trace/partition/sync.js.map +1 -1
  309. package/dist/workspace/generate.d.ts.map +1 -1
  310. package/dist/workspace/generate.js +1 -0
  311. package/dist/workspace/generate.js.map +1 -1
  312. package/package.json +1 -1
@@ -0,0 +1,618 @@
1
+ import { sql } from '../../data/db/sql.js';
2
+ import { pipe } from '../../data/db/db.js';
3
+ import { requireContext, getContext, runInContext } from '../../request/context/store.js';
4
+ import { writeInfraAudit } from '../../data/write/audit/infra-audit.js';
5
+ import { inTenant } from '../../auth/tenant/in-tenant.js';
6
+ import { bindTransaction } from '../../request/context/guard.js';
7
+ import { ValidationError } from '../../core/errors/index.js';
8
+ const DEFAULT_WARN_SHARE = 0.8;
9
+ const NOW = sql `(clock_timestamp() AT TIME ZONE 'UTC')`;
10
+ function toDate(value) {
11
+ if (value instanceof Date)
12
+ return value;
13
+ const text = String(value);
14
+ return new Date(/(Z|[+-]\d\d(:?\d\d)?)$/.test(text) ? text : `${text.replace(' ', 'T')}Z`);
15
+ }
16
+ function toDateOrNull(value) {
17
+ return value === null || value === undefined ? null : toDate(value);
18
+ }
19
+ function num(value) {
20
+ return Number(value);
21
+ }
22
+ function numOrNull(value) {
23
+ return value === null || value === undefined ? null : Number(value);
24
+ }
25
+ function utc(date) {
26
+ return sql `(${date.toISOString()}::timestamptz AT TIME ZONE 'UTC')`;
27
+ }
28
+ function rowToReservation(row) {
29
+ return {
30
+ id: row['id'],
31
+ tenantId: row['tenant_id'],
32
+ jobId: row['job_id'],
33
+ amountCents: num(row['amount']),
34
+ status: row['status'],
35
+ settledAmountCents: numOrNull(row['settled_amount']),
36
+ authorizationId: row['authorization_id'] ?? null,
37
+ createdAt: toDate(row['created_at']),
38
+ settledAt: toDateOrNull(row['settled_at']),
39
+ };
40
+ }
41
+ function rowToEntry(row) {
42
+ return {
43
+ id: row['id'],
44
+ tenantId: row['tenant_id'],
45
+ jobId: row['job_id'],
46
+ reservationId: row['reservation_id'],
47
+ amountCents: num(row['amount_cents']),
48
+ paidCents: num(row['paid_cents']),
49
+ authorizationId: row['authorization_id'] ?? null,
50
+ at: toDate(row['at']),
51
+ };
52
+ }
53
+ function rowToPayment(row) {
54
+ return {
55
+ id: row['id'],
56
+ tenantId: row['tenant_id'],
57
+ reference: row['reference'],
58
+ amountCents: num(row['amount_cents']),
59
+ unappliedCents: num(row['unapplied_cents']),
60
+ at: toDate(row['at']),
61
+ };
62
+ }
63
+ function rowToAuthorization(row) {
64
+ const names = row['replaces_caps'];
65
+ return {
66
+ id: row['id'],
67
+ tenantId: row['tenant_id'],
68
+ reference: row['reference'],
69
+ amountCents: num(row['amount_cents']),
70
+ startsAt: toDate(row['starts_at']),
71
+ endsAt: toDate(row['ends_at']),
72
+ replacesCaps: Array.isArray(names) ? names : [],
73
+ revokedAt: toDateOrNull(row['revoked_at']),
74
+ createdAt: toDate(row['created_at']),
75
+ };
76
+ }
77
+ function capsToJson(caps) {
78
+ return JSON.stringify(caps.map((c) => ({
79
+ name: c.name,
80
+ cents: c.cents,
81
+ window: typeof c.window === 'string' ? c.window : { from: c.window.from.toISOString(), to: c.window.to.toISOString() },
82
+ ...(c.warnShare === undefined ? {} : { warnShare: c.warnShare }),
83
+ })));
84
+ }
85
+ function capsFromJson(value) {
86
+ const list = Array.isArray(value) ? value : [];
87
+ return list.map(c => ({
88
+ name: c.name,
89
+ cents: c.cents,
90
+ window: typeof c.window === 'string' ? c.window : { from: new Date(c.window.from), to: new Date(c.window.to) },
91
+ ...(c.warnShare === undefined ? {} : { warnShare: c.warnShare }),
92
+ }));
93
+ }
94
+ function fail(field, detail, message) {
95
+ throw new ValidationError({ [field]: [detail] }, message);
96
+ }
97
+ function requireCents(name, value, min) {
98
+ if (!Number.isSafeInteger(value) || value < min) {
99
+ const detail = min === 1 ? 'must be a positive integer' : 'must be a non-negative integer';
100
+ fail(name, detail, `Credit line ${name} ${detail} (whole cents).`);
101
+ }
102
+ }
103
+ function requireText(name, value) {
104
+ if (typeof value !== 'string' || value === '')
105
+ fail(name, 'must be a non-empty string', `Credit line ${name} must be a non-empty string.`);
106
+ }
107
+ function requireShare(name, value) {
108
+ if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0 || value > 1) {
109
+ fail(name, 'must be greater than 0 and at most 1', `Credit line ${name} must be greater than 0 and at most 1.`);
110
+ }
111
+ }
112
+ function requireSpan(name, from, to) {
113
+ if (!(from instanceof Date) || !(to instanceof Date) || Number.isNaN(from.getTime()) || Number.isNaN(to.getTime()) || from.getTime() >= to.getTime()) {
114
+ fail(name, 'from must be a valid date before to', `Credit line ${name}: from must be a valid date before to.`);
115
+ }
116
+ }
117
+ function requireCaps(caps) {
118
+ const seen = new Set();
119
+ for (const cap of caps) {
120
+ requireText('caps.name', cap.name);
121
+ if (seen.has(cap.name))
122
+ fail('caps', `name "${cap.name}" is used twice`, `Credit line cap names must be unique; "${cap.name}" appears twice.`);
123
+ seen.add(cap.name);
124
+ requireCents(`cap "${cap.name}" cents`, cap.cents, 1);
125
+ if (cap.warnShare !== undefined)
126
+ requireShare(`cap "${cap.name}" warnShare`, cap.warnShare);
127
+ const window = cap.window;
128
+ if (window === 'calendar_year' || window === 'calendar_month')
129
+ continue;
130
+ const span = (typeof window === 'object' && window !== null ? window : {});
131
+ if (span.from === undefined || span.to === undefined) {
132
+ fail('caps.window', 'must be calendar_year, calendar_month or { from, to }', `Credit line cap "${cap.name}" window must be 'calendar_year', 'calendar_month' or { from, to }.`);
133
+ }
134
+ requireSpan(`cap "${cap.name}" window`, span.from, span.to);
135
+ }
136
+ }
137
+ export function createCreditLine(config = {}) {
138
+ const defaultShare = config.warnShare ?? DEFAULT_WARN_SHARE;
139
+ requireShare('config warnShare', defaultShare);
140
+ const timeZone = config.timeZone ?? 'UTC';
141
+ try {
142
+ new Intl.DateTimeFormat('en', { timeZone });
143
+ }
144
+ catch {
145
+ throw new Error(`[pipework] creditLine timeZone "${timeZone}" is not a time zone name (use an IANA name such as "America/New_York").\n`);
146
+ }
147
+ async function run(explicit, fn) {
148
+ if (explicit !== null) {
149
+ requireTenantId(explicit);
150
+ const flow = requireContext();
151
+ return inTenant(flow, explicit, tx => fn(tx, explicit, body => runForTenant(flow, explicit, tx, body)));
152
+ }
153
+ const flow = requireContext();
154
+ if (flow.tenant === null) {
155
+ throw new Error('[pipework] This creditLine method needs a tenant in the Flow.\n\n' +
156
+ ' Call it from an authenticated tenant route, or use the ...For(tenantId, ...) form\n' +
157
+ ' (openLineFor, reserveFor, settleFor, ...) from a webhook or a job with no tenant.\n');
158
+ }
159
+ const tenantId = flow.tenant;
160
+ return pipe.transaction((tx) => fn(tx, tenantId, body => body()));
161
+ }
162
+ function runForTenant(flow, tenantId, tx, body) {
163
+ const dbName = flow.instance.config.defaultDatabaseName();
164
+ const child = { ...flow, tenant: tenantId, transactions: new Map(flow.transactions) };
165
+ child.transactions.set(dbName, bindTransaction(child, dbName, tx));
166
+ return runInContext(child, body);
167
+ }
168
+ function requireTenantId(tenantId) {
169
+ requireText('tenantId', tenantId);
170
+ }
171
+ async function lockTenant(tx, tenantId) {
172
+ await tx.execute(sql `SELECT pg_advisory_xact_lock(hashtextextended(${`pipework.credit_line:${tenantId}`}, 0))`);
173
+ }
174
+ async function auditStep(tx, tenantId, lineId, verb, after, before) {
175
+ const flow = getContext();
176
+ if (flow === undefined)
177
+ return;
178
+ await writeInfraAudit(flow, tx, { tenantId, entity: 'CreditLine', verb, entityId: lineId, after, ...(before === undefined ? {} : { before }) });
179
+ }
180
+ async function loadLine(tx, tenantId) {
181
+ const rows = await tx.execute(sql `SELECT * FROM credit_line WHERE tenant_id = ${tenantId}`);
182
+ if (rows.length === 0)
183
+ return null;
184
+ const r = rows[0];
185
+ return {
186
+ id: r['id'],
187
+ ceilingCents: num(r['ceiling_cents']),
188
+ warnShare: numOrNull(r['warn_share']),
189
+ caps: capsFromJson(r['caps']),
190
+ openedAt: toDate(r['opened_at']),
191
+ closedAt: toDateOrNull(r['closed_at']),
192
+ };
193
+ }
194
+ async function requireLine(tx, tenantId, what) {
195
+ const line = await loadLine(tx, tenantId);
196
+ if (line === null) {
197
+ throw new Error(`[pipework] creditLine.${what}: tenant ${tenantId} has no line.\n\n Call openLine({ ceilingCents }) first.\n`);
198
+ }
199
+ return line;
200
+ }
201
+ function windowBounds(window) {
202
+ if (typeof window !== 'string')
203
+ return { start: utc(window.from), end: utc(window.to) };
204
+ const unit = window === 'calendar_year' ? 'year' : 'month';
205
+ const local = sql `date_trunc(${sql.raw(`'${unit}'`)}, clock_timestamp() AT TIME ZONE ${timeZone}::text)`;
206
+ return {
207
+ start: sql `((${local}) AT TIME ZONE ${timeZone}::text) AT TIME ZONE 'UTC'`,
208
+ end: sql `((${local} + ${sql.raw(`interval '1 ${unit}'`)}) AT TIME ZONE ${timeZone}::text) AT TIME ZONE 'UTC'`,
209
+ };
210
+ }
211
+ async function sumOf(tx, query) {
212
+ const rows = await tx.execute(query);
213
+ return num(rows[0]['n']);
214
+ }
215
+ function authStatus(row, used, held, inForce) {
216
+ const a = rowToAuthorization(row);
217
+ return { ...a, usedCents: used, heldCents: held, remainingCents: Math.max(0, a.amountCents - used - held), inForce };
218
+ }
219
+ async function statusOf(tx, tenantId, row) {
220
+ const id = row['id'];
221
+ const used = await sumOf(tx, sql `SELECT COALESCE(SUM(amount_cents), 0) AS n FROM credit_line_entry WHERE tenant_id = ${tenantId} AND authorization_id = ${id}`);
222
+ const held = await sumOf(tx, sql `SELECT COALESCE(SUM(amount), 0) AS n FROM credit_line_reservation WHERE tenant_id = ${tenantId} AND authorization_id = ${id} AND status = 'held'`);
223
+ const flag = await tx.execute(sql `
224
+ SELECT (revoked_at IS NULL AND starts_at <= ${NOW} AND ${NOW} < ends_at) AS in_force FROM credit_line_authorization WHERE id = ${id}
225
+ `);
226
+ return authStatus(row, used, held, flag[0]['in_force'] === true);
227
+ }
228
+ // Everything a limit check or a crossing needs, read in the operation's own transaction.
229
+ async function measure(tx, tenantId) {
230
+ const line = await loadLine(tx, tenantId);
231
+ const unpaid = await sumOf(tx, sql `SELECT COALESCE(SUM(amount_cents - paid_cents), 0) AS n FROM credit_line_entry WHERE tenant_id = ${tenantId}`);
232
+ const held = await sumOf(tx, sql `SELECT COALESCE(SUM(amount), 0) AS n FROM credit_line_reservation WHERE tenant_id = ${tenantId} AND status = 'held'`);
233
+ if (line === null)
234
+ return { line, auth: null, unpaid, held, caps: [], limits: [] };
235
+ const authRows = await tx.execute(sql `
236
+ SELECT * FROM credit_line_authorization
237
+ WHERE tenant_id = ${tenantId} AND revoked_at IS NULL AND starts_at <= ${NOW} AND ${NOW} < ends_at
238
+ ORDER BY starts_at DESC LIMIT 1
239
+ `);
240
+ const auth = authRows.length === 0 ? null : await statusOf(tx, tenantId, authRows[0]);
241
+ const warnAt = (limit, own) => Math.floor(limit * (own ?? line.warnShare ?? defaultShare) + 1e-9);
242
+ const limits = auth === null
243
+ ? [{ ref: { kind: 'ceiling', name: 'ceiling' }, limitCents: line.ceilingCents, usedCents: unpaid + held, warnAtCents: warnAt(line.ceilingCents) }]
244
+ : [{ ref: { kind: 'authorization', name: auth.reference }, limitCents: auth.amountCents, usedCents: auth.usedCents + auth.heldCents, warnAtCents: warnAt(auth.amountCents) }];
245
+ const caps = [];
246
+ for (const cap of line.caps) {
247
+ const { start, end } = windowBounds(cap.window);
248
+ const settled = await sumOf(tx, sql `
249
+ SELECT COALESCE(SUM(amount_cents), 0) AS n FROM credit_line_entry
250
+ WHERE tenant_id = ${tenantId} AND "at" >= ${start} AND "at" < ${end}
251
+ `);
252
+ const used = settled + held;
253
+ const replaced = auth !== null && auth.replacesCaps.includes(cap.name);
254
+ caps.push({ ...cap, usedCents: used, replaced });
255
+ if (!replaced)
256
+ limits.push({ ref: { kind: 'cap', name: cap.name }, limitCents: cap.cents, usedCents: used, warnAtCents: warnAt(cap.cents, cap.warnShare) });
257
+ }
258
+ return { line, auth, unpaid, held, caps, limits };
259
+ }
260
+ function stateOf(tenantId, s) {
261
+ const remaining = s.limits.map(l => l.limitCents - l.usedCents);
262
+ return {
263
+ tenantId,
264
+ open: s.line !== null && s.line.closedAt === null,
265
+ ceilingCents: s.line?.ceilingCents ?? 0,
266
+ warnShare: s.line?.warnShare ?? null,
267
+ unpaidCents: s.unpaid,
268
+ heldCents: s.held,
269
+ availableCents: remaining.length === 0 ? 0 : Math.max(0, Math.min(...remaining)),
270
+ caps: s.caps,
271
+ authorization: s.auth,
272
+ openedAt: s.line?.openedAt ?? null,
273
+ closedAt: s.line?.closedAt ?? null,
274
+ };
275
+ }
276
+ // A limit warns when a write takes its use from below its warn amount to at-or-above it. Compared
277
+ // within the write, so nothing is stored and a fall back below re-arms it. Awaited in the tenant's
278
+ // Flow, so a throwing hook rolls the write back.
279
+ async function fireCrossings(tenantId, inFlow, before, after) {
280
+ const fn = config.onLineCrossed;
281
+ if (fn === undefined)
282
+ return;
283
+ const key = (l) => `${l.ref.kind}:${l.ref.name}`;
284
+ const prior = new Map(before.limits.map(l => [key(l), l]));
285
+ for (const l of after.limits) {
286
+ const b = prior.get(key(l));
287
+ if (b === undefined || !(b.usedCents < b.warnAtCents && l.usedCents >= l.warnAtCents))
288
+ continue;
289
+ const event = { tenantId, limit: l.ref, usedCents: l.usedCents, limitCents: l.limitCents, warnAtCents: l.warnAtCents };
290
+ await inFlow(async () => { await fn(event); });
291
+ }
292
+ }
293
+ async function findReservation(tx, tenantId, jobId, lock) {
294
+ const rows = await tx.execute(lock
295
+ ? sql `SELECT * FROM credit_line_reservation WHERE tenant_id = ${tenantId} AND job_id = ${jobId} FOR UPDATE`
296
+ : sql `SELECT * FROM credit_line_reservation WHERE tenant_id = ${tenantId} AND job_id = ${jobId}`);
297
+ return rows.length === 0 ? null : rowToReservation(rows[0]);
298
+ }
299
+ // Pays unpaid entries from a payment's unapplied cents, oldest entry first.
300
+ async function applyPayment(tx, tenantId, paymentId, unapplied) {
301
+ const entries = await tx.execute(sql `
302
+ SELECT id, amount_cents, paid_cents FROM credit_line_entry
303
+ WHERE tenant_id = ${tenantId} AND amount_cents > paid_cents ORDER BY "at", id FOR UPDATE
304
+ `);
305
+ let left = unapplied;
306
+ for (const e of entries) {
307
+ if (left === 0)
308
+ break;
309
+ const take = Math.min(num(e['amount_cents']) - num(e['paid_cents']), left);
310
+ await tx.execute(sql `UPDATE credit_line_entry SET paid_cents = paid_cents + ${take} WHERE id = ${e['id']}`);
311
+ await tx.execute(sql `
312
+ INSERT INTO credit_line_payment_application (tenant_id, payment_id, entry_id, cents) VALUES (${tenantId}, ${paymentId}, ${e['id']}, ${take})
313
+ `);
314
+ left -= take;
315
+ }
316
+ await tx.execute(sql `UPDATE credit_line_payment SET unapplied_cents = ${left} WHERE id = ${paymentId}`);
317
+ return left;
318
+ }
319
+ // Pays one new entry from payments with unapplied cents, oldest payment first.
320
+ async function clearEntry(tx, tenantId, entryId, amount) {
321
+ const payments = await tx.execute(sql `
322
+ SELECT id, unapplied_cents FROM credit_line_payment
323
+ WHERE tenant_id = ${tenantId} AND unapplied_cents > 0 ORDER BY "at", id FOR UPDATE
324
+ `);
325
+ let need = amount;
326
+ for (const p of payments) {
327
+ if (need === 0)
328
+ break;
329
+ const take = Math.min(num(p['unapplied_cents']), need);
330
+ await tx.execute(sql `UPDATE credit_line_payment SET unapplied_cents = unapplied_cents - ${take} WHERE id = ${p['id']}`);
331
+ await tx.execute(sql `
332
+ INSERT INTO credit_line_payment_application (tenant_id, payment_id, entry_id, cents) VALUES (${tenantId}, ${p['id']}, ${entryId}, ${take})
333
+ `);
334
+ need -= take;
335
+ }
336
+ return amount - need;
337
+ }
338
+ async function doOpen(explicit, o) {
339
+ requireCents('ceilingCents', o.ceilingCents, 1);
340
+ if (o.warnShare !== undefined)
341
+ requireShare('warnShare', o.warnShare);
342
+ const caps = o.caps ?? [];
343
+ requireCaps(caps);
344
+ return run(explicit, async (tx, tenantId, inFlow) => {
345
+ await lockTenant(tx, tenantId);
346
+ const before = await measure(tx, tenantId);
347
+ const rows = await tx.execute(sql `
348
+ INSERT INTO credit_line (tenant_id, ceiling_cents, warn_share, caps, opened_at, closed_at)
349
+ VALUES (${tenantId}, ${o.ceilingCents}, ${o.warnShare ?? null}, ${capsToJson(caps)}::jsonb, ${NOW}, NULL)
350
+ ON CONFLICT (tenant_id) DO UPDATE
351
+ SET ceiling_cents = EXCLUDED.ceiling_cents, warn_share = EXCLUDED.warn_share, caps = EXCLUDED.caps, closed_at = NULL
352
+ RETURNING id
353
+ `);
354
+ const after = await measure(tx, tenantId);
355
+ const prev = before.line;
356
+ await auditStep(tx, tenantId, rows[0]['id'], 'open', {
357
+ ceilingCents: o.ceilingCents, warnShare: o.warnShare ?? null, caps: JSON.parse(capsToJson(caps)),
358
+ }, prev === null ? undefined : {
359
+ ceilingCents: prev.ceilingCents, warnShare: prev.warnShare, caps: JSON.parse(capsToJson(prev.caps)), closed: prev.closedAt !== null,
360
+ });
361
+ await fireCrossings(tenantId, inFlow, before, after);
362
+ return stateOf(tenantId, after);
363
+ });
364
+ }
365
+ async function doClose(explicit) {
366
+ return run(explicit, async (tx, tenantId) => {
367
+ await lockTenant(tx, tenantId);
368
+ const line = await requireLine(tx, tenantId, 'closeLine');
369
+ if (line.closedAt === null) {
370
+ await tx.execute(sql `UPDATE credit_line SET closed_at = ${NOW} WHERE id = ${line.id}`);
371
+ await auditStep(tx, tenantId, line.id, 'close', { closed: true });
372
+ }
373
+ return stateOf(tenantId, await measure(tx, tenantId));
374
+ });
375
+ }
376
+ async function doReserve(explicit, o) {
377
+ requireText('jobId', o.jobId);
378
+ requireCents('amountCents', o.amountCents, 1);
379
+ return run(explicit, async (tx, tenantId, inFlow) => {
380
+ await lockTenant(tx, tenantId);
381
+ const existing = await findReservation(tx, tenantId, o.jobId, false);
382
+ if (existing !== null)
383
+ return { ok: true, reservation: existing };
384
+ const before = await measure(tx, tenantId);
385
+ if (before.line === null || before.line.closedAt !== null)
386
+ return { ok: false, reason: 'no-line' };
387
+ for (const l of before.limits) {
388
+ if (l.usedCents + o.amountCents > l.limitCents) {
389
+ return { ok: false, reason: 'limit', limit: l.ref, limitCents: l.limitCents, usedCents: l.usedCents, requestedCents: o.amountCents };
390
+ }
391
+ }
392
+ const inserted = await tx.execute(sql `
393
+ INSERT INTO credit_line_reservation (tenant_id, job_id, amount, status, authorization_id, created_at)
394
+ VALUES (${tenantId}, ${o.jobId}, ${o.amountCents}, 'held', ${before.auth?.id ?? null}, ${NOW})
395
+ ON CONFLICT (tenant_id, job_id) DO NOTHING
396
+ RETURNING *
397
+ `);
398
+ if (inserted.length === 0)
399
+ return { ok: true, reservation: (await findReservation(tx, tenantId, o.jobId, false)) };
400
+ const reservation = rowToReservation(inserted[0]);
401
+ await auditStep(tx, tenantId, before.line.id, 'reserve', {
402
+ jobId: o.jobId, amountCents: o.amountCents, reservationId: reservation.id, authorizationId: reservation.authorizationId,
403
+ });
404
+ await fireCrossings(tenantId, inFlow, before, await measure(tx, tenantId));
405
+ return { ok: true, reservation };
406
+ });
407
+ }
408
+ async function doSettle(explicit, o) {
409
+ requireText('jobId', o.jobId);
410
+ requireCents('amountCents', o.amountCents, 0);
411
+ return run(explicit, async (tx, tenantId, inFlow) => {
412
+ await lockTenant(tx, tenantId);
413
+ const reservation = await findReservation(tx, tenantId, o.jobId, true);
414
+ if (reservation === null) {
415
+ throw new Error(`[pipework] creditLine.settle: no reservation for job "${o.jobId}".\n\n Call reserve({ jobId, amountCents }) before the job, with the same jobId.\n`);
416
+ }
417
+ if (reservation.status === 'released') {
418
+ throw new Error(`[pipework] creditLine.settle: the reservation for job "${o.jobId}" was released.\n`);
419
+ }
420
+ if (reservation.status === 'settled') {
421
+ if (reservation.settledAmountCents === o.amountCents)
422
+ return reservation;
423
+ throw new Error(`[pipework] creditLine.settle: job "${o.jobId}" was already settled at ${reservation.settledAmountCents}, not ${o.amountCents}.\n`);
424
+ }
425
+ const line = await requireLine(tx, tenantId, 'settle');
426
+ const before = await measure(tx, tenantId);
427
+ const rows = await tx.execute(sql `
428
+ UPDATE credit_line_reservation SET status = 'settled', settled_amount = ${o.amountCents}, settled_at = ${NOW}
429
+ WHERE id = ${reservation.id} RETURNING *
430
+ `);
431
+ const entry = await tx.execute(sql `
432
+ INSERT INTO credit_line_entry (tenant_id, job_id, reservation_id, amount_cents, paid_cents, authorization_id, "at")
433
+ VALUES (${tenantId}, ${o.jobId}, ${reservation.id}, ${o.amountCents}, 0, ${reservation.authorizationId}, ${NOW})
434
+ RETURNING id
435
+ `);
436
+ const entryId = entry[0]['id'];
437
+ const cleared = await clearEntry(tx, tenantId, entryId, o.amountCents);
438
+ if (cleared > 0)
439
+ await tx.execute(sql `UPDATE credit_line_entry SET paid_cents = ${cleared} WHERE id = ${entryId}`);
440
+ await auditStep(tx, tenantId, line.id, 'settle', {
441
+ jobId: o.jobId, amountCents: o.amountCents, reservedCents: reservation.amountCents, reservationId: reservation.id,
442
+ entryId, authorizationId: reservation.authorizationId, clearedCents: cleared,
443
+ });
444
+ await fireCrossings(tenantId, inFlow, before, await measure(tx, tenantId));
445
+ return rowToReservation(rows[0]);
446
+ });
447
+ }
448
+ async function doRelease(explicit, o) {
449
+ requireText('jobId', o.jobId);
450
+ return run(explicit, async (tx, tenantId) => {
451
+ await lockTenant(tx, tenantId);
452
+ const reservation = await findReservation(tx, tenantId, o.jobId, true);
453
+ if (reservation === null)
454
+ return { released: false, reservation: null };
455
+ if (reservation.status === 'released')
456
+ return { released: false, reservation };
457
+ if (reservation.status === 'settled') {
458
+ throw new Error(`[pipework] creditLine.release: job "${o.jobId}" was already settled; settled usage cannot be released.\n`);
459
+ }
460
+ const line = await requireLine(tx, tenantId, 'release');
461
+ const rows = await tx.execute(sql `
462
+ UPDATE credit_line_reservation SET status = 'released', settled_at = ${NOW} WHERE id = ${reservation.id} RETURNING *
463
+ `);
464
+ await auditStep(tx, tenantId, line.id, 'release', {
465
+ jobId: o.jobId, amountCents: reservation.amountCents, reservationId: reservation.id, authorizationId: reservation.authorizationId,
466
+ });
467
+ return { released: true, reservation: rowToReservation(rows[0]) };
468
+ });
469
+ }
470
+ async function doAuthorize(explicit, o) {
471
+ requireText('reference', o.reference);
472
+ requireCents('amountCents', o.amountCents, 1);
473
+ requireSpan('authorization dates', o.from, o.to);
474
+ const replaces = o.replacesCaps ?? [];
475
+ for (const name of replaces)
476
+ requireText('replacesCaps', name);
477
+ return run(explicit, async (tx, tenantId) => {
478
+ await lockTenant(tx, tenantId);
479
+ const line = await requireLine(tx, tenantId, 'authorize');
480
+ const found = await tx.execute(sql `SELECT * FROM credit_line_authorization WHERE tenant_id = ${tenantId} AND reference = ${o.reference}`);
481
+ if (found.length > 0)
482
+ return { authorization: rowToAuthorization(found[0]), created: false };
483
+ const clash = await tx.execute(sql `
484
+ SELECT reference FROM credit_line_authorization
485
+ WHERE tenant_id = ${tenantId} AND revoked_at IS NULL AND starts_at < ${utc(o.to)} AND ${utc(o.from)} < ends_at
486
+ LIMIT 1
487
+ `);
488
+ if (clash.length > 0) {
489
+ fail('from', `overlaps authorization "${clash[0]['reference']}"`, `Credit line authorization "${o.reference}" overlaps "${clash[0]['reference']}"; revoke that one or choose dates after it ends.`);
490
+ }
491
+ const rows = await tx.execute(sql `
492
+ INSERT INTO credit_line_authorization (tenant_id, reference, amount_cents, starts_at, ends_at, replaces_caps, created_at)
493
+ VALUES (${tenantId}, ${o.reference}, ${o.amountCents}, ${utc(o.from)}, ${utc(o.to)}, ${JSON.stringify(replaces)}::jsonb, ${NOW})
494
+ RETURNING *
495
+ `);
496
+ const authorization = rowToAuthorization(rows[0]);
497
+ await auditStep(tx, tenantId, line.id, 'authorize', {
498
+ reference: o.reference, amountCents: o.amountCents, from: o.from.toISOString(), to: o.to.toISOString(), replacesCaps: replaces,
499
+ authorizationId: authorization.id,
500
+ });
501
+ return { authorization, created: true };
502
+ });
503
+ }
504
+ async function doRevoke(explicit, reference) {
505
+ requireText('reference', reference);
506
+ return run(explicit, async (tx, tenantId) => {
507
+ await lockTenant(tx, tenantId);
508
+ const line = await requireLine(tx, tenantId, 'revokeAuthorization');
509
+ const found = await tx.execute(sql `SELECT * FROM credit_line_authorization WHERE tenant_id = ${tenantId} AND reference = ${reference} FOR UPDATE`);
510
+ if (found.length === 0) {
511
+ throw new Error(`[pipework] creditLine.revokeAuthorization: no authorization "${reference}" for tenant ${tenantId}.\n`);
512
+ }
513
+ const current = rowToAuthorization(found[0]);
514
+ if (current.revokedAt !== null)
515
+ return current;
516
+ const rows = await tx.execute(sql `UPDATE credit_line_authorization SET revoked_at = ${NOW} WHERE id = ${current.id} RETURNING *`);
517
+ await auditStep(tx, tenantId, line.id, 'revoke_authorization', { reference, authorizationId: current.id });
518
+ return rowToAuthorization(rows[0]);
519
+ });
520
+ }
521
+ async function doAuthorizations(explicit) {
522
+ return run(explicit, async (tx, tenantId) => {
523
+ const rows = await tx.execute(sql `SELECT * FROM credit_line_authorization WHERE tenant_id = ${tenantId} ORDER BY created_at, id`);
524
+ const out = [];
525
+ for (const row of rows)
526
+ out.push(await statusOf(tx, tenantId, row));
527
+ return out;
528
+ });
529
+ }
530
+ async function doPayment(explicit, o) {
531
+ requireText('reference', o.reference);
532
+ requireCents('amountCents', o.amountCents, 1);
533
+ if (o.at !== undefined && Number.isNaN(o.at.getTime()))
534
+ fail('at', 'must be a valid date', 'Credit line payment at must be a valid date.');
535
+ return run(explicit, async (tx, tenantId) => {
536
+ await lockTenant(tx, tenantId);
537
+ const line = await requireLine(tx, tenantId, 'recordPayment');
538
+ const found = await tx.execute(sql `SELECT * FROM credit_line_payment WHERE tenant_id = ${tenantId} AND reference = ${o.reference}`);
539
+ if (found.length > 0)
540
+ return { payment: rowToPayment(found[0]), created: false };
541
+ const rows = await tx.execute(sql `
542
+ INSERT INTO credit_line_payment (tenant_id, reference, amount_cents, unapplied_cents, "at")
543
+ VALUES (${tenantId}, ${o.reference}, ${o.amountCents}, ${o.amountCents}, ${o.at === undefined ? NOW : utc(o.at)})
544
+ RETURNING id
545
+ `);
546
+ const id = rows[0]['id'];
547
+ const left = await applyPayment(tx, tenantId, id, o.amountCents);
548
+ await auditStep(tx, tenantId, line.id, 'payment', {
549
+ reference: o.reference, amountCents: o.amountCents, clearedCents: o.amountCents - left, unappliedCents: left, paymentId: id,
550
+ });
551
+ const saved = await tx.execute(sql `SELECT * FROM credit_line_payment WHERE id = ${id}`);
552
+ return { payment: rowToPayment(saved[0]), created: true };
553
+ });
554
+ }
555
+ async function doStatement(explicit, o) {
556
+ requireSpan('statement period', o.from, o.to);
557
+ return run(explicit, async (tx, tenantId) => {
558
+ const entries = (await tx.execute(sql `
559
+ SELECT * FROM credit_line_entry WHERE tenant_id = ${tenantId} AND "at" >= ${utc(o.from)} AND "at" < ${utc(o.to)} ORDER BY "at", id
560
+ `)).map(rowToEntry);
561
+ const paymentRows = await tx.execute(sql `
562
+ SELECT * FROM credit_line_payment WHERE tenant_id = ${tenantId} AND "at" >= ${utc(o.from)} AND "at" < ${utc(o.to)} ORDER BY "at", id
563
+ `);
564
+ const payments = [];
565
+ for (const row of paymentRows) {
566
+ const apps = await tx.execute(sql `
567
+ SELECT a.entry_id, a.cents, e.job_id FROM credit_line_payment_application a
568
+ JOIN credit_line_entry e ON e.id = a.entry_id
569
+ WHERE a.tenant_id = ${tenantId} AND a.payment_id = ${row['id']} ORDER BY e."at", e.id
570
+ `);
571
+ payments.push({
572
+ ...rowToPayment(row),
573
+ applications: apps.map(a => ({ entryId: a['entry_id'], jobId: a['job_id'], cents: num(a['cents']) })),
574
+ });
575
+ }
576
+ const unpaid = await sumOf(tx, sql `SELECT COALESCE(SUM(amount_cents - paid_cents), 0) AS n FROM credit_line_entry WHERE tenant_id = ${tenantId}`);
577
+ return {
578
+ from: o.from,
579
+ to: o.to,
580
+ entries,
581
+ payments,
582
+ totals: {
583
+ settledCents: entries.reduce((s, e) => s + e.amountCents, 0),
584
+ paidCents: payments.reduce((s, p) => s + p.amountCents, 0),
585
+ unpaidCents: unpaid,
586
+ },
587
+ };
588
+ });
589
+ }
590
+ async function doLine(explicit) {
591
+ return run(explicit, async (tx, tenantId) => stateOf(tenantId, await measure(tx, tenantId)));
592
+ }
593
+ return {
594
+ openLine: o => doOpen(null, o),
595
+ openLineFor: (t, o) => doOpen(t, o),
596
+ closeLine: () => doClose(null),
597
+ closeLineFor: t => doClose(t),
598
+ reserve: o => doReserve(null, o),
599
+ reserveFor: (t, o) => doReserve(t, o),
600
+ settle: o => doSettle(null, o),
601
+ settleFor: (t, o) => doSettle(t, o),
602
+ release: o => doRelease(null, o),
603
+ releaseFor: (t, o) => doRelease(t, o),
604
+ authorize: o => doAuthorize(null, o),
605
+ authorizeFor: (t, o) => doAuthorize(t, o),
606
+ revokeAuthorization: r => doRevoke(null, r),
607
+ revokeAuthorizationFor: (t, r) => doRevoke(t, r),
608
+ authorizations: () => doAuthorizations(null),
609
+ authorizationsFor: t => doAuthorizations(t),
610
+ recordPayment: o => doPayment(null, o),
611
+ recordPaymentFor: (t, o) => doPayment(t, o),
612
+ statement: o => doStatement(null, o),
613
+ statementFor: (t, o) => doStatement(t, o),
614
+ line: () => doLine(null),
615
+ lineFor: t => doLine(t),
616
+ };
617
+ }
618
+ //# sourceMappingURL=handle.js.map