includio-cms 0.36.9 → 0.37.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (247) hide show
  1. package/API.md +178 -3
  2. package/CHANGELOG.md +77 -0
  3. package/DOCS.md +28 -3
  4. package/ROADMAP.md +32 -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 +247 -0
  62. package/dist/admin/remote/booking.remote.js +286 -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 +42 -0
  76. package/dist/booking/client/index.js +48 -0
  77. package/dist/booking/client/use-booking.svelte.d.ts +24 -0
  78. package/dist/booking/client/use-booking.svelte.js +56 -0
  79. package/dist/booking/config.d.ts +176 -0
  80. package/dist/booking/config.js +37 -0
  81. package/dist/booking/draft.d.ts +96 -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 +139 -0
  121. package/dist/booking/server/bookings.d.ts +51 -0
  122. package/dist/booking/server/bookings.js +336 -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 +70 -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 +103 -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 +206 -0
  152. package/dist/booking/server/portal.js +236 -0
  153. package/dist/booking/server/price-source.d.ts +73 -0
  154. package/dist/booking/server/price-source.js +94 -0
  155. package/dist/booking/server/refunds.d.ts +7 -0
  156. package/dist/booking/server/refunds.js +58 -0
  157. package/dist/booking/server/reminders.d.ts +9 -0
  158. package/dist/booking/server/reminders.js +24 -0
  159. package/dist/booking/server/validation.d.ts +17 -0
  160. package/dist/booking/server/validation.js +55 -0
  161. package/dist/booking/state-machine.d.ts +41 -0
  162. package/dist/booking/state-machine.js +73 -0
  163. package/dist/booking/templates/_partials/footer.en.html +4 -0
  164. package/dist/booking/templates/_partials/footer.pl.html +4 -0
  165. package/dist/booking/templates/_partials/header.en.html +4 -0
  166. package/dist/booking/templates/_partials/header.pl.html +4 -0
  167. package/dist/booking/templates/balance-due.en.html +10 -0
  168. package/dist/booking/templates/balance-due.pl.html +10 -0
  169. package/dist/booking/templates/balance-overdue-cancelled.en.html +5 -0
  170. package/dist/booking/templates/balance-overdue-cancelled.pl.html +5 -0
  171. package/dist/booking/templates/booking-received.en.html +9 -0
  172. package/dist/booking/templates/booking-received.pl.html +9 -0
  173. package/dist/booking/templates/deposit-paid.en.html +6 -0
  174. package/dist/booking/templates/deposit-paid.pl.html +6 -0
  175. package/dist/booking/templates/payment-link.en.html +5 -0
  176. package/dist/booking/templates/payment-link.pl.html +5 -0
  177. package/dist/booking/templates/payment-received.en.html +10 -0
  178. package/dist/booking/templates/payment-received.pl.html +10 -0
  179. package/dist/booking/totals.d.ts +38 -0
  180. package/dist/booking/totals.js +61 -0
  181. package/dist/booking/webhook-logic.d.ts +28 -0
  182. package/dist/booking/webhook-logic.js +50 -0
  183. package/dist/cli/create-user.d.ts +7 -1
  184. package/dist/cli/create-user.js +23 -15
  185. package/dist/cli/index.js +55 -7
  186. package/dist/cli/scaffold/admin.d.ts +6 -0
  187. package/dist/cli/scaffold/admin.js +5 -3
  188. package/dist/cli/scaffold/booking.d.ts +12 -0
  189. package/dist/cli/scaffold/booking.js +103 -0
  190. package/dist/core/cms.d.ts +24 -9
  191. package/dist/core/cms.js +72 -34
  192. package/dist/core/config/validatePreviews.d.ts +12 -0
  193. package/dist/core/config/validatePreviews.js +90 -0
  194. package/dist/core/fields/slugPath.d.ts +9 -1
  195. package/dist/core/fields/slugPath.js +16 -2
  196. package/dist/core/server/fields/resolveRelationFields.d.ts +11 -2
  197. package/dist/core/server/fields/resolveRelationFields.js +12 -3
  198. package/dist/core/server/generator/generator.js +7 -2
  199. package/dist/db-postgres/schema/booking/bookingAdjustments.d.ts +186 -0
  200. package/dist/db-postgres/schema/booking/bookingAdjustments.js +22 -0
  201. package/dist/db-postgres/schema/booking/bookingItemPersons.d.ts +117 -0
  202. package/dist/db-postgres/schema/booking/bookingItemPersons.js +22 -0
  203. package/dist/db-postgres/schema/booking/bookingItems.d.ts +181 -0
  204. package/dist/db-postgres/schema/booking/bookingItems.js +22 -0
  205. package/dist/db-postgres/schema/booking/bookingPayments.d.ts +204 -0
  206. package/dist/db-postgres/schema/booking/bookingPayments.js +25 -0
  207. package/dist/db-postgres/schema/booking/bookingPersons.d.ts +99 -0
  208. package/dist/db-postgres/schema/booking/bookingPersons.js +17 -0
  209. package/dist/db-postgres/schema/booking/bookingReminders.d.ts +131 -0
  210. package/dist/db-postgres/schema/booking/bookingReminders.js +18 -0
  211. package/dist/db-postgres/schema/booking/bookings.d.ts +324 -0
  212. package/dist/db-postgres/schema/booking/bookings.js +52 -0
  213. package/dist/db-postgres/schema/booking/index.d.ts +7 -0
  214. package/dist/db-postgres/schema/booking/index.js +7 -0
  215. package/dist/db-postgres/schema/index.d.ts +1 -0
  216. package/dist/db-postgres/schema/index.js +1 -0
  217. package/dist/db-postgres/schema-booking.d.ts +1 -0
  218. package/dist/db-postgres/schema-booking.js +1 -0
  219. package/dist/server/security/rate-limit-handler.d.ts +19 -0
  220. package/dist/server/security/rate-limit-handler.js +34 -0
  221. package/dist/shop/adapters/payu/client.js +3 -1
  222. package/dist/shop/adapters/payu/index.js +37 -17
  223. package/dist/shop/adapters/payu/payload.d.ts +6 -0
  224. package/dist/shop/adapters/payu/payload.js +1 -1
  225. package/dist/shop/adapters/stripe/index.js +36 -21
  226. package/dist/shop/index.d.ts +11 -2
  227. package/dist/shop/index.js +43 -1
  228. package/dist/shop/rate-limit.d.ts +1 -9
  229. package/dist/shop/rate-limit.js +3 -28
  230. package/dist/shop/types.d.ts +19 -0
  231. package/dist/sveltekit/config.d.ts +4 -0
  232. package/dist/sveltekit/config.js +6 -0
  233. package/dist/types/cms.d.ts +15 -1
  234. package/dist/types/cms.schema.d.ts +96 -2
  235. package/dist/types/cms.schema.js +84 -1
  236. package/dist/types/config.d.ts +33 -0
  237. package/dist/types/layout.d.ts +6 -0
  238. package/dist/types/modules.d.ts +133 -0
  239. package/dist/types/modules.js +27 -0
  240. package/dist/updates/0.37.0/index.d.ts +2 -0
  241. package/dist/updates/0.37.0/index.js +52 -0
  242. package/dist/updates/0.37.1/index.d.ts +2 -0
  243. package/dist/updates/0.37.1/index.js +24 -0
  244. package/dist/updates/index.js +5 -1
  245. package/package.json +19 -1
  246. package/dist/admin/components/layout/nav-shop.svelte +0 -33
  247. package/dist/admin/components/layout/nav-shop.svelte.d.ts +0 -3
