includio-cms 0.36.8 → 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 (259) hide show
  1. package/API.md +179 -3
  2. package/CHANGELOG.md +64 -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 +21 -2
  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/row-actions.svelte +12 -4
  21. package/dist/admin/client/collection/row-actions.svelte.d.ts +1 -0
  22. package/dist/admin/client/collection/status-badge.svelte +39 -3
  23. package/dist/admin/client/collection/status-badge.svelte.d.ts +5 -3
  24. package/dist/admin/client/entry/entry.svelte +26 -9
  25. package/dist/admin/client/entry/hybrid/hybrid-preview.svelte +73 -10
  26. package/dist/admin/client/entry/hybrid/hybrid-preview.svelte.d.ts +6 -5
  27. package/dist/admin/client/entry/preview-views-context.svelte.d.ts +22 -0
  28. package/dist/admin/client/entry/preview-views-context.svelte.js +58 -0
  29. package/dist/admin/client/entry/preview-views.d.ts +27 -0
  30. package/dist/admin/client/entry/preview-views.js +33 -0
  31. package/dist/admin/client/index.d.ts +3 -0
  32. package/dist/admin/client/index.js +3 -0
  33. package/dist/admin/client/modules/dashboard-registry.d.ts +7 -0
  34. package/dist/admin/client/modules/dashboard-registry.js +11 -0
  35. package/dist/admin/client/modules/module-dashboard-page.svelte +26 -0
  36. package/dist/admin/client/modules/module-dashboard-page.svelte.d.ts +6 -0
  37. package/dist/admin/client/modules/stat-card.svelte +22 -0
  38. package/dist/admin/client/modules/stat-card.svelte.d.ts +10 -0
  39. package/dist/admin/client/shop/shop-dashboard.svelte +29 -0
  40. package/dist/admin/client/shop/shop-dashboard.svelte.d.ts +3 -0
  41. package/dist/admin/components/fields/relation-field.svelte +11 -2
  42. package/dist/admin/components/icons/icon-map.js +8 -0
  43. package/dist/admin/components/layout/app-sidebar.svelte +45 -14
  44. package/dist/admin/components/layout/context-switcher.svelte +90 -0
  45. package/dist/admin/components/layout/context-switcher.svelte.d.ts +3 -0
  46. package/dist/admin/components/layout/lang.d.ts +4 -0
  47. package/dist/admin/components/layout/lang.js +8 -0
  48. package/dist/admin/components/layout/layout-renderer.svelte +2 -1
  49. package/dist/admin/components/layout/layout-tabs.svelte +14 -0
  50. package/dist/admin/components/layout/layout-tabs.svelte.d.ts +2 -0
  51. package/dist/admin/components/layout/nav-collections.svelte +16 -9
  52. package/dist/admin/components/layout/nav-collections.svelte.d.ts +5 -1
  53. package/dist/admin/components/layout/nav-global.svelte +45 -0
  54. package/dist/admin/components/layout/nav-global.svelte.d.ts +3 -0
  55. package/dist/admin/components/layout/nav-main.svelte +8 -32
  56. package/dist/admin/components/layout/nav-main.svelte.d.ts +6 -1
  57. package/dist/admin/components/layout/nav-module.svelte +57 -0
  58. package/dist/admin/components/layout/nav-module.svelte.d.ts +7 -0
  59. package/dist/admin/components/layout/nav-section.svelte +1 -1
  60. package/dist/admin/components/layout/nav-section.svelte.d.ts +1 -1
  61. package/dist/admin/components/layout/nav-singletons.svelte +14 -7
  62. package/dist/admin/components/layout/nav-singletons.svelte.d.ts +5 -1
  63. package/dist/admin/remote/booking.remote.d.ts +244 -0
  64. package/dist/admin/remote/booking.remote.js +283 -0
  65. package/dist/admin/remote/entry.remote.d.ts +3 -0
  66. package/dist/admin/remote/entry.remote.js +14 -3
  67. package/dist/admin/remote/index.d.ts +2 -0
  68. package/dist/admin/remote/index.js +2 -0
  69. package/dist/admin/remote/modules.remote.d.ts +10 -0
  70. package/dist/admin/remote/modules.remote.js +14 -0
  71. package/dist/admin/state/active-context-resolve.d.ts +37 -0
  72. package/dist/admin/state/active-context-resolve.js +66 -0
  73. package/dist/admin/state/active-context.svelte.d.ts +8 -0
  74. package/dist/admin/state/active-context.svelte.js +27 -0
  75. package/dist/booking/adjustments.d.ts +33 -0
  76. package/dist/booking/adjustments.js +26 -0
  77. package/dist/booking/capacity.d.ts +12 -0
  78. package/dist/booking/capacity.js +10 -0
  79. package/dist/booking/client/index.d.ts +39 -0
  80. package/dist/booking/client/index.js +44 -0
  81. package/dist/booking/client/use-booking.svelte.d.ts +25 -0
  82. package/dist/booking/client/use-booking.svelte.js +56 -0
  83. package/dist/booking/config.d.ts +156 -0
  84. package/dist/booking/config.js +36 -0
  85. package/dist/booking/draft.d.ts +77 -0
  86. package/dist/booking/draft.js +67 -0
  87. package/dist/booking/expiry-logic.d.ts +30 -0
  88. package/dist/booking/expiry-logic.js +33 -0
  89. package/dist/booking/index.d.ts +18 -0
  90. package/dist/booking/index.js +52 -0
  91. package/dist/booking/ledger.d.ts +46 -0
  92. package/dist/booking/ledger.js +60 -0
  93. package/dist/booking/number.d.ts +2 -0
  94. package/dist/booking/number.js +11 -0
  95. package/dist/booking/participant-fields/access.d.ts +10 -0
  96. package/dist/booking/participant-fields/access.js +14 -0
  97. package/dist/booking/participant-fields/age.d.ts +12 -0
  98. package/dist/booking/participant-fields/age.js +28 -0
  99. package/dist/booking/participant-fields/completeness.d.ts +8 -0
  100. package/dist/booking/participant-fields/completeness.js +13 -0
  101. package/dist/booking/participant-fields/group.d.ts +6 -0
  102. package/dist/booking/participant-fields/group.js +15 -0
  103. package/dist/booking/participant-fields/index.d.ts +6 -0
  104. package/dist/booking/participant-fields/index.js +6 -0
  105. package/dist/booking/participant-fields/types.d.ts +50 -0
  106. package/dist/booking/participant-fields/types.js +5 -0
  107. package/dist/booking/participant-fields/validate.d.ts +7 -0
  108. package/dist/booking/participant-fields/validate.js +44 -0
  109. package/dist/booking/participant-pricing.d.ts +93 -0
  110. package/dist/booking/participant-pricing.js +91 -0
  111. package/dist/booking/participants-view.d.ts +32 -0
  112. package/dist/booking/participants-view.js +22 -0
  113. package/dist/booking/payment-context.d.ts +19 -0
  114. package/dist/booking/payment-context.js +29 -0
  115. package/dist/booking/pricing-inputs.d.ts +28 -0
  116. package/dist/booking/pricing-inputs.js +35 -0
  117. package/dist/booking/pricing.d.ts +86 -0
  118. package/dist/booking/pricing.js +125 -0
  119. package/dist/booking/recompute.d.ts +22 -0
  120. package/dist/booking/recompute.js +31 -0
  121. package/dist/booking/reminders-schedule.d.ts +11 -0
  122. package/dist/booking/reminders-schedule.js +28 -0
  123. package/dist/booking/server/adjustments.d.ts +27 -0
  124. package/dist/booking/server/adjustments.js +136 -0
  125. package/dist/booking/server/bookings.d.ts +45 -0
  126. package/dist/booking/server/bookings.js +288 -0
  127. package/dist/booking/server/catalog.d.ts +9 -0
  128. package/dist/booking/server/catalog.js +20 -0
  129. package/dist/booking/server/db.d.ts +7 -0
  130. package/dist/booking/server/db.js +18 -0
  131. package/dist/booking/server/email.d.ts +23 -0
  132. package/dist/booking/server/email.js +59 -0
  133. package/dist/booking/server/emailTemplateRegistry.d.ts +29 -0
  134. package/dist/booking/server/emailTemplateRegistry.js +125 -0
  135. package/dist/booking/server/http/create-handler.d.ts +5 -0
  136. package/dist/booking/server/http/create-handler.js +68 -0
  137. package/dist/booking/server/http/index.d.ts +4 -0
  138. package/dist/booking/server/http/index.js +4 -0
  139. package/dist/booking/server/http/maintenance-handler.d.ts +9 -0
  140. package/dist/booking/server/http/maintenance-handler.js +36 -0
  141. package/dist/booking/server/http/portal-handler.d.ts +13 -0
  142. package/dist/booking/server/http/portal-handler.js +99 -0
  143. package/dist/booking/server/http/webhook-handler.d.ts +9 -0
  144. package/dist/booking/server/http/webhook-handler.js +39 -0
  145. package/dist/booking/server/index.d.ts +12 -0
  146. package/dist/booking/server/index.js +14 -0
  147. package/dist/booking/server/maintenance.d.ts +28 -0
  148. package/dist/booking/server/maintenance.js +134 -0
  149. package/dist/booking/server/participants.d.ts +26 -0
  150. package/dist/booking/server/participants.js +92 -0
  151. package/dist/booking/server/payments.d.ts +34 -0
  152. package/dist/booking/server/payments.js +207 -0
  153. package/dist/booking/server/portal-session.d.ts +16 -0
  154. package/dist/booking/server/portal-session.js +42 -0
  155. package/dist/booking/server/portal.d.ts +207 -0
  156. package/dist/booking/server/portal.js +234 -0
  157. package/dist/booking/server/refunds.d.ts +7 -0
  158. package/dist/booking/server/refunds.js +58 -0
  159. package/dist/booking/server/reminders.d.ts +9 -0
  160. package/dist/booking/server/reminders.js +24 -0
  161. package/dist/booking/server/validation.d.ts +17 -0
  162. package/dist/booking/server/validation.js +55 -0
  163. package/dist/booking/state-machine.d.ts +41 -0
  164. package/dist/booking/state-machine.js +73 -0
  165. package/dist/booking/templates/_partials/footer.en.html +4 -0
  166. package/dist/booking/templates/_partials/footer.pl.html +4 -0
  167. package/dist/booking/templates/_partials/header.en.html +4 -0
  168. package/dist/booking/templates/_partials/header.pl.html +4 -0
  169. package/dist/booking/templates/balance-due.en.html +10 -0
  170. package/dist/booking/templates/balance-due.pl.html +10 -0
  171. package/dist/booking/templates/balance-overdue-cancelled.en.html +5 -0
  172. package/dist/booking/templates/balance-overdue-cancelled.pl.html +5 -0
  173. package/dist/booking/templates/booking-received.en.html +9 -0
  174. package/dist/booking/templates/booking-received.pl.html +9 -0
  175. package/dist/booking/templates/deposit-paid.en.html +6 -0
  176. package/dist/booking/templates/deposit-paid.pl.html +6 -0
  177. package/dist/booking/templates/payment-link.en.html +5 -0
  178. package/dist/booking/templates/payment-link.pl.html +5 -0
  179. package/dist/booking/templates/payment-received.en.html +10 -0
  180. package/dist/booking/templates/payment-received.pl.html +10 -0
  181. package/dist/booking/totals.d.ts +38 -0
  182. package/dist/booking/totals.js +61 -0
  183. package/dist/booking/webhook-logic.d.ts +28 -0
  184. package/dist/booking/webhook-logic.js +50 -0
  185. package/dist/cli/create-user.d.ts +7 -1
  186. package/dist/cli/create-user.js +23 -15
  187. package/dist/cli/index.js +55 -7
  188. package/dist/cli/scaffold/admin.d.ts +6 -0
  189. package/dist/cli/scaffold/admin.js +5 -3
  190. package/dist/cli/scaffold/booking.d.ts +12 -0
  191. package/dist/cli/scaffold/booking.js +103 -0
  192. package/dist/core/cms.d.ts +24 -9
  193. package/dist/core/cms.js +72 -34
  194. package/dist/core/config/validatePreviews.d.ts +12 -0
  195. package/dist/core/config/validatePreviews.js +90 -0
  196. package/dist/core/fields/dotPath.d.ts +4 -0
  197. package/dist/core/fields/dotPath.js +18 -0
  198. package/dist/core/fields/slugPath.d.ts +9 -1
  199. package/dist/core/fields/slugPath.js +16 -2
  200. package/dist/core/fields/slugify.d.ts +3 -0
  201. package/dist/core/fields/slugify.js +4 -0
  202. package/dist/core/server/entries/operations/duplicate.d.ts +10 -0
  203. package/dist/core/server/entries/operations/duplicate.js +73 -0
  204. package/dist/core/server/entries/operations/slugNormalize.d.ts +8 -0
  205. package/dist/core/server/entries/operations/slugNormalize.js +20 -0
  206. package/dist/core/server/entries/operations/slugUniqueness.d.ts +17 -0
  207. package/dist/core/server/entries/operations/slugUniqueness.js +39 -0
  208. package/dist/core/server/fields/resolveRelationFields.d.ts +11 -2
  209. package/dist/core/server/fields/resolveRelationFields.js +12 -3
  210. package/dist/core/server/generator/generator.js +7 -2
  211. package/dist/db-postgres/schema/booking/bookingAdjustments.d.ts +186 -0
  212. package/dist/db-postgres/schema/booking/bookingAdjustments.js +22 -0
  213. package/dist/db-postgres/schema/booking/bookingItemPersons.d.ts +117 -0
  214. package/dist/db-postgres/schema/booking/bookingItemPersons.js +22 -0
  215. package/dist/db-postgres/schema/booking/bookingItems.d.ts +181 -0
  216. package/dist/db-postgres/schema/booking/bookingItems.js +22 -0
  217. package/dist/db-postgres/schema/booking/bookingPayments.d.ts +204 -0
  218. package/dist/db-postgres/schema/booking/bookingPayments.js +25 -0
  219. package/dist/db-postgres/schema/booking/bookingPersons.d.ts +99 -0
  220. package/dist/db-postgres/schema/booking/bookingPersons.js +17 -0
  221. package/dist/db-postgres/schema/booking/bookingReminders.d.ts +131 -0
  222. package/dist/db-postgres/schema/booking/bookingReminders.js +18 -0
  223. package/dist/db-postgres/schema/booking/bookings.d.ts +307 -0
  224. package/dist/db-postgres/schema/booking/bookings.js +40 -0
  225. package/dist/db-postgres/schema/booking/index.d.ts +7 -0
  226. package/dist/db-postgres/schema/booking/index.js +7 -0
  227. package/dist/db-postgres/schema/index.d.ts +1 -0
  228. package/dist/db-postgres/schema/index.js +1 -0
  229. package/dist/db-postgres/schema-booking.d.ts +1 -0
  230. package/dist/db-postgres/schema-booking.js +1 -0
  231. package/dist/server/security/rate-limit-handler.d.ts +19 -0
  232. package/dist/server/security/rate-limit-handler.js +34 -0
  233. package/dist/shop/adapters/payu/client.js +3 -1
  234. package/dist/shop/adapters/payu/index.js +37 -17
  235. package/dist/shop/adapters/payu/payload.d.ts +6 -0
  236. package/dist/shop/adapters/payu/payload.js +1 -1
  237. package/dist/shop/adapters/stripe/index.js +36 -21
  238. package/dist/shop/index.d.ts +11 -2
  239. package/dist/shop/index.js +43 -1
  240. package/dist/shop/rate-limit.d.ts +1 -9
  241. package/dist/shop/rate-limit.js +3 -28
  242. package/dist/shop/types.d.ts +19 -0
  243. package/dist/sveltekit/config.d.ts +4 -0
  244. package/dist/sveltekit/config.js +6 -0
  245. package/dist/types/cms.d.ts +15 -1
  246. package/dist/types/cms.schema.d.ts +96 -2
  247. package/dist/types/cms.schema.js +84 -1
  248. package/dist/types/config.d.ts +33 -0
  249. package/dist/types/layout.d.ts +6 -0
  250. package/dist/types/modules.d.ts +133 -0
  251. package/dist/types/modules.js +27 -0
  252. package/dist/updates/0.36.9/index.d.ts +2 -0
  253. package/dist/updates/0.36.9/index.js +12 -0
  254. package/dist/updates/0.37.0/index.d.ts +2 -0
  255. package/dist/updates/0.37.0/index.js +52 -0
  256. package/dist/updates/index.js +5 -1
  257. package/package.json +19 -1
  258. package/dist/admin/components/layout/nav-shop.svelte +0 -33
  259. package/dist/admin/components/layout/nav-shop.svelte.d.ts +0 -3
