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.
- package/API.md +178 -3
- package/CHANGELOG.md +54 -0
- package/DOCS.md +28 -3
- package/ROADMAP.md +18 -0
- package/dist/admin/client/booking/booking-dashboard.svelte +40 -0
- package/dist/admin/client/booking/booking-dashboard.svelte.d.ts +3 -0
- package/dist/admin/client/booking/customer-panel.svelte +102 -0
- package/dist/admin/client/booking/customer-panel.svelte.d.ts +11 -0
- package/dist/admin/client/booking/customer-view.d.ts +52 -0
- package/dist/admin/client/booking/customer-view.js +134 -0
- package/dist/admin/client/booking/participant-data-panel.svelte +330 -0
- package/dist/admin/client/booking/participant-data-panel.svelte.d.ts +22 -0
- package/dist/admin/client/booking/reservation-detail-page.svelte +637 -0
- package/dist/admin/client/booking/reservation-detail-page.svelte.d.ts +3 -0
- package/dist/admin/client/booking/reservations-page.svelte +221 -0
- package/dist/admin/client/booking/reservations-page.svelte.d.ts +3 -0
- package/dist/admin/client/collection/collection-entries.svelte +1 -1
- package/dist/admin/client/collection/entry-link.svelte +6 -4
- package/dist/admin/client/collection/entry-link.svelte.d.ts +3 -1
- package/dist/admin/client/collection/status-badge.svelte +39 -3
- package/dist/admin/client/collection/status-badge.svelte.d.ts +5 -3
- package/dist/admin/client/entry/entry.svelte +26 -9
- package/dist/admin/client/entry/hybrid/hybrid-preview.svelte +73 -10
- package/dist/admin/client/entry/hybrid/hybrid-preview.svelte.d.ts +6 -5
- package/dist/admin/client/entry/preview-views-context.svelte.d.ts +22 -0
- package/dist/admin/client/entry/preview-views-context.svelte.js +58 -0
- package/dist/admin/client/entry/preview-views.d.ts +27 -0
- package/dist/admin/client/entry/preview-views.js +33 -0
- package/dist/admin/client/index.d.ts +3 -0
- package/dist/admin/client/index.js +3 -0
- package/dist/admin/client/modules/dashboard-registry.d.ts +7 -0
- package/dist/admin/client/modules/dashboard-registry.js +11 -0
- package/dist/admin/client/modules/module-dashboard-page.svelte +26 -0
- package/dist/admin/client/modules/module-dashboard-page.svelte.d.ts +6 -0
- package/dist/admin/client/modules/stat-card.svelte +22 -0
- package/dist/admin/client/modules/stat-card.svelte.d.ts +10 -0
- package/dist/admin/client/shop/shop-dashboard.svelte +29 -0
- package/dist/admin/client/shop/shop-dashboard.svelte.d.ts +3 -0
- package/dist/admin/components/fields/relation-field.svelte +11 -2
- package/dist/admin/components/icons/icon-map.js +8 -0
- package/dist/admin/components/layout/app-sidebar.svelte +45 -14
- package/dist/admin/components/layout/context-switcher.svelte +90 -0
- package/dist/admin/components/layout/context-switcher.svelte.d.ts +3 -0
- package/dist/admin/components/layout/lang.d.ts +4 -0
- package/dist/admin/components/layout/lang.js +8 -0
- package/dist/admin/components/layout/layout-renderer.svelte +2 -1
- package/dist/admin/components/layout/layout-tabs.svelte +14 -0
- package/dist/admin/components/layout/layout-tabs.svelte.d.ts +2 -0
- package/dist/admin/components/layout/nav-collections.svelte +16 -9
- package/dist/admin/components/layout/nav-collections.svelte.d.ts +5 -1
- package/dist/admin/components/layout/nav-global.svelte +45 -0
- package/dist/admin/components/layout/nav-global.svelte.d.ts +3 -0
- package/dist/admin/components/layout/nav-main.svelte +8 -32
- package/dist/admin/components/layout/nav-main.svelte.d.ts +6 -1
- package/dist/admin/components/layout/nav-module.svelte +57 -0
- package/dist/admin/components/layout/nav-module.svelte.d.ts +7 -0
- package/dist/admin/components/layout/nav-section.svelte +1 -1
- package/dist/admin/components/layout/nav-section.svelte.d.ts +1 -1
- package/dist/admin/components/layout/nav-singletons.svelte +14 -7
- package/dist/admin/components/layout/nav-singletons.svelte.d.ts +5 -1
- package/dist/admin/remote/booking.remote.d.ts +244 -0
- package/dist/admin/remote/booking.remote.js +283 -0
- package/dist/admin/remote/index.d.ts +2 -0
- package/dist/admin/remote/index.js +2 -0
- package/dist/admin/remote/modules.remote.d.ts +10 -0
- package/dist/admin/remote/modules.remote.js +14 -0
- package/dist/admin/state/active-context-resolve.d.ts +37 -0
- package/dist/admin/state/active-context-resolve.js +66 -0
- package/dist/admin/state/active-context.svelte.d.ts +8 -0
- package/dist/admin/state/active-context.svelte.js +27 -0
- package/dist/booking/adjustments.d.ts +33 -0
- package/dist/booking/adjustments.js +26 -0
- package/dist/booking/capacity.d.ts +12 -0
- package/dist/booking/capacity.js +10 -0
- package/dist/booking/client/index.d.ts +39 -0
- package/dist/booking/client/index.js +44 -0
- package/dist/booking/client/use-booking.svelte.d.ts +25 -0
- package/dist/booking/client/use-booking.svelte.js +56 -0
- package/dist/booking/config.d.ts +156 -0
- package/dist/booking/config.js +36 -0
- package/dist/booking/draft.d.ts +77 -0
- package/dist/booking/draft.js +67 -0
- package/dist/booking/expiry-logic.d.ts +30 -0
- package/dist/booking/expiry-logic.js +33 -0
- package/dist/booking/index.d.ts +18 -0
- package/dist/booking/index.js +52 -0
- package/dist/booking/ledger.d.ts +46 -0
- package/dist/booking/ledger.js +60 -0
- package/dist/booking/number.d.ts +2 -0
- package/dist/booking/number.js +11 -0
- package/dist/booking/participant-fields/access.d.ts +10 -0
- package/dist/booking/participant-fields/access.js +14 -0
- package/dist/booking/participant-fields/age.d.ts +12 -0
- package/dist/booking/participant-fields/age.js +28 -0
- package/dist/booking/participant-fields/completeness.d.ts +8 -0
- package/dist/booking/participant-fields/completeness.js +13 -0
- package/dist/booking/participant-fields/group.d.ts +6 -0
- package/dist/booking/participant-fields/group.js +15 -0
- package/dist/booking/participant-fields/index.d.ts +6 -0
- package/dist/booking/participant-fields/index.js +6 -0
- package/dist/booking/participant-fields/types.d.ts +50 -0
- package/dist/booking/participant-fields/types.js +5 -0
- package/dist/booking/participant-fields/validate.d.ts +7 -0
- package/dist/booking/participant-fields/validate.js +44 -0
- package/dist/booking/participant-pricing.d.ts +93 -0
- package/dist/booking/participant-pricing.js +91 -0
- package/dist/booking/participants-view.d.ts +32 -0
- package/dist/booking/participants-view.js +22 -0
- package/dist/booking/payment-context.d.ts +19 -0
- package/dist/booking/payment-context.js +29 -0
- package/dist/booking/pricing-inputs.d.ts +28 -0
- package/dist/booking/pricing-inputs.js +35 -0
- package/dist/booking/pricing.d.ts +86 -0
- package/dist/booking/pricing.js +125 -0
- package/dist/booking/recompute.d.ts +22 -0
- package/dist/booking/recompute.js +31 -0
- package/dist/booking/reminders-schedule.d.ts +11 -0
- package/dist/booking/reminders-schedule.js +28 -0
- package/dist/booking/server/adjustments.d.ts +27 -0
- package/dist/booking/server/adjustments.js +136 -0
- package/dist/booking/server/bookings.d.ts +45 -0
- package/dist/booking/server/bookings.js +288 -0
- package/dist/booking/server/catalog.d.ts +9 -0
- package/dist/booking/server/catalog.js +20 -0
- package/dist/booking/server/db.d.ts +7 -0
- package/dist/booking/server/db.js +18 -0
- package/dist/booking/server/email.d.ts +23 -0
- package/dist/booking/server/email.js +59 -0
- package/dist/booking/server/emailTemplateRegistry.d.ts +29 -0
- package/dist/booking/server/emailTemplateRegistry.js +125 -0
- package/dist/booking/server/http/create-handler.d.ts +5 -0
- package/dist/booking/server/http/create-handler.js +68 -0
- package/dist/booking/server/http/index.d.ts +4 -0
- package/dist/booking/server/http/index.js +4 -0
- package/dist/booking/server/http/maintenance-handler.d.ts +9 -0
- package/dist/booking/server/http/maintenance-handler.js +36 -0
- package/dist/booking/server/http/portal-handler.d.ts +13 -0
- package/dist/booking/server/http/portal-handler.js +99 -0
- package/dist/booking/server/http/webhook-handler.d.ts +9 -0
- package/dist/booking/server/http/webhook-handler.js +39 -0
- package/dist/booking/server/index.d.ts +12 -0
- package/dist/booking/server/index.js +14 -0
- package/dist/booking/server/maintenance.d.ts +28 -0
- package/dist/booking/server/maintenance.js +134 -0
- package/dist/booking/server/participants.d.ts +26 -0
- package/dist/booking/server/participants.js +92 -0
- package/dist/booking/server/payments.d.ts +34 -0
- package/dist/booking/server/payments.js +207 -0
- package/dist/booking/server/portal-session.d.ts +16 -0
- package/dist/booking/server/portal-session.js +42 -0
- package/dist/booking/server/portal.d.ts +207 -0
- package/dist/booking/server/portal.js +234 -0
- package/dist/booking/server/refunds.d.ts +7 -0
- package/dist/booking/server/refunds.js +58 -0
- package/dist/booking/server/reminders.d.ts +9 -0
- package/dist/booking/server/reminders.js +24 -0
- package/dist/booking/server/validation.d.ts +17 -0
- package/dist/booking/server/validation.js +55 -0
- package/dist/booking/state-machine.d.ts +41 -0
- package/dist/booking/state-machine.js +73 -0
- package/dist/booking/templates/_partials/footer.en.html +4 -0
- package/dist/booking/templates/_partials/footer.pl.html +4 -0
- package/dist/booking/templates/_partials/header.en.html +4 -0
- package/dist/booking/templates/_partials/header.pl.html +4 -0
- package/dist/booking/templates/balance-due.en.html +10 -0
- package/dist/booking/templates/balance-due.pl.html +10 -0
- package/dist/booking/templates/balance-overdue-cancelled.en.html +5 -0
- package/dist/booking/templates/balance-overdue-cancelled.pl.html +5 -0
- package/dist/booking/templates/booking-received.en.html +9 -0
- package/dist/booking/templates/booking-received.pl.html +9 -0
- package/dist/booking/templates/deposit-paid.en.html +6 -0
- package/dist/booking/templates/deposit-paid.pl.html +6 -0
- package/dist/booking/templates/payment-link.en.html +5 -0
- package/dist/booking/templates/payment-link.pl.html +5 -0
- package/dist/booking/templates/payment-received.en.html +10 -0
- package/dist/booking/templates/payment-received.pl.html +10 -0
- package/dist/booking/totals.d.ts +38 -0
- package/dist/booking/totals.js +61 -0
- package/dist/booking/webhook-logic.d.ts +28 -0
- package/dist/booking/webhook-logic.js +50 -0
- package/dist/cli/create-user.d.ts +7 -1
- package/dist/cli/create-user.js +23 -15
- package/dist/cli/index.js +55 -7
- package/dist/cli/scaffold/admin.d.ts +6 -0
- package/dist/cli/scaffold/admin.js +5 -3
- package/dist/cli/scaffold/booking.d.ts +12 -0
- package/dist/cli/scaffold/booking.js +103 -0
- package/dist/core/cms.d.ts +24 -9
- package/dist/core/cms.js +72 -34
- package/dist/core/config/validatePreviews.d.ts +12 -0
- package/dist/core/config/validatePreviews.js +90 -0
- package/dist/core/fields/slugPath.d.ts +9 -1
- package/dist/core/fields/slugPath.js +16 -2
- package/dist/core/server/fields/resolveRelationFields.d.ts +11 -2
- package/dist/core/server/fields/resolveRelationFields.js +12 -3
- package/dist/core/server/generator/generator.js +7 -2
- package/dist/db-postgres/schema/booking/bookingAdjustments.d.ts +186 -0
- package/dist/db-postgres/schema/booking/bookingAdjustments.js +22 -0
- package/dist/db-postgres/schema/booking/bookingItemPersons.d.ts +117 -0
- package/dist/db-postgres/schema/booking/bookingItemPersons.js +22 -0
- package/dist/db-postgres/schema/booking/bookingItems.d.ts +181 -0
- package/dist/db-postgres/schema/booking/bookingItems.js +22 -0
- package/dist/db-postgres/schema/booking/bookingPayments.d.ts +204 -0
- package/dist/db-postgres/schema/booking/bookingPayments.js +25 -0
- package/dist/db-postgres/schema/booking/bookingPersons.d.ts +99 -0
- package/dist/db-postgres/schema/booking/bookingPersons.js +17 -0
- package/dist/db-postgres/schema/booking/bookingReminders.d.ts +131 -0
- package/dist/db-postgres/schema/booking/bookingReminders.js +18 -0
- package/dist/db-postgres/schema/booking/bookings.d.ts +307 -0
- package/dist/db-postgres/schema/booking/bookings.js +40 -0
- package/dist/db-postgres/schema/booking/index.d.ts +7 -0
- package/dist/db-postgres/schema/booking/index.js +7 -0
- package/dist/db-postgres/schema/index.d.ts +1 -0
- package/dist/db-postgres/schema/index.js +1 -0
- package/dist/db-postgres/schema-booking.d.ts +1 -0
- package/dist/db-postgres/schema-booking.js +1 -0
- package/dist/server/security/rate-limit-handler.d.ts +19 -0
- package/dist/server/security/rate-limit-handler.js +34 -0
- package/dist/shop/adapters/payu/client.js +3 -1
- package/dist/shop/adapters/payu/index.js +37 -17
- package/dist/shop/adapters/payu/payload.d.ts +6 -0
- package/dist/shop/adapters/payu/payload.js +1 -1
- package/dist/shop/adapters/stripe/index.js +36 -21
- package/dist/shop/index.d.ts +11 -2
- package/dist/shop/index.js +43 -1
- package/dist/shop/rate-limit.d.ts +1 -9
- package/dist/shop/rate-limit.js +3 -28
- package/dist/shop/types.d.ts +19 -0
- package/dist/sveltekit/config.d.ts +4 -0
- package/dist/sveltekit/config.js +6 -0
- package/dist/types/cms.d.ts +15 -1
- package/dist/types/cms.schema.d.ts +96 -2
- package/dist/types/cms.schema.js +84 -1
- package/dist/types/config.d.ts +33 -0
- package/dist/types/layout.d.ts +6 -0
- package/dist/types/modules.d.ts +133 -0
- package/dist/types/modules.js +27 -0
- package/dist/updates/0.37.0/index.d.ts +2 -0
- package/dist/updates/0.37.0/index.js +52 -0
- package/dist/updates/index.js +3 -1
- package/package.json +19 -1
- package/dist/admin/components/layout/nav-shop.svelte +0 -33
- package/dist/admin/components/layout/nav-shop.svelte.d.ts +0 -3
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether a given audience may fill/edit a field. Default `whoFills` is `'both'`.
|
|
3
|
+
*
|
|
4
|
+
* Staff (admin) is **privileged** — it may edit every field regardless of
|
|
5
|
+
* `whoFills` (operators must always be able to correct/complete any datum).
|
|
6
|
+
* `whoFills` therefore only gates the **customer/portal** side: a `'staff'`
|
|
7
|
+
* field is simply hidden from the portal, not locked away from the operator.
|
|
8
|
+
*/
|
|
9
|
+
export function canFill(field, audience) {
|
|
10
|
+
if (audience === 'staff')
|
|
11
|
+
return true;
|
|
12
|
+
const who = field.whoFills ?? 'both';
|
|
13
|
+
return who === 'both' || who === 'customer';
|
|
14
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { FieldDef, AgeFlagConfig } from './types.js';
|
|
2
|
+
/** Pełne lata między `dob` a `anchor`. `null` gdy data niepoprawna. */
|
|
3
|
+
export declare function ageAt(dob: string, anchor: Date): number | null;
|
|
4
|
+
/**
|
|
5
|
+
* Generyczna flaga wieku — bez magicznego klucza. Bierze pole z rolą `birthDate`,
|
|
6
|
+
* liczy wiek na `anchorDate` (data wylotu) i flaguje gdy < `config.below`.
|
|
7
|
+
* Brak configu / kotwicy / pola-roli / wartości ⇒ `{ flagged: false, age: null }`.
|
|
8
|
+
*/
|
|
9
|
+
export declare function deriveAgeFlag(data: Record<string, unknown>, fields: FieldDef[], config: AgeFlagConfig | null, anchorDate: Date | null): {
|
|
10
|
+
flagged: boolean;
|
|
11
|
+
age: number | null;
|
|
12
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** Pełne lata między `dob` a `anchor`. `null` gdy data niepoprawna. */
|
|
2
|
+
export function ageAt(dob, anchor) {
|
|
3
|
+
if (typeof dob !== 'string' || Number.isNaN(Date.parse(dob)))
|
|
4
|
+
return null;
|
|
5
|
+
const d = new Date(dob);
|
|
6
|
+
let age = anchor.getUTCFullYear() - d.getUTCFullYear();
|
|
7
|
+
const m = anchor.getUTCMonth() - d.getUTCMonth();
|
|
8
|
+
if (m < 0 || (m === 0 && anchor.getUTCDate() < d.getUTCDate()))
|
|
9
|
+
age--;
|
|
10
|
+
return age;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Generyczna flaga wieku — bez magicznego klucza. Bierze pole z rolą `birthDate`,
|
|
14
|
+
* liczy wiek na `anchorDate` (data wylotu) i flaguje gdy < `config.below`.
|
|
15
|
+
* Brak configu / kotwicy / pola-roli / wartości ⇒ `{ flagged: false, age: null }`.
|
|
16
|
+
*/
|
|
17
|
+
export function deriveAgeFlag(data, fields, config, anchorDate) {
|
|
18
|
+
if (!config || !anchorDate)
|
|
19
|
+
return { flagged: false, age: null };
|
|
20
|
+
const dobField = fields.find((f) => f.role === 'birthDate');
|
|
21
|
+
if (!dobField)
|
|
22
|
+
return { flagged: false, age: null };
|
|
23
|
+
const raw = data[dobField.key];
|
|
24
|
+
if (typeof raw !== 'string')
|
|
25
|
+
return { flagged: false, age: null };
|
|
26
|
+
const age = ageAt(raw, anchorDate);
|
|
27
|
+
return { flagged: age !== null && age < config.below, age };
|
|
28
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
function isEmpty(v) {
|
|
2
|
+
return v === undefined || v === null || v === '';
|
|
3
|
+
}
|
|
4
|
+
export function completeness(data, fields) {
|
|
5
|
+
const required = fields.filter((f) => f.required);
|
|
6
|
+
const missing = required.filter((f) => isEmpty(data[f.key])).map((f) => f.label);
|
|
7
|
+
return {
|
|
8
|
+
filled: required.length - missing.length,
|
|
9
|
+
total: required.length,
|
|
10
|
+
missing,
|
|
11
|
+
complete: missing.length === 0
|
|
12
|
+
};
|
|
13
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export function groupBySection(fields) {
|
|
2
|
+
const groups = [];
|
|
3
|
+
const index = new Map();
|
|
4
|
+
for (const f of fields) {
|
|
5
|
+
const key = f.section ?? null;
|
|
6
|
+
let g = index.get(key);
|
|
7
|
+
if (!g) {
|
|
8
|
+
g = { section: key, fields: [] };
|
|
9
|
+
index.set(key, g);
|
|
10
|
+
groups.push(g);
|
|
11
|
+
}
|
|
12
|
+
g.fields.push(f);
|
|
13
|
+
}
|
|
14
|
+
return groups;
|
|
15
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/** Paleta typów pól v1. Rozszerzalna przez dopisanie typu + renderera/walidatora. */
|
|
2
|
+
export declare const FIELD_TYPES: readonly ["text", "number", "boolean", "select", "date"];
|
|
3
|
+
export type FieldType = (typeof FIELD_TYPES)[number];
|
|
4
|
+
export declare function isFieldType(v: unknown): v is FieldType;
|
|
5
|
+
/** Kto wypełnia pole. Default 'both'. */
|
|
6
|
+
export type WhoFills = 'customer' | 'staff' | 'both';
|
|
7
|
+
/**
|
|
8
|
+
* Generyczna „rola" pola — opcjonalna wskazówka semantyczna, którą UI/logika
|
|
9
|
+
* mogą wykorzystać bez magicznych kluczy:
|
|
10
|
+
* - `name` — wartość użyta jako tytuł karty uczestnika,
|
|
11
|
+
* - `birthDate` — pole daty będące podstawą flagi wieku (patrz {@link AgeFlagConfig}).
|
|
12
|
+
*/
|
|
13
|
+
export type FieldRole = 'name' | 'birthDate';
|
|
14
|
+
/** Serializowalna definicja pojedynczego pola uczestnika. */
|
|
15
|
+
export interface FieldDef {
|
|
16
|
+
/** Stabilny klucz w `participant.data`. */
|
|
17
|
+
key: string;
|
|
18
|
+
label: string;
|
|
19
|
+
type: FieldType;
|
|
20
|
+
required?: boolean;
|
|
21
|
+
/** Dla `select`. */
|
|
22
|
+
options?: string[];
|
|
23
|
+
/** Grupowanie w UI (np. "Podstawowe", "Ankieta"). */
|
|
24
|
+
section?: string;
|
|
25
|
+
/** Kto wypełnia (default 'both'). */
|
|
26
|
+
whoFills?: WhoFills;
|
|
27
|
+
/** Opcjonalna rola semantyczna (tytuł karty / data do flagi wieku). */
|
|
28
|
+
role?: FieldRole;
|
|
29
|
+
helpText?: string;
|
|
30
|
+
/** v1 stałe 'participant'; szew pod 'reservation' później. */
|
|
31
|
+
scope?: 'participant';
|
|
32
|
+
/** Dla `number`. */
|
|
33
|
+
min?: number;
|
|
34
|
+
max?: number;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Generyczna flaga wieku: oznacza uczestnika, którego wiek (liczony z pola roli
|
|
38
|
+
* `birthDate` na datę-kotwicę) jest **poniżej** progu `below`. Domeno-agnostyczna —
|
|
39
|
+
* klient nadaje znaczenie przez `label` ("Dziecko", "Junior", …). Brak configu =
|
|
40
|
+
* brak flagi. Próg wieku liczony na datę wylotu pozycji.
|
|
41
|
+
*/
|
|
42
|
+
export interface AgeFlagConfig {
|
|
43
|
+
/** Flaguj, gdy wiek < `below`. */
|
|
44
|
+
below: number;
|
|
45
|
+
/** Etykieta badge'a (domyślnie „Dziecko"/"Child"). */
|
|
46
|
+
label?: {
|
|
47
|
+
pl: string;
|
|
48
|
+
en: string;
|
|
49
|
+
};
|
|
50
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { FieldDef } from './types.js';
|
|
2
|
+
export interface FieldError {
|
|
3
|
+
key: string;
|
|
4
|
+
code: 'required' | 'type' | 'min' | 'max' | 'option';
|
|
5
|
+
message: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function validateAgainstSchema(data: Record<string, unknown>, fields: FieldDef[]): FieldError[];
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
function isEmpty(v) {
|
|
2
|
+
return v === undefined || v === null || v === '';
|
|
3
|
+
}
|
|
4
|
+
export function validateAgainstSchema(data, fields) {
|
|
5
|
+
const errors = [];
|
|
6
|
+
for (const f of fields) {
|
|
7
|
+
const v = data[f.key];
|
|
8
|
+
if (isEmpty(v)) {
|
|
9
|
+
if (f.required)
|
|
10
|
+
errors.push({ key: f.key, code: 'required', message: `${f.label} jest wymagane` });
|
|
11
|
+
continue; // puste opcjonalne = OK, nie typujemy
|
|
12
|
+
}
|
|
13
|
+
switch (f.type) {
|
|
14
|
+
case 'text':
|
|
15
|
+
if (typeof v !== 'string')
|
|
16
|
+
errors.push({ key: f.key, code: 'type', message: `${f.label}: oczekiwano tekstu` });
|
|
17
|
+
break;
|
|
18
|
+
case 'boolean':
|
|
19
|
+
if (typeof v !== 'boolean')
|
|
20
|
+
errors.push({ key: f.key, code: 'type', message: `${f.label}: oczekiwano tak/nie` });
|
|
21
|
+
break;
|
|
22
|
+
case 'date':
|
|
23
|
+
if (typeof v !== 'string' || Number.isNaN(Date.parse(v)))
|
|
24
|
+
errors.push({ key: f.key, code: 'type', message: `${f.label}: niepoprawna data` });
|
|
25
|
+
break;
|
|
26
|
+
case 'select':
|
|
27
|
+
if (!(f.options ?? []).includes(String(v)))
|
|
28
|
+
errors.push({ key: f.key, code: 'option', message: `${f.label}: niedozwolona wartość` });
|
|
29
|
+
break;
|
|
30
|
+
case 'number': {
|
|
31
|
+
if (typeof v !== 'number' || Number.isNaN(v)) {
|
|
32
|
+
errors.push({ key: f.key, code: 'type', message: `${f.label}: oczekiwano liczby` });
|
|
33
|
+
break;
|
|
34
|
+
}
|
|
35
|
+
if (f.min !== undefined && v < f.min)
|
|
36
|
+
errors.push({ key: f.key, code: 'min', message: `${f.label}: min ${f.min}` });
|
|
37
|
+
if (f.max !== undefined && v > f.max)
|
|
38
|
+
errors.push({ key: f.key, code: 'max', message: `${f.label}: max ${f.max}` });
|
|
39
|
+
break;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return errors;
|
|
44
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-participant booking pricing — pure, deterministic, module-agnostic.
|
|
3
|
+
*
|
|
4
|
+
* Unlike the per-line {@link price} engine, this prices each *participant ×
|
|
5
|
+
* trip* unit, so age-based pricing and volume discounts compose correctly:
|
|
6
|
+
*
|
|
7
|
+
* line base = round(adultPrice * ageBandPct / 100)
|
|
8
|
+
* package = participant's cheapest trip * ladder% (full-price only)
|
|
9
|
+
* early-bird = participant's gross * earlyBird% (full-price only)
|
|
10
|
+
*
|
|
11
|
+
* INVARIANT: volume discounts (package + early-bird) apply ONLY to full-price
|
|
12
|
+
* participants (age band pct === 100). Any participant with an age discount is
|
|
13
|
+
* excluded — "a child already on −30% does not also stack the bundle".
|
|
14
|
+
*
|
|
15
|
+
* Amounts are in **minor units** (grosze). No clock access: `now` and `anchor`
|
|
16
|
+
* (the arrival date) are injected by the caller.
|
|
17
|
+
*/
|
|
18
|
+
import type { Perk } from './pricing.js';
|
|
19
|
+
/** A global age band: participants aged [minAge, maxAge] pay `pct`% of adult price. */
|
|
20
|
+
export interface AgeBand {
|
|
21
|
+
label?: string;
|
|
22
|
+
/** Inclusive lower bound (whole years). */
|
|
23
|
+
minAge: number;
|
|
24
|
+
/** Inclusive upper bound (whole years); `null` ⇒ no upper bound. */
|
|
25
|
+
maxAge: number | null;
|
|
26
|
+
/** Percent of the adult price, 0–100. */
|
|
27
|
+
pct: number;
|
|
28
|
+
}
|
|
29
|
+
/** Resolve the age-band percent for an age; 100 when null/unmatched (full price). */
|
|
30
|
+
export declare function bandPctFor(age: number | null, bands: AgeBand[]): number;
|
|
31
|
+
/** One ladder tier: from `minTrips` trips, the cheapest trip gets `percent`% off. */
|
|
32
|
+
export interface LadderTier {
|
|
33
|
+
minTrips: number;
|
|
34
|
+
percent: number;
|
|
35
|
+
perk?: string;
|
|
36
|
+
}
|
|
37
|
+
/** Highest ladder tier whose `minTrips` ≤ tripCount; null when none qualifies. */
|
|
38
|
+
export declare function ladderTierFor(tripCount: number, ladder: LadderTier[]): LadderTier | null;
|
|
39
|
+
/** Early-bird: `percent`% off a full-price participant's gross when booked early. */
|
|
40
|
+
export interface EarlyBird {
|
|
41
|
+
daysBefore: number;
|
|
42
|
+
percent: number;
|
|
43
|
+
}
|
|
44
|
+
/** Serializable per-participant pricing config (sourced from client globalSettings). */
|
|
45
|
+
export interface ParticipantPricingConfig {
|
|
46
|
+
ageBands: AgeBand[];
|
|
47
|
+
ladder: LadderTier[];
|
|
48
|
+
earlyBird?: EarlyBird | null;
|
|
49
|
+
}
|
|
50
|
+
/** A trip available in the booking, with its full (adult) price (minor units). */
|
|
51
|
+
export interface PricedTrip {
|
|
52
|
+
ref: string;
|
|
53
|
+
adultPrice: number;
|
|
54
|
+
}
|
|
55
|
+
/** A participant and the trips they take. */
|
|
56
|
+
export interface PricingParticipant {
|
|
57
|
+
id: string;
|
|
58
|
+
/** ISO date of birth; `null`/invalid ⇒ treated as a full-price adult. */
|
|
59
|
+
dob: string | null;
|
|
60
|
+
tripRefs: string[];
|
|
61
|
+
}
|
|
62
|
+
export interface ParticipantLine {
|
|
63
|
+
tripRef: string;
|
|
64
|
+
/** Age-adjusted base for this participant on this trip (minor units). */
|
|
65
|
+
base: number;
|
|
66
|
+
}
|
|
67
|
+
export interface ParticipantBreakdown {
|
|
68
|
+
participantId: string;
|
|
69
|
+
/** Resolved age-band percent (100 when no band matches / no dob). */
|
|
70
|
+
ageBandPct: number;
|
|
71
|
+
/** True ⇔ ageBandPct === 100 (eligible for volume discounts). */
|
|
72
|
+
fullPrice: boolean;
|
|
73
|
+
lines: ParticipantLine[];
|
|
74
|
+
/** Sum of line bases. */
|
|
75
|
+
gross: number;
|
|
76
|
+
/** Package (ladder) discount, minor units (0 when ineligible). */
|
|
77
|
+
packageDiscount: number;
|
|
78
|
+
/** Trip ref the package discount targets, or null. */
|
|
79
|
+
packageTargetRef: string | null;
|
|
80
|
+
/** Early-bird discount, minor units (0 when ineligible). */
|
|
81
|
+
earlyBirdDiscount: number;
|
|
82
|
+
perks: string[];
|
|
83
|
+
/** gross − packageDiscount − earlyBirdDiscount. */
|
|
84
|
+
net: number;
|
|
85
|
+
}
|
|
86
|
+
export interface ParticipantPriceResult {
|
|
87
|
+
gross: number;
|
|
88
|
+
discountTotal: number;
|
|
89
|
+
net: number;
|
|
90
|
+
participants: ParticipantBreakdown[];
|
|
91
|
+
perks: Perk[];
|
|
92
|
+
}
|
|
93
|
+
export declare function priceByParticipant(participants: PricingParticipant[], trips: PricedTrip[], anchor: Date | null, now: Date, config: ParticipantPricingConfig): ParticipantPriceResult;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { daysUntil } from './pricing.js';
|
|
2
|
+
import { ageAt } from './participant-fields/age.js';
|
|
3
|
+
/** Resolve the age-band percent for an age; 100 when null/unmatched (full price). */
|
|
4
|
+
export function bandPctFor(age, bands) {
|
|
5
|
+
if (age === null)
|
|
6
|
+
return 100;
|
|
7
|
+
for (const b of bands) {
|
|
8
|
+
const lo = age >= b.minAge;
|
|
9
|
+
const hi = b.maxAge === null || age <= b.maxAge;
|
|
10
|
+
if (lo && hi)
|
|
11
|
+
return b.pct;
|
|
12
|
+
}
|
|
13
|
+
return 100;
|
|
14
|
+
}
|
|
15
|
+
/** Highest ladder tier whose `minTrips` ≤ tripCount; null when none qualifies. */
|
|
16
|
+
export function ladderTierFor(tripCount, ladder) {
|
|
17
|
+
let best = null;
|
|
18
|
+
for (const t of ladder) {
|
|
19
|
+
if (tripCount >= t.minTrips && (!best || t.minTrips > best.minTrips))
|
|
20
|
+
best = t;
|
|
21
|
+
}
|
|
22
|
+
return best;
|
|
23
|
+
}
|
|
24
|
+
export function priceByParticipant(participants, trips, anchor, now, config) {
|
|
25
|
+
const priceOf = new Map(trips.map((t) => [t.ref, t.adultPrice]));
|
|
26
|
+
const daysBefore = anchor ? daysUntil(anchor, now) : null;
|
|
27
|
+
const breakdowns = participants.map((p) => {
|
|
28
|
+
const age = p.dob && anchor ? ageAt(p.dob, anchor) : null;
|
|
29
|
+
const pct = bandPctFor(age, config.ageBands);
|
|
30
|
+
const fullPrice = pct === 100;
|
|
31
|
+
const lines = p.tripRefs.map((ref) => ({
|
|
32
|
+
tripRef: ref,
|
|
33
|
+
base: Math.round(((priceOf.get(ref) ?? 0) * pct) / 100)
|
|
34
|
+
}));
|
|
35
|
+
const gross = lines.reduce((s, l) => s + l.base, 0);
|
|
36
|
+
let packageDiscount = 0;
|
|
37
|
+
let packageTargetRef = null;
|
|
38
|
+
const perks = [];
|
|
39
|
+
if (fullPrice) {
|
|
40
|
+
const tier = ladderTierFor(p.tripRefs.length, config.ladder);
|
|
41
|
+
if (tier) {
|
|
42
|
+
const cheapest = lines.reduce((min, l) => (!min || l.base < min.base ? l : min), null);
|
|
43
|
+
if (cheapest) {
|
|
44
|
+
packageDiscount = Math.round((cheapest.base * tier.percent) / 100);
|
|
45
|
+
packageTargetRef = cheapest.tripRef;
|
|
46
|
+
}
|
|
47
|
+
if (tier.perk)
|
|
48
|
+
perks.push(tier.perk);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
let earlyBirdDiscount = 0;
|
|
52
|
+
if (fullPrice &&
|
|
53
|
+
config.earlyBird &&
|
|
54
|
+
daysBefore !== null &&
|
|
55
|
+
daysBefore >= config.earlyBird.daysBefore) {
|
|
56
|
+
earlyBirdDiscount = Math.round((gross * config.earlyBird.percent) / 100);
|
|
57
|
+
}
|
|
58
|
+
const net = Math.max(0, gross - packageDiscount - earlyBirdDiscount);
|
|
59
|
+
return {
|
|
60
|
+
participantId: p.id,
|
|
61
|
+
ageBandPct: pct,
|
|
62
|
+
fullPrice,
|
|
63
|
+
lines,
|
|
64
|
+
gross,
|
|
65
|
+
packageDiscount,
|
|
66
|
+
packageTargetRef,
|
|
67
|
+
earlyBirdDiscount,
|
|
68
|
+
perks,
|
|
69
|
+
net
|
|
70
|
+
};
|
|
71
|
+
});
|
|
72
|
+
const gross = breakdowns.reduce((s, b) => s + b.gross, 0);
|
|
73
|
+
const discountTotal = breakdowns.reduce((s, b) => s + b.packageDiscount + b.earlyBirdDiscount, 0);
|
|
74
|
+
const seen = new Set();
|
|
75
|
+
const perks = [];
|
|
76
|
+
for (const b of breakdowns) {
|
|
77
|
+
for (const label of b.perks) {
|
|
78
|
+
if (seen.has(label))
|
|
79
|
+
continue;
|
|
80
|
+
seen.add(label);
|
|
81
|
+
perks.push({ label });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
gross,
|
|
86
|
+
discountTotal: Math.min(gross, discountTotal),
|
|
87
|
+
net: Math.max(0, gross - discountTotal),
|
|
88
|
+
participants: breakdowns,
|
|
89
|
+
perks
|
|
90
|
+
};
|
|
91
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure read adapter: fold person + assignment rows into the flat
|
|
3
|
+
* "participant per trip line" shape the admin/portal UI was written against
|
|
4
|
+
* (one row per person×item, with identity + per-trip answers merged into
|
|
5
|
+
* `data`). Keeps the read DTOs backward-compatible while the storage moved to
|
|
6
|
+
* the person-centric model. UI modernization (split identity vs per-trip) is a
|
|
7
|
+
* later plan.
|
|
8
|
+
*/
|
|
9
|
+
/** Person row (subset). `data` holds identity answers (name, dob, …). */
|
|
10
|
+
export interface PersonRecord {
|
|
11
|
+
id: string;
|
|
12
|
+
data: Record<string, unknown>;
|
|
13
|
+
}
|
|
14
|
+
/** Assignment (junction) row (subset). `data` holds per-trip answers. */
|
|
15
|
+
export interface AssignmentRecord {
|
|
16
|
+
id: string;
|
|
17
|
+
itemId: string;
|
|
18
|
+
personId: string;
|
|
19
|
+
data: Record<string, unknown>;
|
|
20
|
+
createdAt: Date;
|
|
21
|
+
updatedAt: Date;
|
|
22
|
+
}
|
|
23
|
+
/** Flat participant: one per assignment, identity + per-trip `data` merged. */
|
|
24
|
+
export interface FlatParticipant {
|
|
25
|
+
id: string;
|
|
26
|
+
itemId: string;
|
|
27
|
+
personId: string;
|
|
28
|
+
data: Record<string, unknown>;
|
|
29
|
+
createdAt: Date;
|
|
30
|
+
updatedAt: Date;
|
|
31
|
+
}
|
|
32
|
+
export declare function flattenAssignments(persons: PersonRecord[], assignments: AssignmentRecord[]): FlatParticipant[];
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure read adapter: fold person + assignment rows into the flat
|
|
3
|
+
* "participant per trip line" shape the admin/portal UI was written against
|
|
4
|
+
* (one row per person×item, with identity + per-trip answers merged into
|
|
5
|
+
* `data`). Keeps the read DTOs backward-compatible while the storage moved to
|
|
6
|
+
* the person-centric model. UI modernization (split identity vs per-trip) is a
|
|
7
|
+
* later plan.
|
|
8
|
+
*/
|
|
9
|
+
export function flattenAssignments(persons, assignments) {
|
|
10
|
+
const personById = new Map(persons.map((p) => [p.id, p]));
|
|
11
|
+
return assignments.map((a) => {
|
|
12
|
+
const person = personById.get(a.personId);
|
|
13
|
+
return {
|
|
14
|
+
id: a.id,
|
|
15
|
+
itemId: a.itemId,
|
|
16
|
+
personId: a.personId,
|
|
17
|
+
data: { ...(person?.data ?? {}), ...a.data },
|
|
18
|
+
createdAt: a.createdAt,
|
|
19
|
+
updatedAt: a.updatedAt
|
|
20
|
+
};
|
|
21
|
+
});
|
|
22
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { PaymentCreateContext } from '../shop/types.js';
|
|
2
|
+
/** Split "Imię Nazwisko" → first/last (first whitespace splits). */
|
|
3
|
+
export declare function splitName(name: string): {
|
|
4
|
+
firstName?: string;
|
|
5
|
+
lastName?: string;
|
|
6
|
+
};
|
|
7
|
+
export interface BuildPaymentContextInput {
|
|
8
|
+
accessToken: string;
|
|
9
|
+
language: string | null;
|
|
10
|
+
customer: Record<string, unknown>;
|
|
11
|
+
portalUrlTemplate: string | null;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Build the {@link PaymentCreateContext} a booking hands to its payment adapter:
|
|
15
|
+
* a fully-resolved portal `continueUrl` (template `{token}` → accessToken) plus
|
|
16
|
+
* buyer identity derived from `customer`. Lets any shop adapter serve booking
|
|
17
|
+
* without a shop module.
|
|
18
|
+
*/
|
|
19
|
+
export declare function buildPaymentContext(input: BuildPaymentContextInput): PaymentCreateContext;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/** Split "Imię Nazwisko" → first/last (first whitespace splits). */
|
|
2
|
+
export function splitName(name) {
|
|
3
|
+
const trimmed = (name ?? '').trim();
|
|
4
|
+
if (!trimmed)
|
|
5
|
+
return {};
|
|
6
|
+
const firstSpace = trimmed.search(/\s/);
|
|
7
|
+
if (firstSpace === -1)
|
|
8
|
+
return { firstName: trimmed };
|
|
9
|
+
return {
|
|
10
|
+
firstName: trimmed.slice(0, firstSpace),
|
|
11
|
+
lastName: trimmed.slice(firstSpace + 1).trim() || undefined
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Build the {@link PaymentCreateContext} a booking hands to its payment adapter:
|
|
16
|
+
* a fully-resolved portal `continueUrl` (template `{token}` → accessToken) plus
|
|
17
|
+
* buyer identity derived from `customer`. Lets any shop adapter serve booking
|
|
18
|
+
* without a shop module.
|
|
19
|
+
*/
|
|
20
|
+
export function buildPaymentContext(input) {
|
|
21
|
+
const { accessToken, language, customer, portalUrlTemplate } = input;
|
|
22
|
+
const { firstName, lastName } = splitName(String(customer.name ?? ''));
|
|
23
|
+
const phone = String(customer.phone ?? '').trim() || undefined;
|
|
24
|
+
return {
|
|
25
|
+
language,
|
|
26
|
+
continueUrl: portalUrlTemplate ? portalUrlTemplate.replace('{token}', accessToken) : undefined,
|
|
27
|
+
buyer: { firstName, lastName, phone }
|
|
28
|
+
};
|
|
29
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure adapter: person + item + assignment rows → {@link priceByParticipant}
|
|
3
|
+
* inputs. Resolves each person's date of birth from identity `data` via the
|
|
4
|
+
* role:'birthDate' field; dedupes trips by `tripRef`.
|
|
5
|
+
*/
|
|
6
|
+
import type { FieldDef } from './participant-fields/types.js';
|
|
7
|
+
import type { PricingParticipant, PricedTrip } from './participant-pricing.js';
|
|
8
|
+
/** Person row (subset needed for pricing). */
|
|
9
|
+
export interface PersonRow {
|
|
10
|
+
id: string;
|
|
11
|
+
data: Record<string, unknown>;
|
|
12
|
+
}
|
|
13
|
+
/** Trip-line row (subset needed for pricing). `unitPrice` = adult price (minor units). */
|
|
14
|
+
export interface ItemRow {
|
|
15
|
+
id: string;
|
|
16
|
+
tripRef: string;
|
|
17
|
+
unitPrice: number;
|
|
18
|
+
}
|
|
19
|
+
/** Assignment (junction) row. */
|
|
20
|
+
export interface ItemPersonRow {
|
|
21
|
+
itemId: string;
|
|
22
|
+
personId: string;
|
|
23
|
+
}
|
|
24
|
+
export interface PricingInputs {
|
|
25
|
+
participants: PricingParticipant[];
|
|
26
|
+
trips: PricedTrip[];
|
|
27
|
+
}
|
|
28
|
+
export declare function buildPricingInputs(persons: PersonRow[], items: ItemRow[], assignments: ItemPersonRow[], identityFields: FieldDef[]): PricingInputs;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export function buildPricingInputs(persons, items, assignments, identityFields) {
|
|
2
|
+
const dobKey = identityFields.find((f) => f.role === 'birthDate')?.key ?? null;
|
|
3
|
+
const tripRefByItem = new Map(items.map((it) => [it.id, it.tripRef]));
|
|
4
|
+
const tripsByPerson = new Map();
|
|
5
|
+
for (const a of assignments) {
|
|
6
|
+
const tripRef = tripRefByItem.get(a.itemId);
|
|
7
|
+
if (tripRef === undefined)
|
|
8
|
+
continue;
|
|
9
|
+
const list = tripsByPerson.get(a.personId);
|
|
10
|
+
if (list) {
|
|
11
|
+
if (!list.includes(tripRef))
|
|
12
|
+
list.push(tripRef);
|
|
13
|
+
}
|
|
14
|
+
else {
|
|
15
|
+
tripsByPerson.set(a.personId, [tripRef]);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
const participants = persons.map((p) => {
|
|
19
|
+
const dobRaw = dobKey ? p.data[dobKey] : null;
|
|
20
|
+
return {
|
|
21
|
+
id: p.id,
|
|
22
|
+
dob: typeof dobRaw === 'string' ? dobRaw : null,
|
|
23
|
+
tripRefs: tripsByPerson.get(p.id) ?? []
|
|
24
|
+
};
|
|
25
|
+
});
|
|
26
|
+
const seen = new Set();
|
|
27
|
+
const trips = [];
|
|
28
|
+
for (const it of items) {
|
|
29
|
+
if (seen.has(it.tripRef))
|
|
30
|
+
continue;
|
|
31
|
+
seen.add(it.tripRef);
|
|
32
|
+
trips.push({ ref: it.tripRef, adultPrice: it.unitPrice });
|
|
33
|
+
}
|
|
34
|
+
return { participants, trips };
|
|
35
|
+
}
|