includio-cms 0.36.9 → 0.37.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 (243) hide show
  1. package/API.md +178 -3
  2. package/CHANGELOG.md +54 -0
  3. package/DOCS.md +28 -3
  4. package/ROADMAP.md +18 -0
  5. package/dist/admin/client/booking/booking-dashboard.svelte +40 -0
  6. package/dist/admin/client/booking/booking-dashboard.svelte.d.ts +3 -0
  7. package/dist/admin/client/booking/customer-panel.svelte +102 -0
  8. package/dist/admin/client/booking/customer-panel.svelte.d.ts +11 -0
  9. package/dist/admin/client/booking/customer-view.d.ts +52 -0
  10. package/dist/admin/client/booking/customer-view.js +134 -0
  11. package/dist/admin/client/booking/participant-data-panel.svelte +330 -0
  12. package/dist/admin/client/booking/participant-data-panel.svelte.d.ts +22 -0
  13. package/dist/admin/client/booking/reservation-detail-page.svelte +637 -0
  14. package/dist/admin/client/booking/reservation-detail-page.svelte.d.ts +3 -0
  15. package/dist/admin/client/booking/reservations-page.svelte +221 -0
  16. package/dist/admin/client/booking/reservations-page.svelte.d.ts +3 -0
  17. package/dist/admin/client/collection/collection-entries.svelte +1 -1
  18. package/dist/admin/client/collection/entry-link.svelte +6 -4
  19. package/dist/admin/client/collection/entry-link.svelte.d.ts +3 -1
  20. package/dist/admin/client/collection/status-badge.svelte +39 -3
  21. package/dist/admin/client/collection/status-badge.svelte.d.ts +5 -3
  22. package/dist/admin/client/entry/entry.svelte +26 -9
  23. package/dist/admin/client/entry/hybrid/hybrid-preview.svelte +73 -10
  24. package/dist/admin/client/entry/hybrid/hybrid-preview.svelte.d.ts +6 -5
  25. package/dist/admin/client/entry/preview-views-context.svelte.d.ts +22 -0
  26. package/dist/admin/client/entry/preview-views-context.svelte.js +58 -0
  27. package/dist/admin/client/entry/preview-views.d.ts +27 -0
  28. package/dist/admin/client/entry/preview-views.js +33 -0
  29. package/dist/admin/client/index.d.ts +3 -0
  30. package/dist/admin/client/index.js +3 -0
  31. package/dist/admin/client/modules/dashboard-registry.d.ts +7 -0
  32. package/dist/admin/client/modules/dashboard-registry.js +11 -0
  33. package/dist/admin/client/modules/module-dashboard-page.svelte +26 -0
  34. package/dist/admin/client/modules/module-dashboard-page.svelte.d.ts +6 -0
  35. package/dist/admin/client/modules/stat-card.svelte +22 -0
  36. package/dist/admin/client/modules/stat-card.svelte.d.ts +10 -0
  37. package/dist/admin/client/shop/shop-dashboard.svelte +29 -0
  38. package/dist/admin/client/shop/shop-dashboard.svelte.d.ts +3 -0
  39. package/dist/admin/components/fields/relation-field.svelte +11 -2
  40. package/dist/admin/components/icons/icon-map.js +8 -0
  41. package/dist/admin/components/layout/app-sidebar.svelte +45 -14
  42. package/dist/admin/components/layout/context-switcher.svelte +90 -0
  43. package/dist/admin/components/layout/context-switcher.svelte.d.ts +3 -0
  44. package/dist/admin/components/layout/lang.d.ts +4 -0
  45. package/dist/admin/components/layout/lang.js +8 -0
  46. package/dist/admin/components/layout/layout-renderer.svelte +2 -1
  47. package/dist/admin/components/layout/layout-tabs.svelte +14 -0
  48. package/dist/admin/components/layout/layout-tabs.svelte.d.ts +2 -0
  49. package/dist/admin/components/layout/nav-collections.svelte +16 -9
  50. package/dist/admin/components/layout/nav-collections.svelte.d.ts +5 -1
  51. package/dist/admin/components/layout/nav-global.svelte +45 -0
  52. package/dist/admin/components/layout/nav-global.svelte.d.ts +3 -0
  53. package/dist/admin/components/layout/nav-main.svelte +8 -32
  54. package/dist/admin/components/layout/nav-main.svelte.d.ts +6 -1
  55. package/dist/admin/components/layout/nav-module.svelte +57 -0
  56. package/dist/admin/components/layout/nav-module.svelte.d.ts +7 -0
  57. package/dist/admin/components/layout/nav-section.svelte +1 -1
  58. package/dist/admin/components/layout/nav-section.svelte.d.ts +1 -1
  59. package/dist/admin/components/layout/nav-singletons.svelte +14 -7
  60. package/dist/admin/components/layout/nav-singletons.svelte.d.ts +5 -1
  61. package/dist/admin/remote/booking.remote.d.ts +244 -0
  62. package/dist/admin/remote/booking.remote.js +283 -0
  63. package/dist/admin/remote/index.d.ts +2 -0
  64. package/dist/admin/remote/index.js +2 -0
  65. package/dist/admin/remote/modules.remote.d.ts +10 -0
  66. package/dist/admin/remote/modules.remote.js +14 -0
  67. package/dist/admin/state/active-context-resolve.d.ts +37 -0
  68. package/dist/admin/state/active-context-resolve.js +66 -0
  69. package/dist/admin/state/active-context.svelte.d.ts +8 -0
  70. package/dist/admin/state/active-context.svelte.js +27 -0
  71. package/dist/booking/adjustments.d.ts +33 -0
  72. package/dist/booking/adjustments.js +26 -0
  73. package/dist/booking/capacity.d.ts +12 -0
  74. package/dist/booking/capacity.js +10 -0
  75. package/dist/booking/client/index.d.ts +39 -0
  76. package/dist/booking/client/index.js +44 -0
  77. package/dist/booking/client/use-booking.svelte.d.ts +25 -0
  78. package/dist/booking/client/use-booking.svelte.js +56 -0
  79. package/dist/booking/config.d.ts +156 -0
  80. package/dist/booking/config.js +36 -0
  81. package/dist/booking/draft.d.ts +77 -0
  82. package/dist/booking/draft.js +67 -0
  83. package/dist/booking/expiry-logic.d.ts +30 -0
  84. package/dist/booking/expiry-logic.js +33 -0
  85. package/dist/booking/index.d.ts +18 -0
  86. package/dist/booking/index.js +52 -0
  87. package/dist/booking/ledger.d.ts +46 -0
  88. package/dist/booking/ledger.js +60 -0
  89. package/dist/booking/number.d.ts +2 -0
  90. package/dist/booking/number.js +11 -0
  91. package/dist/booking/participant-fields/access.d.ts +10 -0
  92. package/dist/booking/participant-fields/access.js +14 -0
  93. package/dist/booking/participant-fields/age.d.ts +12 -0
  94. package/dist/booking/participant-fields/age.js +28 -0
  95. package/dist/booking/participant-fields/completeness.d.ts +8 -0
  96. package/dist/booking/participant-fields/completeness.js +13 -0
  97. package/dist/booking/participant-fields/group.d.ts +6 -0
  98. package/dist/booking/participant-fields/group.js +15 -0
  99. package/dist/booking/participant-fields/index.d.ts +6 -0
  100. package/dist/booking/participant-fields/index.js +6 -0
  101. package/dist/booking/participant-fields/types.d.ts +50 -0
  102. package/dist/booking/participant-fields/types.js +5 -0
  103. package/dist/booking/participant-fields/validate.d.ts +7 -0
  104. package/dist/booking/participant-fields/validate.js +44 -0
  105. package/dist/booking/participant-pricing.d.ts +93 -0
  106. package/dist/booking/participant-pricing.js +91 -0
  107. package/dist/booking/participants-view.d.ts +32 -0
  108. package/dist/booking/participants-view.js +22 -0
  109. package/dist/booking/payment-context.d.ts +19 -0
  110. package/dist/booking/payment-context.js +29 -0
  111. package/dist/booking/pricing-inputs.d.ts +28 -0
  112. package/dist/booking/pricing-inputs.js +35 -0
  113. package/dist/booking/pricing.d.ts +86 -0
  114. package/dist/booking/pricing.js +125 -0
  115. package/dist/booking/recompute.d.ts +22 -0
  116. package/dist/booking/recompute.js +31 -0
  117. package/dist/booking/reminders-schedule.d.ts +11 -0
  118. package/dist/booking/reminders-schedule.js +28 -0
  119. package/dist/booking/server/adjustments.d.ts +27 -0
  120. package/dist/booking/server/adjustments.js +136 -0
  121. package/dist/booking/server/bookings.d.ts +45 -0
  122. package/dist/booking/server/bookings.js +288 -0
  123. package/dist/booking/server/catalog.d.ts +9 -0
  124. package/dist/booking/server/catalog.js +20 -0
  125. package/dist/booking/server/db.d.ts +7 -0
  126. package/dist/booking/server/db.js +18 -0
  127. package/dist/booking/server/email.d.ts +23 -0
  128. package/dist/booking/server/email.js +59 -0
  129. package/dist/booking/server/emailTemplateRegistry.d.ts +29 -0
  130. package/dist/booking/server/emailTemplateRegistry.js +125 -0
  131. package/dist/booking/server/http/create-handler.d.ts +5 -0
  132. package/dist/booking/server/http/create-handler.js +68 -0
  133. package/dist/booking/server/http/index.d.ts +4 -0
  134. package/dist/booking/server/http/index.js +4 -0
  135. package/dist/booking/server/http/maintenance-handler.d.ts +9 -0
  136. package/dist/booking/server/http/maintenance-handler.js +36 -0
  137. package/dist/booking/server/http/portal-handler.d.ts +13 -0
  138. package/dist/booking/server/http/portal-handler.js +99 -0
  139. package/dist/booking/server/http/webhook-handler.d.ts +9 -0
  140. package/dist/booking/server/http/webhook-handler.js +39 -0
  141. package/dist/booking/server/index.d.ts +12 -0
  142. package/dist/booking/server/index.js +14 -0
  143. package/dist/booking/server/maintenance.d.ts +28 -0
  144. package/dist/booking/server/maintenance.js +134 -0
  145. package/dist/booking/server/participants.d.ts +26 -0
  146. package/dist/booking/server/participants.js +92 -0
  147. package/dist/booking/server/payments.d.ts +34 -0
  148. package/dist/booking/server/payments.js +207 -0
  149. package/dist/booking/server/portal-session.d.ts +16 -0
  150. package/dist/booking/server/portal-session.js +42 -0
  151. package/dist/booking/server/portal.d.ts +207 -0
  152. package/dist/booking/server/portal.js +234 -0
  153. package/dist/booking/server/refunds.d.ts +7 -0
  154. package/dist/booking/server/refunds.js +58 -0
  155. package/dist/booking/server/reminders.d.ts +9 -0
  156. package/dist/booking/server/reminders.js +24 -0
  157. package/dist/booking/server/validation.d.ts +17 -0
  158. package/dist/booking/server/validation.js +55 -0
  159. package/dist/booking/state-machine.d.ts +41 -0
  160. package/dist/booking/state-machine.js +73 -0
  161. package/dist/booking/templates/_partials/footer.en.html +4 -0
  162. package/dist/booking/templates/_partials/footer.pl.html +4 -0
  163. package/dist/booking/templates/_partials/header.en.html +4 -0
  164. package/dist/booking/templates/_partials/header.pl.html +4 -0
  165. package/dist/booking/templates/balance-due.en.html +10 -0
  166. package/dist/booking/templates/balance-due.pl.html +10 -0
  167. package/dist/booking/templates/balance-overdue-cancelled.en.html +5 -0
  168. package/dist/booking/templates/balance-overdue-cancelled.pl.html +5 -0
  169. package/dist/booking/templates/booking-received.en.html +9 -0
  170. package/dist/booking/templates/booking-received.pl.html +9 -0
  171. package/dist/booking/templates/deposit-paid.en.html +6 -0
  172. package/dist/booking/templates/deposit-paid.pl.html +6 -0
  173. package/dist/booking/templates/payment-link.en.html +5 -0
  174. package/dist/booking/templates/payment-link.pl.html +5 -0
  175. package/dist/booking/templates/payment-received.en.html +10 -0
  176. package/dist/booking/templates/payment-received.pl.html +10 -0
  177. package/dist/booking/totals.d.ts +38 -0
  178. package/dist/booking/totals.js +61 -0
  179. package/dist/booking/webhook-logic.d.ts +28 -0
  180. package/dist/booking/webhook-logic.js +50 -0
  181. package/dist/cli/create-user.d.ts +7 -1
  182. package/dist/cli/create-user.js +23 -15
  183. package/dist/cli/index.js +55 -7
  184. package/dist/cli/scaffold/admin.d.ts +6 -0
  185. package/dist/cli/scaffold/admin.js +5 -3
  186. package/dist/cli/scaffold/booking.d.ts +12 -0
  187. package/dist/cli/scaffold/booking.js +103 -0
  188. package/dist/core/cms.d.ts +24 -9
  189. package/dist/core/cms.js +72 -34
  190. package/dist/core/config/validatePreviews.d.ts +12 -0
  191. package/dist/core/config/validatePreviews.js +90 -0
  192. package/dist/core/fields/slugPath.d.ts +9 -1
  193. package/dist/core/fields/slugPath.js +16 -2
  194. package/dist/core/server/fields/resolveRelationFields.d.ts +11 -2
  195. package/dist/core/server/fields/resolveRelationFields.js +12 -3
  196. package/dist/core/server/generator/generator.js +7 -2
  197. package/dist/db-postgres/schema/booking/bookingAdjustments.d.ts +186 -0
  198. package/dist/db-postgres/schema/booking/bookingAdjustments.js +22 -0
  199. package/dist/db-postgres/schema/booking/bookingItemPersons.d.ts +117 -0
  200. package/dist/db-postgres/schema/booking/bookingItemPersons.js +22 -0
  201. package/dist/db-postgres/schema/booking/bookingItems.d.ts +181 -0
  202. package/dist/db-postgres/schema/booking/bookingItems.js +22 -0
  203. package/dist/db-postgres/schema/booking/bookingPayments.d.ts +204 -0
  204. package/dist/db-postgres/schema/booking/bookingPayments.js +25 -0
  205. package/dist/db-postgres/schema/booking/bookingPersons.d.ts +99 -0
  206. package/dist/db-postgres/schema/booking/bookingPersons.js +17 -0
  207. package/dist/db-postgres/schema/booking/bookingReminders.d.ts +131 -0
  208. package/dist/db-postgres/schema/booking/bookingReminders.js +18 -0
  209. package/dist/db-postgres/schema/booking/bookings.d.ts +307 -0
  210. package/dist/db-postgres/schema/booking/bookings.js +40 -0
  211. package/dist/db-postgres/schema/booking/index.d.ts +7 -0
  212. package/dist/db-postgres/schema/booking/index.js +7 -0
  213. package/dist/db-postgres/schema/index.d.ts +1 -0
  214. package/dist/db-postgres/schema/index.js +1 -0
  215. package/dist/db-postgres/schema-booking.d.ts +1 -0
  216. package/dist/db-postgres/schema-booking.js +1 -0
  217. package/dist/server/security/rate-limit-handler.d.ts +19 -0
  218. package/dist/server/security/rate-limit-handler.js +34 -0
  219. package/dist/shop/adapters/payu/client.js +3 -1
  220. package/dist/shop/adapters/payu/index.js +37 -17
  221. package/dist/shop/adapters/payu/payload.d.ts +6 -0
  222. package/dist/shop/adapters/payu/payload.js +1 -1
  223. package/dist/shop/adapters/stripe/index.js +36 -21
  224. package/dist/shop/index.d.ts +11 -2
  225. package/dist/shop/index.js +43 -1
  226. package/dist/shop/rate-limit.d.ts +1 -9
  227. package/dist/shop/rate-limit.js +3 -28
  228. package/dist/shop/types.d.ts +19 -0
  229. package/dist/sveltekit/config.d.ts +4 -0
  230. package/dist/sveltekit/config.js +6 -0
  231. package/dist/types/cms.d.ts +15 -1
  232. package/dist/types/cms.schema.d.ts +96 -2
  233. package/dist/types/cms.schema.js +84 -1
  234. package/dist/types/config.d.ts +33 -0
  235. package/dist/types/layout.d.ts +6 -0
  236. package/dist/types/modules.d.ts +133 -0
  237. package/dist/types/modules.js +27 -0
  238. package/dist/updates/0.37.0/index.d.ts +2 -0
  239. package/dist/updates/0.37.0/index.js +52 -0
  240. package/dist/updates/index.js +3 -1
  241. package/package.json +19 -1
  242. package/dist/admin/components/layout/nav-shop.svelte +0 -33
  243. package/dist/admin/components/layout/nav-shop.svelte.d.ts +0 -3
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Booking discount rules engine — a pure, deterministic, module-agnostic
3
+ * function. No side effects, no clock access (the caller passes `now`).
4
+ *
5
+ * A rule is serializable data (`when` conditions → `then` effects). Rules carry
6
+ * an optional `group`: within a group the single largest-discount rule wins;
7
+ * across groups discounts sum. Amounts are in **minor units** (grosze).
8
+ */
9
+ /** A condition; all conditions in a rule's `when` must hold (logical AND). */
10
+ export type Condition = {
11
+ kind: 'minTripCount';
12
+ value: number;
13
+ } | {
14
+ kind: 'minParticipants';
15
+ value: number;
16
+ } | {
17
+ kind: 'minDaysBefore';
18
+ value: number;
19
+ };
20
+ /** An effect applied when a rule wins its group. */
21
+ export type Effect = {
22
+ kind: 'percentOff';
23
+ value: number;
24
+ } | {
25
+ kind: 'amountOff';
26
+ value: number;
27
+ } | {
28
+ kind: 'percentOffCheapestLine';
29
+ value: number;
30
+ } | {
31
+ kind: 'grantPerk';
32
+ label: string;
33
+ code?: string;
34
+ };
35
+ /** A serializable discount rule. */
36
+ export interface DiscountRule {
37
+ /** Stable id (labels, audit, future UI). */
38
+ id: string;
39
+ /** Human label for admin/portal/email; defaults to `id`. */
40
+ label?: string;
41
+ /** Exclusivity group; rules in the same group compete (one wins). Omitted ⇒ own singleton group. */
42
+ group?: string;
43
+ /** Tiebreak within a group when discount amounts are equal (higher wins). Default 0. */
44
+ priority?: number;
45
+ when: Condition[];
46
+ then: Effect[];
47
+ }
48
+ export interface PricingConfig {
49
+ rules?: DiscountRule[];
50
+ }
51
+ /** A single priced line that retains trip identity (for cheapest/distinct-count). */
52
+ export interface PricingLine {
53
+ /** Trip reference — identity for distinct-count and discount targeting. */
54
+ ref: string;
55
+ /** Unit price (minor units), per participant. */
56
+ unitPrice: number;
57
+ /** Participant count on this line. */
58
+ quantity: number;
59
+ /** Departure date — drives early-bird (earliest across lines). */
60
+ departureDate: Date | null;
61
+ /** Precomputed line gross (minor units); overrides unitPrice*quantity when set. */
62
+ subtotal?: number;
63
+ }
64
+ export interface AppliedRule {
65
+ ruleId: string;
66
+ /** Human label (rule.label ?? rule.id). */
67
+ label: string;
68
+ /** Monetary discount contributed, in minor units. */
69
+ amount: number;
70
+ /** Line `ref` this discount targets (set for line-targeted effects only). */
71
+ targetRef?: string;
72
+ }
73
+ export interface Perk {
74
+ code?: string;
75
+ label: string;
76
+ }
77
+ export interface PriceResult {
78
+ gross: number;
79
+ discountTotal: number;
80
+ net: number;
81
+ appliedRules: AppliedRule[];
82
+ perks: Perk[];
83
+ }
84
+ /** Whole days from `now` until `departureDate` (negative if departure is past). */
85
+ export declare function daysUntil(departureDate: Date, now: Date): number;
86
+ export declare function price(lines: PricingLine[], now: Date, config: PricingConfig): PriceResult;
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Booking discount rules engine — a pure, deterministic, module-agnostic
3
+ * function. No side effects, no clock access (the caller passes `now`).
4
+ *
5
+ * A rule is serializable data (`when` conditions → `then` effects). Rules carry
6
+ * an optional `group`: within a group the single largest-discount rule wins;
7
+ * across groups discounts sum. Amounts are in **minor units** (grosze).
8
+ */
9
+ const MS_PER_DAY = 86_400_000;
10
+ /** Whole days from `now` until `departureDate` (negative if departure is past). */
11
+ export function daysUntil(departureDate, now) {
12
+ return Math.floor((departureDate.getTime() - now.getTime()) / MS_PER_DAY);
13
+ }
14
+ function conditionHolds(c, ctx) {
15
+ switch (c.kind) {
16
+ case 'minTripCount':
17
+ return ctx.distinctTrips >= c.value;
18
+ case 'minParticipants':
19
+ return ctx.totalQty >= c.value;
20
+ case 'minDaysBefore':
21
+ return ctx.daysBefore >= c.value;
22
+ }
23
+ }
24
+ /** Monetary amount of an effect, clamped to [0, gross]; null for non-monetary effects. */
25
+ function effectMonetary(e, ctx) {
26
+ switch (e.kind) {
27
+ case 'percentOff':
28
+ return { amount: Math.max(0, Math.min(ctx.gross, Math.round((ctx.gross * e.value) / 100))) };
29
+ case 'amountOff':
30
+ return { amount: Math.max(0, Math.min(ctx.gross, Math.round(e.value))) };
31
+ case 'percentOffCheapestLine': {
32
+ if (!ctx.cheapest)
33
+ return null;
34
+ const amt = Math.round((ctx.cheapest.total * e.value) / 100);
35
+ return { amount: Math.max(0, Math.min(ctx.gross, amt)), targetRef: ctx.cheapest.ref };
36
+ }
37
+ case 'grantPerk':
38
+ return null;
39
+ }
40
+ }
41
+ /**
42
+ * Prices a set of lines against discount rules.
43
+ *
44
+ * @param lines - Priced lines retaining trip identity (minor units).
45
+ * @param now - Reference "current" time (injected for determinism).
46
+ * @param config - Discount rules (values from client config).
47
+ */
48
+ /** Line gross: explicit subtotal when provided, else unitPrice * quantity. */
49
+ function lineTotal(l) {
50
+ return l.subtotal ?? l.unitPrice * l.quantity;
51
+ }
52
+ export function price(lines, now, config) {
53
+ const gross = lines.reduce((s, l) => s + lineTotal(l), 0);
54
+ const totalQty = lines.reduce((s, l) => s + l.quantity, 0);
55
+ const distinctTrips = new Set(lines.map((l) => l.ref)).size;
56
+ const dated = lines.map((l) => l.departureDate).filter((d) => d instanceof Date);
57
+ const basis = dated.length ? new Date(Math.min(...dated.map((d) => d.getTime()))) : now;
58
+ const daysBefore = daysUntil(basis, now);
59
+ let cheapest = null;
60
+ for (const l of lines) {
61
+ const total = lineTotal(l);
62
+ if (!cheapest || total < cheapest.total)
63
+ cheapest = { ref: l.ref, total };
64
+ }
65
+ const ctx = { gross, totalQty, distinctTrips, daysBefore, cheapest };
66
+ // Eligible rules, preserving declaration order.
67
+ const eligible = (config.rules ?? [])
68
+ .map((r, i) => ({ r, i }))
69
+ .filter(({ r }) => r.when.every((c) => conditionHolds(c, ctx)));
70
+ // Bucket by exclusivity group (omitted group ⇒ unique singleton).
71
+ const groups = new Map();
72
+ for (const e of eligible) {
73
+ const key = e.r.group ?? `__single__${e.i}`;
74
+ const bucket = groups.get(key);
75
+ if (bucket)
76
+ bucket.push(e);
77
+ else
78
+ groups.set(key, [e]);
79
+ }
80
+ const appliedRules = [];
81
+ const perks = [];
82
+ for (const members of groups.values()) {
83
+ const evaluated = members.map(({ r, i }) => {
84
+ const effects = r.then.map((e) => ({ e, m: effectMonetary(e, ctx) }));
85
+ const monetary = effects.reduce((s, x) => s + (x.m?.amount ?? 0), 0);
86
+ return { r, i, effects, monetary };
87
+ });
88
+ // Winner: largest monetary, tie → higher priority, tie → earlier order.
89
+ evaluated.sort((a, b) => b.monetary - a.monetary || (b.r.priority ?? 0) - (a.r.priority ?? 0) || a.i - b.i);
90
+ const winner = evaluated[0];
91
+ if (!winner)
92
+ continue;
93
+ for (const { e, m } of winner.effects) {
94
+ if (e.kind === 'grantPerk') {
95
+ perks.push({ code: e.code, label: e.label });
96
+ }
97
+ else if (m && m.amount > 0) {
98
+ const ar = {
99
+ ruleId: winner.r.id,
100
+ label: winner.r.label ?? winner.r.id,
101
+ amount: m.amount
102
+ };
103
+ if (m.targetRef)
104
+ ar.targetRef = m.targetRef;
105
+ appliedRules.push(ar);
106
+ }
107
+ }
108
+ }
109
+ const discountTotal = Math.min(gross, appliedRules.reduce((s, a) => s + a.amount, 0));
110
+ const seen = new Set();
111
+ const dedupedPerks = perks.filter((p) => {
112
+ const k = p.code ?? p.label;
113
+ if (seen.has(k))
114
+ return false;
115
+ seen.add(k);
116
+ return true;
117
+ });
118
+ return {
119
+ gross,
120
+ discountTotal,
121
+ net: Math.max(0, gross - discountTotal),
122
+ appliedRules,
123
+ perks: dedupedPerks
124
+ };
125
+ }
@@ -0,0 +1,22 @@
1
+ import { type PaymentEntry } from './ledger.js';
2
+ import { type BookingTotals } from './totals.js';
3
+ import type { ParticipantPriceResult } from './participant-pricing.js';
4
+ import type { AdjustmentSums } from './adjustments.js';
5
+ import type { BookingStatus } from './state-machine.js';
6
+ /** Thrown when an edit is not allowed (terminal booking). */
7
+ export declare class BookingEditError extends Error {
8
+ constructor(message: string);
9
+ }
10
+ /**
11
+ * Pure recompute of a booking's money snapshot after an edit. Takes the
12
+ * already-computed per-participant pricing result (the caller prices from the
13
+ * edited or existing people/items) and folds in payments.
14
+ */
15
+ export declare function recomputeTotals(result: ParticipantPriceResult, sums: AdjustmentSums, payments: PaymentEntry[]): BookingTotals;
16
+ /** Block edits to terminal bookings (cancelled/completed). */
17
+ export declare function assertEditAllowed(status: BookingStatus): void;
18
+ /**
19
+ * Portal (self-service) edit guard: block terminal bookings AND any edit that
20
+ * would drop the gross below what's already paid (would owe a refund → staff).
21
+ */
22
+ export declare function assertPortalEditAllowed(status: BookingStatus, newGross: number, payments: PaymentEntry[]): void;
@@ -0,0 +1,31 @@
1
+ import { totalPaid, assertGrossNotBelowPaid } from './ledger.js';
2
+ import { resultToTotals } from './totals.js';
3
+ /** Thrown when an edit is not allowed (terminal booking). */
4
+ export class BookingEditError extends Error {
5
+ constructor(message) {
6
+ super(message);
7
+ this.name = 'BookingEditError';
8
+ }
9
+ }
10
+ /**
11
+ * Pure recompute of a booking's money snapshot after an edit. Takes the
12
+ * already-computed per-participant pricing result (the caller prices from the
13
+ * edited or existing people/items) and folds in payments.
14
+ */
15
+ export function recomputeTotals(result, sums, payments) {
16
+ return resultToTotals(result, sums, totalPaid(payments));
17
+ }
18
+ /** Block edits to terminal bookings (cancelled/completed). */
19
+ export function assertEditAllowed(status) {
20
+ if (status === 'cancelled' || status === 'completed') {
21
+ throw new BookingEditError(`Cannot edit a ${status} booking`);
22
+ }
23
+ }
24
+ /**
25
+ * Portal (self-service) edit guard: block terminal bookings AND any edit that
26
+ * would drop the gross below what's already paid (would owe a refund → staff).
27
+ */
28
+ export function assertPortalEditAllowed(status, newGross, payments) {
29
+ assertEditAllowed(status);
30
+ assertGrossNotBelowPaid(newGross, payments); // throws LedgerError when newGross < paid
31
+ }
@@ -0,0 +1,11 @@
1
+ import type { ReminderRule } from './config.js';
2
+ /** The earliest non-null departure across booking items, or null when none. */
3
+ export declare function earliestDeparture(dates: Array<Date | null>): Date | null;
4
+ /** Balance-due deadline = earliest departure − `balanceDueDaysBefore`; null if unconfigured. */
5
+ export declare function computeBalanceDueAt(dates: Array<Date | null>, balanceDueDaysBefore: number | null): Date | null;
6
+ /** One scheduled-reminder row per rule, anchored to the earliest departure. */
7
+ export declare function computeReminderSchedule(dates: Array<Date | null>, rules: ReminderRule[]): Array<{
8
+ type: string;
9
+ templateKey: string;
10
+ scheduledFor: Date;
11
+ }>;
@@ -0,0 +1,28 @@
1
+ const MS_PER_DAY = 86_400_000;
2
+ /** The earliest non-null departure across booking items, or null when none. */
3
+ export function earliestDeparture(dates) {
4
+ const valid = dates.filter((d) => d instanceof Date);
5
+ if (valid.length === 0)
6
+ return null;
7
+ return valid.reduce((min, d) => (d.getTime() < min.getTime() ? d : min));
8
+ }
9
+ /** Balance-due deadline = earliest departure − `balanceDueDaysBefore`; null if unconfigured. */
10
+ export function computeBalanceDueAt(dates, balanceDueDaysBefore) {
11
+ if (balanceDueDaysBefore == null)
12
+ return null;
13
+ const dep = earliestDeparture(dates);
14
+ if (!dep)
15
+ return null;
16
+ return new Date(dep.getTime() - balanceDueDaysBefore * MS_PER_DAY);
17
+ }
18
+ /** One scheduled-reminder row per rule, anchored to the earliest departure. */
19
+ export function computeReminderSchedule(dates, rules) {
20
+ const dep = earliestDeparture(dates);
21
+ if (!dep || rules.length === 0)
22
+ return [];
23
+ return rules.map((r) => ({
24
+ type: r.type,
25
+ templateKey: r.templateKey,
26
+ scheduledFor: new Date(dep.getTime() - r.daysBefore * MS_PER_DAY)
27
+ }));
28
+ }
@@ -0,0 +1,27 @@
1
+ import { type AdjustmentKind, type BookingAdjustment } from '../adjustments.js';
2
+ export interface AddAdjustmentInput {
3
+ kind: AdjustmentKind;
4
+ /** UI-entered POSITIVE magnitude (minor units). */
5
+ magnitude: number;
6
+ label: string;
7
+ reason?: string | null;
8
+ }
9
+ /** List all adjustments (active + voided) for a booking, newest first. */
10
+ export declare function listBookingAdjustments(bookingId: string): Promise<BookingAdjustment[]>;
11
+ /**
12
+ * Add a manual adjustment (surcharge/discount) and recompute the booking totals
13
+ * + derived status in one transaction. Rejects an adjustment that would push the
14
+ * net owed below zero. `amount` is stored signed (surcharge > 0, discount < 0).
15
+ */
16
+ export declare function addBookingAdjustment(bookingId: string, input: AddAdjustmentInput, opts: {
17
+ actor: string;
18
+ now?: Date;
19
+ }): Promise<BookingAdjustment>;
20
+ /**
21
+ * Soft-void an adjustment (kept for audit) and recompute the booking totals +
22
+ * status. Throws if the adjustment is missing or already voided.
23
+ */
24
+ export declare function voidBookingAdjustment(adjustmentId: string, opts: {
25
+ actor: string;
26
+ now?: Date;
27
+ }): Promise<void>;
@@ -0,0 +1,136 @@
1
+ import { and, desc, eq, isNull } from 'drizzle-orm';
2
+ import { getBookingDb, requireBookingConfig } from './db.js';
3
+ import { bookingsTable, bookingPaymentsTable, bookingAdjustmentsTable } from '../../db-postgres/schema/booking/index.js';
4
+ import { signedAmount, sumAdjustments, assertAdjustmentMagnitude } from '../adjustments.js';
5
+ import { recomputeTotals, assertEditAllowed } from '../recompute.js';
6
+ import { netOf } from '../totals.js';
7
+ import { deriveStatus } from '../state-machine.js';
8
+ import { priceFromExistingRows } from './bookings.js';
9
+ /** List all adjustments (active + voided) for a booking, newest first. */
10
+ export async function listBookingAdjustments(bookingId) {
11
+ const db = getBookingDb();
12
+ const rows = await db
13
+ .select()
14
+ .from(bookingAdjustmentsTable)
15
+ .where(eq(bookingAdjustmentsTable.bookingId, bookingId))
16
+ .orderBy(desc(bookingAdjustmentsTable.createdAt));
17
+ return rows;
18
+ }
19
+ /**
20
+ * Add a manual adjustment (surcharge/discount) and recompute the booking totals
21
+ * + derived status in one transaction. Rejects an adjustment that would push the
22
+ * net owed below zero. `amount` is stored signed (surcharge > 0, discount < 0).
23
+ */
24
+ export async function addBookingAdjustment(bookingId, input, opts) {
25
+ assertAdjustmentMagnitude(input.magnitude);
26
+ const config = requireBookingConfig();
27
+ const db = getBookingDb();
28
+ const now = opts.now ?? new Date();
29
+ const amount = signedAmount(input.kind, input.magnitude);
30
+ return db.transaction(async (tx) => {
31
+ const [booking] = await tx
32
+ .select()
33
+ .from(bookingsTable)
34
+ .where(eq(bookingsTable.id, bookingId))
35
+ .for('update');
36
+ if (!booking)
37
+ throw new Error(`Booking ${bookingId} not found`);
38
+ assertEditAllowed(booking.status);
39
+ const result = await priceFromExistingRows(tx, bookingId, config, now);
40
+ const active = await tx
41
+ .select()
42
+ .from(bookingAdjustmentsTable)
43
+ .where(and(eq(bookingAdjustmentsTable.bookingId, bookingId), isNull(bookingAdjustmentsTable.voidedAt)));
44
+ const prospective = sumAdjustments([...active, { amount, voidedAt: null }]);
45
+ const payments = (await tx
46
+ .select()
47
+ .from(bookingPaymentsTable)
48
+ .where(eq(bookingPaymentsTable.bookingId, bookingId))).map((p) => ({ amount: p.amount, status: p.status, kind: p.kind }));
49
+ const totals = recomputeTotals(result, prospective, payments);
50
+ // Raw (unclamped) net — netOf() floors at 0, so guard on the raw value.
51
+ const rawNet = totals.gross - totals.discount - totals.manualDiscount + totals.surcharge;
52
+ if (rawNet < 0) {
53
+ throw new Error('Adjustment would make the booking total negative');
54
+ }
55
+ const [row] = await tx
56
+ .insert(bookingAdjustmentsTable)
57
+ .values({
58
+ bookingId,
59
+ kind: input.kind,
60
+ amount,
61
+ label: input.label,
62
+ reason: input.reason ?? null,
63
+ createdBy: opts.actor
64
+ })
65
+ .returning();
66
+ await tx
67
+ .update(bookingsTable)
68
+ .set({
69
+ totals,
70
+ status: deriveStatus({
71
+ gross: netOf(totals),
72
+ paid: totals.paid,
73
+ depositRequired: config.depositAmount,
74
+ current: booking.status
75
+ }),
76
+ updatedAt: now
77
+ })
78
+ .where(eq(bookingsTable.id, bookingId));
79
+ return row;
80
+ });
81
+ }
82
+ /**
83
+ * Soft-void an adjustment (kept for audit) and recompute the booking totals +
84
+ * status. Throws if the adjustment is missing or already voided.
85
+ */
86
+ export async function voidBookingAdjustment(adjustmentId, opts) {
87
+ const config = requireBookingConfig();
88
+ const db = getBookingDb();
89
+ const now = opts.now ?? new Date();
90
+ await db.transaction(async (tx) => {
91
+ const [adj] = await tx
92
+ .select()
93
+ .from(bookingAdjustmentsTable)
94
+ .where(eq(bookingAdjustmentsTable.id, adjustmentId))
95
+ .for('update');
96
+ if (!adj)
97
+ throw new Error('Adjustment not found');
98
+ if (adj.voidedAt)
99
+ throw new Error('Adjustment already voided');
100
+ const [booking] = await tx
101
+ .select()
102
+ .from(bookingsTable)
103
+ .where(eq(bookingsTable.id, adj.bookingId))
104
+ .for('update');
105
+ if (!booking)
106
+ throw new Error('Booking not found');
107
+ assertEditAllowed(booking.status);
108
+ await tx
109
+ .update(bookingAdjustmentsTable)
110
+ .set({ voidedAt: now, voidedBy: opts.actor })
111
+ .where(eq(bookingAdjustmentsTable.id, adjustmentId));
112
+ const result = await priceFromExistingRows(tx, adj.bookingId, config, now);
113
+ const active = await tx
114
+ .select()
115
+ .from(bookingAdjustmentsTable)
116
+ .where(and(eq(bookingAdjustmentsTable.bookingId, adj.bookingId), isNull(bookingAdjustmentsTable.voidedAt)));
117
+ const payments = (await tx
118
+ .select()
119
+ .from(bookingPaymentsTable)
120
+ .where(eq(bookingPaymentsTable.bookingId, adj.bookingId))).map((p) => ({ amount: p.amount, status: p.status, kind: p.kind }));
121
+ const totals = recomputeTotals(result, sumAdjustments(active), payments);
122
+ await tx
123
+ .update(bookingsTable)
124
+ .set({
125
+ totals,
126
+ status: deriveStatus({
127
+ gross: netOf(totals),
128
+ paid: totals.paid,
129
+ depositRequired: config.depositAmount,
130
+ current: booking.status
131
+ }),
132
+ updatedAt: now
133
+ })
134
+ .where(eq(bookingsTable.id, adj.bookingId));
135
+ });
136
+ }
@@ -0,0 +1,45 @@
1
+ import { bookingsTable } from '../../db-postgres/schema/booking/index.js';
2
+ import { type CreateBookingInput, type PersonInput, type ItemAssignmentInput } from '../draft.js';
3
+ import { type ParticipantPriceResult } from '../participant-pricing.js';
4
+ import type { ResolvedBookingConfig } from '../config.js';
5
+ import { type BookingTx } from './reminders.js';
6
+ /** Edit payload: replace people + items (+assignments) and/or customer details. */
7
+ export interface BookingEditChanges {
8
+ persons?: PersonInput[];
9
+ items?: Array<{
10
+ tripRef: string;
11
+ departureDate: Date | null;
12
+ unitPrice: number;
13
+ assignments: ItemAssignmentInput[];
14
+ }>;
15
+ customer?: Record<string, unknown>;
16
+ }
17
+ /**
18
+ * Create a booking (public self-serve or staff). Validates catalog capacity +
19
+ * the client seam, prices the draft per-participant, and persists booking +
20
+ * people + items + assignments + reminders in one transaction. Status starts
21
+ * `draft`; the deposit payment (and its webhook) drives the status forward.
22
+ */
23
+ export declare function createBooking(input: CreateBookingInput, opts?: {
24
+ now?: Date;
25
+ }): Promise<{
26
+ booking: typeof bookingsTable.$inferSelect;
27
+ accessToken: string;
28
+ }>;
29
+ /**
30
+ * Edit a booking (staff or portal). Recomputes totals per-participant + derived
31
+ * status, enforces the actor's guards (`portal` blocks dropping below paid),
32
+ * re-runs capacity + validate seam when items change, replaces people/items/
33
+ * assignments, and reschedules reminders on a departure change. One transaction.
34
+ */
35
+ export declare function editBooking(bookingId: string, changes: BookingEditChanges, opts: {
36
+ actor: 'staff' | 'portal';
37
+ now?: Date;
38
+ }): Promise<typeof bookingsTable.$inferSelect>;
39
+ /**
40
+ * Price a booking from its CURRENTLY persisted people/items/assignments (no
41
+ * edit). Shared by adjustment recompute — mirrors the "no items change" branch
42
+ * of {@link editBooking}. Must run inside a transaction that has locked the
43
+ * booking row.
44
+ */
45
+ export declare function priceFromExistingRows(tx: BookingTx, bookingId: string, config: ResolvedBookingConfig, now: Date): Promise<ParticipantPriceResult>;