@@ -0,0 +1,92 @@
1
+ import { eq } from 'drizzle-orm';
2
+ import { getBookingDb, requireBookingConfig } from './db.js';
3
+ import { bookingItemsTable, bookingPersonsTable, bookingItemPersonsTable } from '../../db-postgres/schema/booking/index.js';
4
+ import { resolveParticipantFields } from '../config.js';
5
+ import { canFill, validateAgainstSchema } from '../participant-fields/index.js';
6
+ import { resolveEntry } from '../../core/server/entries/operations/resolveEntry.js';
7
+ /** Thrown when a write violates field types/min/max/option. Missing required is soft. */
8
+ export class ParticipantValidationError extends Error {
9
+ errors;
10
+ constructor(errors) {
11
+ super(errors.map((e) => e.message).join('; '));
12
+ this.errors = errors;
13
+ this.name = 'ParticipantValidationError';
14
+ }
15
+ }
16
+ /** Resolve the participant-field schema for a booking item's catalog entry. */
17
+ export async function resolveItemSchema(tripRef) {
18
+ const cfg = requireBookingConfig();
19
+ if (typeof cfg.participantFields !== 'function' || !cfg.catalogCollection) {
20
+ return resolveParticipantFields(cfg, {});
21
+ }
22
+ const entry = await resolveEntry({ id: tripRef, status: 'published' });
23
+ return resolveParticipantFields(cfg, (entry ?? {}));
24
+ }
25
+ /** A field is identity (stored once on the person) when it carries an identity role. */
26
+ function isIdentityField(f) {
27
+ return f.role === 'name' || f.role === 'birthDate';
28
+ }
29
+ /**
30
+ * Merge a partial patch into a person's assignment, scoped to fields the
31
+ * `scope` audience may fill. Identity fields (role name/birthDate) are written
32
+ * once to the person row (shared across trips); the rest to this trip's
33
+ * assignment. Validates types/min/max/option (hard reject); missing required is
34
+ * allowed (soft status, surfaced in the UI elsewhere).
35
+ *
36
+ * @param assignmentId - `booking_item_persons.id` (the person-on-this-trip row).
37
+ */
38
+ export async function saveParticipantData(assignmentId, incoming, scope) {
39
+ const db = getBookingDb();
40
+ const [assignment] = await db
41
+ .select()
42
+ .from(bookingItemPersonsTable)
43
+ .where(eq(bookingItemPersonsTable.id, assignmentId));
44
+ if (!assignment)
45
+ throw new Error('Participant not found');
46
+ const [item] = await db
47
+ .select()
48
+ .from(bookingItemsTable)
49
+ .where(eq(bookingItemsTable.id, assignment.itemId));
50
+ if (!item)
51
+ throw new Error('Booking item not found');
52
+ const [person] = await db
53
+ .select()
54
+ .from(bookingPersonsTable)
55
+ .where(eq(bookingPersonsTable.id, assignment.personId));
56
+ if (!person)
57
+ throw new Error('Person not found');
58
+ const fields = await resolveItemSchema(item.tripRef);
59
+ const editable = new Set(fields.filter((f) => canFill(f, scope)).map((f) => f.key));
60
+ const identityKeys = new Set(fields.filter(isIdentityField).map((f) => f.key));
61
+ // Drop any key the audience is not allowed to write; split identity vs per-trip.
62
+ const identityPatch = {};
63
+ const tripPatch = {};
64
+ for (const [k, v] of Object.entries(incoming)) {
65
+ if (!editable.has(k))
66
+ continue;
67
+ if (identityKeys.has(k))
68
+ identityPatch[k] = v;
69
+ else
70
+ tripPatch[k] = v;
71
+ }
72
+ const mergedPerson = { ...person.data, ...identityPatch };
73
+ const mergedAssignment = { ...assignment.data, ...tripPatch };
74
+ // Validate the full combined record (identity + per-trip) against the schema.
75
+ const combined = { ...mergedPerson, ...mergedAssignment };
76
+ const hard = validateAgainstSchema(combined, fields).filter((e) => e.code !== 'required');
77
+ if (hard.length > 0) {
78
+ throw new ParticipantValidationError(hard.map((e) => ({ key: e.key, message: e.message })));
79
+ }
80
+ if (Object.keys(identityPatch).length > 0) {
81
+ await db
82
+ .update(bookingPersonsTable)
83
+ .set({ data: mergedPerson, updatedAt: new Date() })
84
+ .where(eq(bookingPersonsTable.id, person.id));
85
+ }
86
+ if (Object.keys(tripPatch).length > 0) {
87
+ await db
88
+ .update(bookingItemPersonsTable)
89
+ .set({ data: mergedAssignment, updatedAt: new Date() })
90
+ .where(eq(bookingItemPersonsTable.id, assignmentId));
91
+ }
92
+ }
@@ -0,0 +1,34 @@
1
+ import type { PaymentEvent } from '../../shop/types.js';
2
+ import type { PaymentKind } from '../ledger.js';
3
+ /**
4
+ * Create the deposit payment for a freshly-created booking: ask the first
5
+ * configured adapter for a payment (charging only the deposit amount via an
6
+ * `OrderRef` with `totalGross = depositAmount`), and record a pending ledger row
7
+ * keyed by the provider ref. Returns the redirect for the storefront.
8
+ */
9
+ export declare function createDepositPayment(bookingId: string): Promise<{
10
+ redirectUrl: string | null;
11
+ status: 'redirect' | 'manual' | 'error';
12
+ providerRef: string | null;
13
+ }>;
14
+ /**
15
+ * Apply a provider webhook event to the ledger, idempotently. Locks the payment
16
+ * row (and its booking) `FOR UPDATE` so a concurrent duplicate webhook can't
17
+ * double-apply: the second caller sees a settled row and no-ops. Recomputes the
18
+ * booking totals + derived status and clears the draft hold on first deposit.
19
+ */
20
+ export declare function applyWebhookPayment(event: PaymentEvent): Promise<{
21
+ applied: boolean;
22
+ bookingId: string | null;
23
+ kind: PaymentKind | null;
24
+ }>;
25
+ /**
26
+ * Generate an installment/balance payment link for an arbitrary amount (staff or
27
+ * portal). Records a pending ledger row keyed by the provider ref; the webhook
28
+ * settles it later.
29
+ */
30
+ export declare function createInstallmentPayment(bookingId: string, amount: number): Promise<{
31
+ paymentLink: string | null;
32
+ }>;
33
+ /** Record a manual cash payment (staff). Settles immediately and recomputes status. */
34
+ export declare function recordCash(bookingId: string, amount: number): Promise<void>;
@@ -0,0 +1,207 @@
1
+ import { eq } from 'drizzle-orm';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { getBookingDb, requireBookingConfig } from './db.js';
4
+ import { bookingsTable, bookingPaymentsTable } from '../../db-postgres/schema/booking/index.js';
5
+ import { applyPaymentEvent } from '../webhook-logic.js';
6
+ import { deriveStatus } from '../state-machine.js';
7
+ import { netOf } from '../totals.js';
8
+ import { buildPaymentContext } from '../payment-context.js';
9
+ /**
10
+ * Create the deposit payment for a freshly-created booking: ask the first
11
+ * configured adapter for a payment (charging only the deposit amount via an
12
+ * `OrderRef` with `totalGross = depositAmount`), and record a pending ledger row
13
+ * keyed by the provider ref. Returns the redirect for the storefront.
14
+ */
15
+ export async function createDepositPayment(bookingId) {
16
+ const config = requireBookingConfig();
17
+ const db = getBookingDb();
18
+ const [booking] = await db.select().from(bookingsTable).where(eq(bookingsTable.id, bookingId));
19
+ if (!booking)
20
+ throw new Error(`Booking ${bookingId} not found`);
21
+ const adapter = config.payment[0];
22
+ if (!adapter || config.depositAmount <= 0) {
23
+ return { redirectUrl: null, status: 'manual', providerRef: null };
24
+ }
25
+ const ref = {
26
+ id: booking.id,
27
+ number: booking.number,
28
+ totalGross: config.depositAmount,
29
+ currency: config.currency,
30
+ customerEmail: String(booking.customer.email ?? '')
31
+ };
32
+ const baseCtx = buildPaymentContext({
33
+ accessToken: booking.accessToken,
34
+ language: booking.language,
35
+ customer: booking.customer,
36
+ portalUrlTemplate: config.portalUrlTemplate
37
+ });
38
+ const ctx = {
39
+ ...baseCtx,
40
+ // Deposit lands on the checkout return page (e.g. thank-you) when configured;
41
+ // otherwise falls back to the portal like other payments.
42
+ continueUrl: config.checkoutReturnUrl ?? baseCtx.continueUrl,
43
+ // PayU's extOrderId must be unique per POS — a booking pays more than once
44
+ // (deposit + balance), so vary it per attempt while keeping the number visible.
45
+ externalId: `${booking.number}-${randomUUID().slice(0, 8)}`
46
+ };
47
+ const result = await adapter.createPayment(ref, ctx);
48
+ await db.insert(bookingPaymentsTable).values({
49
+ bookingId: booking.id,
50
+ kind: 'deposit',
51
+ amount: config.depositAmount,
52
+ status: 'pending',
53
+ providerRef: result.providerRef ?? null,
54
+ paymentLink: result.redirectUrl ?? null,
55
+ createdBy: 'system'
56
+ });
57
+ return {
58
+ redirectUrl: result.redirectUrl ?? null,
59
+ status: result.status,
60
+ providerRef: result.providerRef ?? null
61
+ };
62
+ }
63
+ /**
64
+ * Apply a provider webhook event to the ledger, idempotently. Locks the payment
65
+ * row (and its booking) `FOR UPDATE` so a concurrent duplicate webhook can't
66
+ * double-apply: the second caller sees a settled row and no-ops. Recomputes the
67
+ * booking totals + derived status and clears the draft hold on first deposit.
68
+ */
69
+ export async function applyWebhookPayment(event) {
70
+ const config = requireBookingConfig();
71
+ const db = getBookingDb();
72
+ if (!event.providerRef)
73
+ return { applied: false, bookingId: null, kind: null };
74
+ return db.transaction(async (tx) => {
75
+ const [payment] = await tx
76
+ .select()
77
+ .from(bookingPaymentsTable)
78
+ .where(eq(bookingPaymentsTable.providerRef, event.providerRef))
79
+ .for('update');
80
+ if (!payment)
81
+ return { applied: false, bookingId: null, kind: null };
82
+ const [booking] = await tx
83
+ .select()
84
+ .from(bookingsTable)
85
+ .where(eq(bookingsTable.id, payment.bookingId))
86
+ .for('update');
87
+ if (!booking)
88
+ return { applied: false, bookingId: null, kind: null };
89
+ const others = (await tx
90
+ .select()
91
+ .from(bookingPaymentsTable)
92
+ .where(eq(bookingPaymentsTable.bookingId, booking.id)))
93
+ .filter((p) => p.id !== payment.id)
94
+ .map((p) => ({ amount: p.amount, status: p.status, kind: p.kind }));
95
+ const r = applyPaymentEvent({
96
+ event,
97
+ payment: { status: payment.status, amount: payment.amount },
98
+ otherPayments: others,
99
+ gross: netOf(booking.totals),
100
+ depositRequired: config.depositAmount,
101
+ currentStatus: booking.status
102
+ });
103
+ if (r.alreadyApplied)
104
+ return { applied: false, bookingId: booking.id, kind: payment.kind };
105
+ await tx
106
+ .update(bookingPaymentsTable)
107
+ .set({
108
+ status: r.newPaymentStatus,
109
+ paidAt: r.newPaymentStatus === 'paid' ? new Date() : null,
110
+ updatedAt: new Date()
111
+ })
112
+ .where(eq(bookingPaymentsTable.id, payment.id));
113
+ await tx
114
+ .update(bookingsTable)
115
+ .set({
116
+ status: r.newStatus,
117
+ totals: {
118
+ ...booking.totals,
119
+ paid: r.newTotalsPaid,
120
+ balance: Math.max(0, netOf(booking.totals) - r.newTotalsPaid)
121
+ },
122
+ holdExpiresAt: r.clearHold ? null : booking.holdExpiresAt,
123
+ updatedAt: new Date()
124
+ })
125
+ .where(eq(bookingsTable.id, booking.id));
126
+ return { applied: r.newPaymentStatus === 'paid', bookingId: booking.id, kind: payment.kind };
127
+ });
128
+ }
129
+ /**
130
+ * Generate an installment/balance payment link for an arbitrary amount (staff or
131
+ * portal). Records a pending ledger row keyed by the provider ref; the webhook
132
+ * settles it later.
133
+ */
134
+ export async function createInstallmentPayment(bookingId, amount) {
135
+ const config = requireBookingConfig();
136
+ const db = getBookingDb();
137
+ const [booking] = await db.select().from(bookingsTable).where(eq(bookingsTable.id, bookingId));
138
+ if (!booking)
139
+ throw new Error('Booking not found');
140
+ const adapter = config.payment[0];
141
+ if (!adapter)
142
+ throw new Error('No payment adapter configured');
143
+ const ctx = {
144
+ ...buildPaymentContext({
145
+ accessToken: booking.accessToken,
146
+ language: booking.language,
147
+ customer: booking.customer,
148
+ portalUrlTemplate: config.portalUrlTemplate
149
+ }),
150
+ // PayU's extOrderId must be unique per POS — a booking pays more than once
151
+ // (deposit + balance), so vary it per attempt while keeping the number visible.
152
+ externalId: `${booking.number}-${randomUUID().slice(0, 8)}`
153
+ };
154
+ const result = await adapter.createPayment({
155
+ id: booking.id,
156
+ number: booking.number,
157
+ totalGross: amount,
158
+ currency: config.currency,
159
+ customerEmail: String(booking.customer.email ?? '')
160
+ }, ctx);
161
+ await db.insert(bookingPaymentsTable).values({
162
+ bookingId,
163
+ kind: 'installment',
164
+ amount,
165
+ status: 'pending',
166
+ providerRef: result.providerRef ?? null,
167
+ paymentLink: result.redirectUrl ?? null,
168
+ createdBy: 'staff'
169
+ });
170
+ return { paymentLink: result.redirectUrl ?? null };
171
+ }
172
+ /** Record a manual cash payment (staff). Settles immediately and recomputes status. */
173
+ export async function recordCash(bookingId, amount) {
174
+ const config = requireBookingConfig();
175
+ const db = getBookingDb();
176
+ await db.transaction(async (tx) => {
177
+ const [b] = await tx
178
+ .select()
179
+ .from(bookingsTable)
180
+ .where(eq(bookingsTable.id, bookingId))
181
+ .for('update');
182
+ if (!b)
183
+ throw new Error('Booking not found');
184
+ await tx.insert(bookingPaymentsTable).values({
185
+ bookingId,
186
+ kind: 'cash',
187
+ amount,
188
+ status: 'paid',
189
+ createdBy: 'staff',
190
+ paidAt: new Date()
191
+ });
192
+ const paid = b.totals.paid + amount;
193
+ await tx
194
+ .update(bookingsTable)
195
+ .set({
196
+ totals: { ...b.totals, paid, balance: Math.max(0, netOf(b.totals) - paid) },
197
+ status: deriveStatus({
198
+ gross: netOf(b.totals),
199
+ paid,
200
+ depositRequired: config.depositAmount,
201
+ current: b.status
202
+ }),
203
+ updatedAt: new Date()
204
+ })
205
+ .where(eq(bookingsTable.id, bookingId));
206
+ });
207
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Bezstanowa sesja portalu (2. czynnik: telefon) + normalizacja numeru.
3
+ * Czyste funkcje — bez dostępu do zegara/env (wstrzykiwane), testowalne wprost.
4
+ * Sesja = podpisane HMAC cookie `"<expSec>.<hmac>"`; token w linku pozostaje
5
+ * 1. czynnikiem, numer telefonu = 2.
6
+ */
7
+ /** Numer do porównania: same cyfry, ostatnie 9. */
8
+ export declare function normalizePhone(raw: string): string;
9
+ /** True gdy oba numery mają identyczną, pełną (9-cyfrową) końcówkę. */
10
+ export declare function phonesMatch(a: string, b: string): boolean;
11
+ /** Wartość cookie sesji portalu: `"<expSec>.<hmac>"`. */
12
+ export declare function signSession(token: string, issuedAtSec: number, ttlSec: number, secret: string): string;
13
+ /** Poprawny podpis dla tokenu i nie wygasł (`nowSec < exp`). Stałoczasowe porównanie. */
14
+ export declare function verifySession(cookieValue: string, token: string, nowSec: number, secret: string): boolean;
15
+ /** Nazwa cookie per-rezerwacja (hash tokenu — nie wycieka tokenu w nazwie). */
16
+ export declare function portalCookieName(token: string): string;
@@ -0,0 +1,42 @@
1
+ import { createHmac, timingSafeEqual } from 'node:crypto';
2
+ /**
3
+ * Bezstanowa sesja portalu (2. czynnik: telefon) + normalizacja numeru.
4
+ * Czyste funkcje — bez dostępu do zegara/env (wstrzykiwane), testowalne wprost.
5
+ * Sesja = podpisane HMAC cookie `"<expSec>.<hmac>"`; token w linku pozostaje
6
+ * 1. czynnikiem, numer telefonu = 2.
7
+ */
8
+ /** Numer do porównania: same cyfry, ostatnie 9. */
9
+ export function normalizePhone(raw) {
10
+ return (raw ?? '').replace(/\D+/g, '').slice(-9);
11
+ }
12
+ /** True gdy oba numery mają identyczną, pełną (9-cyfrową) końcówkę. */
13
+ export function phonesMatch(a, b) {
14
+ const na = normalizePhone(a);
15
+ return na.length === 9 && na === normalizePhone(b);
16
+ }
17
+ function hmac(secret, payload) {
18
+ return createHmac('sha256', secret).update(payload).digest('base64url');
19
+ }
20
+ /** Wartość cookie sesji portalu: `"<expSec>.<hmac>"`. */
21
+ export function signSession(token, issuedAtSec, ttlSec, secret) {
22
+ const exp = issuedAtSec + ttlSec;
23
+ return `${exp}.${hmac(secret, `${token}.${exp}`)}`;
24
+ }
25
+ /** Poprawny podpis dla tokenu i nie wygasł (`nowSec < exp`). Stałoczasowe porównanie. */
26
+ export function verifySession(cookieValue, token, nowSec, secret) {
27
+ if (!cookieValue || !secret)
28
+ return false;
29
+ const dot = cookieValue.indexOf('.');
30
+ if (dot <= 0)
31
+ return false;
32
+ const exp = Number(cookieValue.slice(0, dot));
33
+ if (!Number.isFinite(exp) || nowSec >= exp)
34
+ return false;
35
+ const sig = Buffer.from(cookieValue.slice(dot + 1));
36
+ const expected = Buffer.from(hmac(secret, `${token}.${exp}`));
37
+ return sig.length === expected.length && timingSafeEqual(sig, expected);
38
+ }
39
+ /** Nazwa cookie per-rezerwacja (hash tokenu — nie wycieka tokenu w nazwie). */
40
+ export function portalCookieName(token) {
41
+ return 'pz_portal_' + createHmac('sha256', 'portal-cookie').update(token).digest('hex').slice(0, 16);
42
+ }
@@ -0,0 +1,207 @@
1
+ import { bookingsTable, bookingItemsTable } from '../../db-postgres/schema/booking/index.js';
2
+ import { type PaymentEntry } from '../ledger.js';
3
+ import { type BookingEditChanges } from './bookings.js';
4
+ import { type FieldDef } from '../participant-fields/index.js';
5
+ import { type FlatParticipant } from '../participants-view.js';
6
+ type BookingRow = typeof bookingsTable.$inferSelect;
7
+ type ItemRow = typeof bookingItemsTable.$inferSelect;
8
+ /**
9
+ * Pure: shape a portal-safe DTO. Explicitly whitelists fields — `staffNotes`,
10
+ * `deletedBy`, and other internals never reach the customer.
11
+ */
12
+ export declare function toPortalView(booking: BookingRow, items: ItemRow[], participants: FlatParticipant[], payments: Array<PaymentEntry & {
13
+ paidAt?: Date | null;
14
+ }>, schemaByItem?: Record<string, FieldDef[]>): {
15
+ number: string;
16
+ status: import("../state-machine.js").BookingStatus;
17
+ currency: string;
18
+ customer: Record<string, unknown>;
19
+ totals: import("../../db-postgres/schema/booking/index.js").BookingTotals;
20
+ balanceDueAt: Date | null;
21
+ items: {
22
+ participantFields: FieldDef[];
23
+ id: string;
24
+ createdAt: Date;
25
+ updatedAt: Date;
26
+ bookingId: string;
27
+ tripRef: string;
28
+ departureDate: Date | null;
29
+ participantsCount: number;
30
+ unitPriceSnapshot: number;
31
+ subtotalSnapshot: number | null;
32
+ appliedDiscounts: import("../pricing.js").AppliedRule[];
33
+ }[];
34
+ participants: FlatParticipant[];
35
+ payments: {
36
+ amount: number;
37
+ status: import("../ledger.js").PaymentStatus;
38
+ kind: import("../ledger.js").PaymentKind | undefined;
39
+ paidAt: Date | null;
40
+ }[];
41
+ ledger: import("../ledger.js").LedgerSummary;
42
+ };
43
+ /** Resolve a booking by its unguessable portal access token (active only). */
44
+ export declare function resolveByToken(token: string): Promise<BookingRow | null>;
45
+ /** Drugi czynnik: zwraca booking gdy podany numer pasuje do `customer.phone`. */
46
+ export declare function verifyPhone(token: string, phone: string): Promise<BookingRow | null>;
47
+ export interface BookingStatusItem {
48
+ tripRef: string;
49
+ departureDate: string | null;
50
+ unitPrice: number;
51
+ }
52
+ export interface BookingStatus {
53
+ number: string;
54
+ status: string;
55
+ currency: string;
56
+ totals: {
57
+ gross: number;
58
+ paid: number;
59
+ balance: number;
60
+ };
61
+ items: BookingStatusItem[];
62
+ /** Payment link of a still-pending deposit (drives a „Zapłać zadatek" CTA), or null. */
63
+ depositLink: string | null;
64
+ }
65
+ export declare function bookingStatusByToken(token: string): Promise<BookingStatus | null>;
66
+ /** Portal view: booking + items + participants + ledger (no staff data). */
67
+ export declare function portalView(token: string): Promise<{
68
+ number: string;
69
+ status: import("../state-machine.js").BookingStatus;
70
+ currency: string;
71
+ customer: Record<string, unknown>;
72
+ totals: import("../../db-postgres/schema/booking/index.js").BookingTotals;
73
+ balanceDueAt: Date | null;
74
+ items: {
75
+ participantFields: FieldDef[];
76
+ id: string;
77
+ createdAt: Date;
78
+ updatedAt: Date;
79
+ bookingId: string;
80
+ tripRef: string;
81
+ departureDate: Date | null;
82
+ participantsCount: number;
83
+ unitPriceSnapshot: number;
84
+ subtotalSnapshot: number | null;
85
+ appliedDiscounts: import("../pricing.js").AppliedRule[];
86
+ }[];
87
+ participants: FlatParticipant[];
88
+ payments: {
89
+ amount: number;
90
+ status: import("../ledger.js").PaymentStatus;
91
+ kind: import("../ledger.js").PaymentKind | undefined;
92
+ paidAt: Date | null;
93
+ }[];
94
+ ledger: import("../ledger.js").LedgerSummary;
95
+ } | null>;
96
+ /** Portal self-service edit (actor:'portal' guards). Throws → handler maps to 409/400. */
97
+ export declare function portalEdit(token: string, changes: BookingEditChanges): Promise<{
98
+ number: string;
99
+ status: import("../state-machine.js").BookingStatus;
100
+ currency: string;
101
+ customer: Record<string, unknown>;
102
+ totals: import("../../db-postgres/schema/booking/index.js").BookingTotals;
103
+ balanceDueAt: Date | null;
104
+ items: {
105
+ participantFields: FieldDef[];
106
+ id: string;
107
+ createdAt: Date;
108
+ updatedAt: Date;
109
+ bookingId: string;
110
+ tripRef: string;
111
+ departureDate: Date | null;
112
+ participantsCount: number;
113
+ unitPriceSnapshot: number;
114
+ subtotalSnapshot: number | null;
115
+ appliedDiscounts: import("../pricing.js").AppliedRule[];
116
+ }[];
117
+ participants: FlatParticipant[];
118
+ payments: {
119
+ amount: number;
120
+ status: import("../ledger.js").PaymentStatus;
121
+ kind: import("../ledger.js").PaymentKind | undefined;
122
+ paidAt: Date | null;
123
+ }[];
124
+ ledger: import("../ledger.js").LedgerSummary;
125
+ } | null>;
126
+ /** Portal: create an installment/balance payment link. */
127
+ export declare function portalPay(token: string, amount: number): Promise<{
128
+ paymentLink: string | null;
129
+ } | null>;
130
+ /**
131
+ * Portal self-service: customer fills their own participant data. Verifies the
132
+ * assignment belongs to the token's booking (no cross-booking writes), then
133
+ * saves with `'customer'` scope (staff-only fields are dropped, not an error).
134
+ * Throws {@link ParticipantValidationError} on type/min/max/option violations.
135
+ *
136
+ * @param assignmentId - `booking_item_persons.id` (person-on-this-trip row).
137
+ */
138
+ export declare function portalUpdateParticipant(token: string, assignmentId: string, data: Record<string, unknown>): Promise<{
139
+ number: string;
140
+ status: import("../state-machine.js").BookingStatus;
141
+ currency: string;
142
+ customer: Record<string, unknown>;
143
+ totals: import("../../db-postgres/schema/booking/index.js").BookingTotals;
144
+ balanceDueAt: Date | null;
145
+ items: {
146
+ participantFields: FieldDef[];
147
+ id: string;
148
+ createdAt: Date;
149
+ updatedAt: Date;
150
+ bookingId: string;
151
+ tripRef: string;
152
+ departureDate: Date | null;
153
+ participantsCount: number;
154
+ unitPriceSnapshot: number;
155
+ subtotalSnapshot: number | null;
156
+ appliedDiscounts: import("../pricing.js").AppliedRule[];
157
+ }[];
158
+ participants: FlatParticipant[];
159
+ payments: {
160
+ amount: number;
161
+ status: import("../ledger.js").PaymentStatus;
162
+ kind: import("../ledger.js").PaymentKind | undefined;
163
+ paidAt: Date | null;
164
+ }[];
165
+ ledger: import("../ledger.js").LedgerSummary;
166
+ } | null>;
167
+ /**
168
+ * Self-service dobieranie wycieczki: doczytaj obecny stan, scal z `additions`
169
+ * (tylko istniejące osoby), przelicz przez `editBooking` (full-replace → recompute
170
+ * tym samym silnikiem per-uczestnik co koszyk). Saldo rośnie; dopłata istniejącym
171
+ * flow. `assignments` w additions = lista `booking_persons.id`. Kotwica nowej
172
+ * pozycji (`departureDate`) = `customer.arrivalDate`.
173
+ */
174
+ export declare function portalAddTrips(token: string, additions: Array<{
175
+ tripRef: string;
176
+ unitPrice: number;
177
+ assignments: string[];
178
+ }>): Promise<{
179
+ number: string;
180
+ status: import("../state-machine.js").BookingStatus;
181
+ currency: string;
182
+ customer: Record<string, unknown>;
183
+ totals: import("../../db-postgres/schema/booking/index.js").BookingTotals;
184
+ balanceDueAt: Date | null;
185
+ items: {
186
+ participantFields: FieldDef[];
187
+ id: string;
188
+ createdAt: Date;
189
+ updatedAt: Date;
190
+ bookingId: string;
191
+ tripRef: string;
192
+ departureDate: Date | null;
193
+ participantsCount: number;
194
+ unitPriceSnapshot: number;
195
+ subtotalSnapshot: number | null;
196
+ appliedDiscounts: import("../pricing.js").AppliedRule[];
197
+ }[];
198
+ participants: FlatParticipant[];
199
+ payments: {
200
+ amount: number;
201
+ status: import("../ledger.js").PaymentStatus;
202
+ kind: import("../ledger.js").PaymentKind | undefined;
203
+ paidAt: Date | null;
204
+ }[];
205
+ ledger: import("../ledger.js").LedgerSummary;
206
+ } | null>;
207
+ export {};