@@ -0,0 +1,70 @@
1
+ import { json } from '@sveltejs/kit';
2
+ import { getCMS } from '../../../core/cms.js';
3
+ import { checkRateLimit, clientKey } from '../../../server/security/rate-limit-handler.js';
4
+ import { createBooking } from '../bookings.js';
5
+ import { createDepositPayment } from '../payments.js';
6
+ function bookingEnabled() {
7
+ try {
8
+ return getCMS().hasModule('booking');
9
+ }
10
+ catch {
11
+ return false;
12
+ }
13
+ }
14
+ /** Public self-serve booking creation. Mount at `POST /api/booking`. */
15
+ export function createBookingHandler() {
16
+ return {
17
+ POST: async ({ request }) => {
18
+ if (!bookingEnabled())
19
+ return json({ error: 'Booking not enabled' }, { status: 404 });
20
+ const rl = checkRateLimit(clientKey(request, 'booking-create'), { limit: 5, windowSec: 60 });
21
+ if (!rl.allowed)
22
+ return json({ error: 'Rate limit exceeded' }, { status: 429 });
23
+ const body = (await request.json().catch(() => null));
24
+ if (!body || !Array.isArray(body.items) || body.items.length === 0) {
25
+ return json({ error: 'items required' }, { status: 400 });
26
+ }
27
+ try {
28
+ const persons = Array.isArray(body.persons)
29
+ ? body.persons.map((p) => ({
30
+ ref: String(p.ref),
31
+ identity: p.identity && typeof p.identity === 'object' ? p.identity : {}
32
+ }))
33
+ : [];
34
+ // `unitPrice` z ciała żądania jest ŚWIADOMIE POMIJANY. Cenę ustala rdzeń z katalogu
35
+ // (szew `resolvePrice`) — inaczej każdy mógłby zamówić wyprawę za grosz.
36
+ const items = body.items.map((it) => ({
37
+ tripRef: String(it.tripRef),
38
+ unitPrice: 0, // nadpisywane przez bramkę cenową (`actor: 'public'`)
39
+ departureDate: it.departureDate ? new Date(it.departureDate) : null,
40
+ assignments: Array.isArray(it.assignments)
41
+ ? it.assignments.map((a) => ({
42
+ personRef: String(a.personRef),
43
+ data: a.data && typeof a.data === 'object' ? a.data : undefined
44
+ }))
45
+ : []
46
+ }));
47
+ const { booking, accessToken } = await createBooking({
48
+ customer: body.customer ?? {},
49
+ persons,
50
+ items,
51
+ language: body.language
52
+ }, { actor: 'public' });
53
+ const payment = await createDepositPayment(booking.id);
54
+ return json({
55
+ bookingId: booking.id,
56
+ number: booking.number,
57
+ accessToken,
58
+ status: booking.status,
59
+ totals: booking.totals,
60
+ paymentStatus: payment.status,
61
+ requiresPaymentRedirect: payment.status === 'redirect',
62
+ redirectUrl: payment.redirectUrl
63
+ });
64
+ }
65
+ catch (err) {
66
+ return json({ error: err instanceof Error ? err.message : 'Booking failed' }, { status: 400 });
67
+ }
68
+ }
69
+ };
70
+ }
@@ -0,0 +1,4 @@
1
+ export { createBookingHandler } from './create-handler.js';
2
+ export { createWebhookHandler } from './webhook-handler.js';
3
+ export { createPortalHandler } from './portal-handler.js';
4
+ export { createMaintenanceHandler } from './maintenance-handler.js';
@@ -0,0 +1,4 @@
1
+ export { createBookingHandler } from './create-handler.js';
2
+ export { createWebhookHandler } from './webhook-handler.js';
3
+ export { createPortalHandler } from './portal-handler.js';
4
+ export { createMaintenanceHandler } from './maintenance-handler.js';
@@ -0,0 +1,9 @@
1
+ import { type RequestHandler } from '@sveltejs/kit';
2
+ /**
3
+ * Secured external trigger for booking maintenance — the swap-in for the
4
+ * in-process loop (e.g. a system cron hitting this endpoint). Requires
5
+ * `Authorization: Bearer <maintenanceToken>`. Mount at `POST /api/booking/maintenance`.
6
+ */
7
+ export declare function createMaintenanceHandler(): {
8
+ POST: RequestHandler;
9
+ };
@@ -0,0 +1,36 @@
1
+ import { json } from '@sveltejs/kit';
2
+ import { requireBookingConfig } from '../db.js';
3
+ import { processDueReminders, processDueExpirations } from '../maintenance.js';
4
+ /** Constant-time string compare (avoids leaking the token via timing). */
5
+ function safeEqual(a, b) {
6
+ if (a.length !== b.length)
7
+ return false;
8
+ let r = 0;
9
+ for (let i = 0; i < a.length; i++)
10
+ r |= a.charCodeAt(i) ^ b.charCodeAt(i);
11
+ return r === 0;
12
+ }
13
+ /**
14
+ * Secured external trigger for booking maintenance — the swap-in for the
15
+ * in-process loop (e.g. a system cron hitting this endpoint). Requires
16
+ * `Authorization: Bearer <maintenanceToken>`. Mount at `POST /api/booking/maintenance`.
17
+ */
18
+ export function createMaintenanceHandler() {
19
+ return {
20
+ POST: async ({ request }) => {
21
+ const config = requireBookingConfig();
22
+ if (!config.maintenanceToken) {
23
+ return json({ error: 'Maintenance trigger disabled' }, { status: 404 });
24
+ }
25
+ const auth = request.headers.get('authorization') ?? '';
26
+ const token = auth.startsWith('Bearer ') ? auth.slice(7) : '';
27
+ if (!safeEqual(token, config.maintenanceToken)) {
28
+ return json({ error: 'Unauthorized' }, { status: 401 });
29
+ }
30
+ const now = new Date();
31
+ const reminders = await processDueReminders(now);
32
+ const expirations = await processDueExpirations(now);
33
+ return json({ ok: true, reminders, ...expirations });
34
+ }
35
+ };
36
+ }
@@ -0,0 +1,13 @@
1
+ import { type RequestHandler } from '@sveltejs/kit';
2
+ /**
3
+ * Self-service portal. Mount at `GET|POST /api/booking/portal/[token]`.
4
+ * Drugi czynnik (D9): token w linku = 1. czynnik; numer telefonu = 2. GET i
5
+ * wszystkie akcje poza `verify-phone` wymagają ważnego podpisanego cookie sesji
6
+ * (`?action=verify-phone` ustawia je po dopasowaniu numeru). Bez skonfigurowanego
7
+ * `portalSecret` portal jest fail-closed.
8
+ * `POST ?action=edit|pay|participant` jak dotąd; below-paid/terminal → 409.
9
+ */
10
+ export declare function createPortalHandler(): {
11
+ GET: RequestHandler;
12
+ POST: RequestHandler;
13
+ };
@@ -0,0 +1,103 @@
1
+ import { json } from '@sveltejs/kit';
2
+ import { checkRateLimit, resetRateLimit, clientKey } from '../../../server/security/rate-limit-handler.js';
3
+ import { portalView, portalEdit, portalPay, portalUpdateParticipant, verifyPhone, portalAddTrips } from '../portal.js';
4
+ import { signSession, verifySession, portalCookieName } from '../portal-session.js';
5
+ import { requireBookingConfig } from '../db.js';
6
+ import { LedgerError } from '../../ledger.js';
7
+ import { BookingEditError } from '../../recompute.js';
8
+ import { BookingValidationError } from '../validation.js';
9
+ const SESSION_TTL = 7200;
10
+ /**
11
+ * Self-service portal. Mount at `GET|POST /api/booking/portal/[token]`.
12
+ * Drugi czynnik (D9): token w linku = 1. czynnik; numer telefonu = 2. GET i
13
+ * wszystkie akcje poza `verify-phone` wymagają ważnego podpisanego cookie sesji
14
+ * (`?action=verify-phone` ustawia je po dopasowaniu numeru). Bez skonfigurowanego
15
+ * `portalSecret` portal jest fail-closed.
16
+ * `POST ?action=edit|pay|participant` jak dotąd; below-paid/terminal → 409.
17
+ */
18
+ export function createPortalHandler() {
19
+ const secret = () => requireBookingConfig().portalSecret;
20
+ const isVerified = (cookies, token) => {
21
+ const s = secret();
22
+ if (!s)
23
+ return false;
24
+ return verifySession(cookies.get(portalCookieName(token)) ?? '', token, Math.floor(Date.now() / 1000), s);
25
+ };
26
+ return {
27
+ GET: async ({ params, cookies }) => {
28
+ const token = params.token ?? '';
29
+ if (!isVerified(cookies, token)) {
30
+ return json({ error: 'Wymagana weryfikacja', code: 'PHONE_REQUIRED' }, { status: 401 });
31
+ }
32
+ const view = await portalView(token);
33
+ return view ? json(view) : json({ error: 'Not found' }, { status: 404 });
34
+ },
35
+ POST: async ({ params, request, url, cookies }) => {
36
+ const rl = checkRateLimit(clientKey(request, 'booking-portal'), { limit: 10, windowSec: 60 });
37
+ if (!rl.allowed)
38
+ return json({ error: 'Rate limit exceeded' }, { status: 429 });
39
+ const token = params.token ?? '';
40
+ const action = url.searchParams.get('action');
41
+ const body = (await request.json().catch(() => ({})));
42
+ if (action === 'verify-phone') {
43
+ const s = secret();
44
+ if (!s)
45
+ return json({ error: 'Portal not configured' }, { status: 503 });
46
+ const lock = checkRateLimit(`portal-verify:${token}`, { limit: 5, windowSec: 600 });
47
+ if (!lock.allowed)
48
+ return json({ error: 'Za dużo prób', code: 'LOCKED' }, { status: 429 });
49
+ const booking = await verifyPhone(token, String(body.phone ?? ''));
50
+ if (!booking)
51
+ return json({ error: 'Numer nie pasuje', code: 'PHONE_MISMATCH' }, { status: 401 });
52
+ resetRateLimit(`portal-verify:${token}`);
53
+ cookies.set(portalCookieName(token), signSession(token, Math.floor(Date.now() / 1000), SESSION_TTL, s), {
54
+ httpOnly: true,
55
+ sameSite: 'lax',
56
+ secure: url.protocol === 'https:',
57
+ path: '/api/booking/portal',
58
+ maxAge: SESSION_TTL
59
+ });
60
+ const view = await portalView(token);
61
+ return view ? json(view) : json({ error: 'Not found' }, { status: 404 });
62
+ }
63
+ if (!isVerified(cookies, token)) {
64
+ return json({ error: 'Wymagana weryfikacja', code: 'PHONE_REQUIRED' }, { status: 401 });
65
+ }
66
+ try {
67
+ if (action === 'pay') {
68
+ const out = await portalPay(token, Number(body.amount) || 0);
69
+ return out ? json(out) : json({ error: 'Not found' }, { status: 404 });
70
+ }
71
+ if (action === 'edit') {
72
+ const out = await portalEdit(token, body);
73
+ return out ? json(out) : json({ error: 'Not found' }, { status: 404 });
74
+ }
75
+ if (action === 'participant') {
76
+ const out = await portalUpdateParticipant(token, String(body.participantId ?? ''), (body.data ?? {}));
77
+ return out ? json(out) : json({ error: 'Not found' }, { status: 404 });
78
+ }
79
+ if (action === 'add-trips') {
80
+ // Cena NIE jest częścią żądania — ustala ją rdzeń z katalogu.
81
+ const additions = Array.isArray(body.additions)
82
+ ? body.additions.map((a) => ({
83
+ tripRef: String(a.tripRef),
84
+ assignments: Array.isArray(a.assignments) ? a.assignments.map(String) : []
85
+ }))
86
+ : [];
87
+ const out = await portalAddTrips(token, additions);
88
+ return out ? json(out) : json({ error: 'Not found' }, { status: 404 });
89
+ }
90
+ return json({ error: 'Unknown action' }, { status: 400 });
91
+ }
92
+ catch (err) {
93
+ if (err instanceof LedgerError || err instanceof BookingEditError) {
94
+ return json({ error: err.message, code: 'CONTACT_STAFF' }, { status: 409 });
95
+ }
96
+ if (err instanceof BookingValidationError) {
97
+ return json({ error: err.message, code: err.code }, { status: 400 });
98
+ }
99
+ return json({ error: err instanceof Error ? err.message : 'Edit failed' }, { status: 400 });
100
+ }
101
+ }
102
+ };
103
+ }
@@ -0,0 +1,9 @@
1
+ import { type RequestHandler } from '@sveltejs/kit';
2
+ /**
3
+ * Payment-provider webhook. Mount at `POST /api/booking/webhook/[provider]`.
4
+ * Delegates parsing to the matching adapter's `handleWebhook`, applies the event
5
+ * idempotently, then sends the right confirmation email on a settled payment.
6
+ */
7
+ export declare function createWebhookHandler(): {
8
+ POST: RequestHandler;
9
+ };
@@ -0,0 +1,39 @@
1
+ import { json } from '@sveltejs/kit';
2
+ import { getCMS } from '../../../core/cms.js';
3
+ import { requireBookingConfig } from '../db.js';
4
+ import { applyWebhookPayment } from '../payments.js';
5
+ import { sendBookingEmail } from '../email.js';
6
+ /**
7
+ * Payment-provider webhook. Mount at `POST /api/booking/webhook/[provider]`.
8
+ * Delegates parsing to the matching adapter's `handleWebhook`, applies the event
9
+ * idempotently, then sends the right confirmation email on a settled payment.
10
+ */
11
+ export function createWebhookHandler() {
12
+ return {
13
+ POST: async ({ request, params }) => {
14
+ try {
15
+ getCMS();
16
+ }
17
+ catch {
18
+ return json({ error: 'not ready' }, { status: 503 });
19
+ }
20
+ const config = requireBookingConfig();
21
+ const adapter = config.payment.find((a) => a.id === params.provider);
22
+ if (!adapter?.handleWebhook)
23
+ return json({ error: 'Unknown provider' }, { status: 404 });
24
+ let event;
25
+ try {
26
+ event = await adapter.handleWebhook(request);
27
+ }
28
+ catch (err) {
29
+ return json({ error: err instanceof Error ? err.message : 'bad webhook' }, { status: 400 });
30
+ }
31
+ const { applied, bookingId, kind } = await applyWebhookPayment(event);
32
+ if (applied && bookingId) {
33
+ const emailKind = kind === 'deposit' ? 'deposit-paid' : 'payment-received';
34
+ await sendBookingEmail(bookingId, emailKind).catch((e) => console.error('[booking] email failed', e));
35
+ }
36
+ return json({ ok: true });
37
+ }
38
+ };
39
+ }
@@ -0,0 +1,12 @@
1
+ export * from './db.js';
2
+ export * from './bookings.js';
3
+ export * from './payments.js';
4
+ export * from './adjustments.js';
5
+ export * from './refunds.js';
6
+ export * from './portal.js';
7
+ export * from './reminders.js';
8
+ export * from './maintenance.js';
9
+ export * from './validation.js';
10
+ export * from './catalog.js';
11
+ export * from './email.js';
12
+ export * from './http/index.js';
@@ -0,0 +1,14 @@
1
+ // Node-only server barrel for the booking module → `includio-cms/booking/server`.
2
+ // Keeps server services + HTTP handlers out of any client bundle.
3
+ export * from './db.js';
4
+ export * from './bookings.js';
5
+ export * from './payments.js';
6
+ export * from './adjustments.js';
7
+ export * from './refunds.js';
8
+ export * from './portal.js';
9
+ export * from './reminders.js';
10
+ export * from './maintenance.js';
11
+ export * from './validation.js';
12
+ export * from './catalog.js';
13
+ export * from './email.js';
14
+ export * from './http/index.js';
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Send all due reminders (unsent, `scheduledFor <= now`) via the email adapter,
3
+ * stamping `sentAt` so they never re-send. Returns the count sent. Idempotent.
4
+ */
5
+ export declare function processDueReminders(now?: Date): Promise<number>;
6
+ /**
7
+ * Auto-cancel expired bookings: abandoned drafts (deposit hold lapsed) and
8
+ * balance-overdue bookings (deposit forfeited + cancellation email). Idempotent.
9
+ */
10
+ export declare function processDueExpirations(now?: Date): Promise<{
11
+ abandoned: number;
12
+ forfeited: number;
13
+ }>;
14
+ /** Run one maintenance pass (reminders + expirations), guarded against overlap. */
15
+ export declare function runBookingMaintenance(now?: Date): Promise<void>;
16
+ /** Start the in-process loop (config-gated). Idempotent. Called from serverHooks.init. */
17
+ export declare function startBookingMaintenance(): void;
18
+ /**
19
+ * Conventional server-init entry run by `initCMS` via `serverHooks.initModule`
20
+ * (see the booking module in `../index.ts`). Server-only — kept out of the
21
+ * client-safe barrel so browser builds don't pull node:fs/url.
22
+ */
23
+ export declare function serverInit(): void;
24
+ export declare function stopBookingMaintenance(): void;
25
+ export declare function getBookingMaintenanceStatus(): {
26
+ running: boolean;
27
+ lastRun: Date | null;
28
+ };
@@ -0,0 +1,134 @@
1
+ import { eq, inArray, isNull } from 'drizzle-orm';
2
+ import { getCMS } from '../../core/cms.js';
3
+ import { getBookingDb } from './db.js';
4
+ import { bookingsTable, bookingRemindersTable } from '../../db-postgres/schema/booking/index.js';
5
+ import { selectDueReminders, decideExpirations } from '../expiry-logic.js';
6
+ import { sendBookingEmail } from './email.js';
7
+ /**
8
+ * Send all due reminders (unsent, `scheduledFor <= now`) via the email adapter,
9
+ * stamping `sentAt` so they never re-send. Returns the count sent. Idempotent.
10
+ */
11
+ export async function processDueReminders(now = new Date()) {
12
+ const db = getBookingDb();
13
+ const rows = await db
14
+ .select()
15
+ .from(bookingRemindersTable)
16
+ .where(isNull(bookingRemindersTable.sentAt));
17
+ const dueIds = selectDueReminders(rows.map((r) => ({ id: r.id, scheduledFor: r.scheduledFor, sentAt: r.sentAt })), now);
18
+ let sent = 0;
19
+ for (const id of dueIds) {
20
+ const row = rows.find((r) => r.id === id);
21
+ if (!row)
22
+ continue;
23
+ try {
24
+ await sendBookingEmail(row.bookingId, row.templateKey);
25
+ await db
26
+ .update(bookingRemindersTable)
27
+ .set({ sentAt: now })
28
+ .where(eq(bookingRemindersTable.id, id));
29
+ sent++;
30
+ }
31
+ catch (e) {
32
+ console.error('[booking] reminder send failed', id, e);
33
+ }
34
+ }
35
+ return sent;
36
+ }
37
+ /**
38
+ * Auto-cancel expired bookings: abandoned drafts (deposit hold lapsed) and
39
+ * balance-overdue bookings (deposit forfeited + cancellation email). Idempotent.
40
+ */
41
+ export async function processDueExpirations(now = new Date()) {
42
+ const db = getBookingDb();
43
+ const active = await db.select().from(bookingsTable).where(isNull(bookingsTable.deletedAt));
44
+ const { abandon, forfeit } = decideExpirations(active.map((b) => ({
45
+ id: b.id,
46
+ status: b.status,
47
+ holdExpiresAt: b.holdExpiresAt,
48
+ balanceDueAt: b.balanceDueAt,
49
+ totals: { balance: b.totals.balance }
50
+ })), now);
51
+ if (abandon.length > 0) {
52
+ await db
53
+ .update(bookingsTable)
54
+ .set({ status: 'cancelled', cancelReason: 'abandoned', updatedAt: now })
55
+ .where(inArray(bookingsTable.id, abandon));
56
+ }
57
+ for (const id of forfeit) {
58
+ await db
59
+ .update(bookingsTable)
60
+ .set({ status: 'cancelled', cancelReason: 'balance_overdue', updatedAt: now })
61
+ .where(eq(bookingsTable.id, id));
62
+ await sendBookingEmail(id, 'balance-overdue-cancelled').catch((e) => console.error('[booking] overdue email failed', e));
63
+ }
64
+ return { abandoned: abandon.length, forfeited: forfeit.length };
65
+ }
66
+ // --- In-process maintenance loop (no infra; secured endpoint is the swap-in) ---
67
+ const STATE_KEY = Symbol.for('includio.booking.maintenance.state');
68
+ function loopState() {
69
+ const g = globalThis;
70
+ if (!g[STATE_KEY])
71
+ g[STATE_KEY] = { timer: null, running: false, lastRun: null };
72
+ return g[STATE_KEY];
73
+ }
74
+ /** Run one maintenance pass (reminders + expirations), guarded against overlap. */
75
+ export async function runBookingMaintenance(now = new Date()) {
76
+ const st = loopState();
77
+ if (st.running)
78
+ return;
79
+ st.running = true;
80
+ try {
81
+ await processDueReminders(now);
82
+ await processDueExpirations(now);
83
+ st.lastRun = now;
84
+ }
85
+ catch (e) {
86
+ console.warn('[booking-maintenance] error', e);
87
+ }
88
+ finally {
89
+ st.running = false;
90
+ }
91
+ }
92
+ /** Start the in-process loop (config-gated). Idempotent. Called from serverHooks.init. */
93
+ export function startBookingMaintenance() {
94
+ const st = loopState();
95
+ if (st.timer)
96
+ return;
97
+ let cfg = null;
98
+ try {
99
+ cfg = getCMS().getModuleConfig('booking') ?? null;
100
+ }
101
+ catch {
102
+ return;
103
+ }
104
+ if (!cfg || cfg.maintenance.autoRun === false)
105
+ return;
106
+ const ms = cfg.maintenance.intervalMinutes * 60_000;
107
+ st.timer = setInterval(() => void runBookingMaintenance(), ms);
108
+ console.info(`[booking-maintenance] scheduled every ${cfg.maintenance.intervalMinutes}m`);
109
+ }
110
+ /**
111
+ * Conventional server-init entry run by `initCMS` via `serverHooks.initModule`
112
+ * (see the booking module in `../index.ts`). Server-only — kept out of the
113
+ * client-safe barrel so browser builds don't pull node:fs/url.
114
+ */
115
+ export function serverInit() {
116
+ startBookingMaintenance();
117
+ }
118
+ export function stopBookingMaintenance() {
119
+ const st = loopState();
120
+ if (st.timer) {
121
+ clearInterval(st.timer);
122
+ st.timer = null;
123
+ }
124
+ }
125
+ export function getBookingMaintenanceStatus() {
126
+ const st = loopState();
127
+ return { running: st.running, lastRun: st.lastRun };
128
+ }
129
+ // Clean up the timer across Vite HMR so it doesn't accumulate.
130
+ if (import.meta.hot) {
131
+ import.meta.hot.dispose(() => {
132
+ stopBookingMaintenance();
133
+ });
134
+ }
@@ -0,0 +1,26 @@
1
+ import { type FieldDef } from '../participant-fields/index.js';
2
+ /** Who is editing — gates which fields a write may touch (see {@link canFill}). */
3
+ export type EditScope = 'staff' | 'customer';
4
+ /** Thrown when a write violates field types/min/max/option. Missing required is soft. */
5
+ export declare class ParticipantValidationError extends Error {
6
+ errors: Array<{
7
+ key: string;
8
+ message: string;
9
+ }>;
10
+ constructor(errors: Array<{
11
+ key: string;
12
+ message: string;
13
+ }>);
14
+ }
15
+ /** Resolve the participant-field schema for a booking item's catalog entry. */
16
+ export declare function resolveItemSchema(tripRef: string): Promise<FieldDef[]>;
17
+ /**
18
+ * Merge a partial patch into a person's assignment, scoped to fields the
19
+ * `scope` audience may fill. Identity fields (role name/birthDate) are written
20
+ * once to the person row (shared across trips); the rest to this trip's
21
+ * assignment. Validates types/min/max/option (hard reject); missing required is
22
+ * allowed (soft status, surfaced in the UI elsewhere).
23
+ *
24
+ * @param assignmentId - `booking_item_persons.id` (the person-on-this-trip row).
25
+ */
26
+ export declare function saveParticipantData(assignmentId: string, incoming: Record<string, unknown>, scope: EditScope): Promise<void>;
@@ -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>;