includio-cms 0.37.0 → 0.37.2
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 +57 -7
- package/CHANGELOG.md +42 -0
- package/DOCS.md +1 -1
- package/ROADMAP.md +32 -0
- package/dist/admin/remote/booking.remote.d.ts +3 -0
- package/dist/admin/remote/booking.remote.js +5 -2
- package/dist/booking/client/index.d.ts +5 -2
- package/dist/booking/client/index.js +5 -1
- package/dist/booking/client/use-booking.svelte.d.ts +0 -1
- package/dist/booking/config.d.ts +20 -0
- package/dist/booking/config.js +1 -0
- package/dist/booking/draft.d.ts +23 -4
- package/dist/booking/payment-amount.d.ts +23 -0
- package/dist/booking/payment-amount.js +37 -0
- package/dist/booking/server/adjustments.js +5 -2
- package/dist/booking/server/bookings.d.ts +7 -1
- package/dist/booking/server/bookings.js +59 -11
- package/dist/booking/server/http/create-handler.js +4 -2
- package/dist/booking/server/http/portal-handler.js +7 -2
- package/dist/booking/server/payments.d.ts +14 -4
- package/dist/booking/server/payments.js +11 -5
- package/dist/booking/server/portal.d.ts +5 -3
- package/dist/booking/server/portal.js +13 -5
- package/dist/booking/server/price-source.d.ts +73 -0
- package/dist/booking/server/price-source.js +94 -0
- package/dist/components/ui/input-group/input-group-input.svelte.d.ts +1 -1
- package/dist/components/ui/sidebar/sidebar-input.svelte.d.ts +1 -1
- package/dist/core/fields/jsonLd/builders.d.ts +74 -0
- package/dist/core/fields/jsonLd/builders.js +88 -0
- package/dist/core/fields/jsonLd/graph.d.ts +15 -0
- package/dist/core/fields/jsonLd/graph.js +43 -0
- package/dist/core/fields/jsonLd/index.d.ts +2 -0
- package/dist/core/fields/jsonLd/index.js +2 -0
- package/dist/core/fields/resolveSeo.d.ts +1 -1
- package/dist/core/fields/resolveSeo.js +1 -1
- package/dist/core/fields/slugifyFilename.d.ts +7 -0
- package/dist/core/fields/slugifyFilename.js +14 -0
- package/dist/core/index.d.ts +2 -0
- package/dist/core/index.js +3 -0
- package/dist/core/server/fields/utils/fixOrphans.d.ts +15 -3
- package/dist/core/server/fields/utils/fixOrphans.js +30 -7
- package/dist/db-postgres/schema/booking/bookings.d.ts +17 -0
- package/dist/db-postgres/schema/booking/bookings.js +12 -0
- package/dist/files-local/index.js +5 -5
- package/dist/sveltekit/components/json-ld.svelte +12 -0
- package/dist/sveltekit/components/json-ld.svelte.d.ts +6 -0
- package/dist/sveltekit/components/seo.svelte +62 -11
- package/dist/sveltekit/components/seo.svelte.d.ts +10 -0
- package/dist/sveltekit/index.d.ts +2 -0
- package/dist/sveltekit/index.js +2 -0
- package/dist/sveltekit/server/index.d.ts +1 -0
- package/dist/sveltekit/server/index.js +2 -0
- package/dist/sveltekit/server/sitemap.d.ts +19 -0
- package/dist/sveltekit/server/sitemap.js +43 -0
- package/dist/updates/0.37.1/index.d.ts +2 -0
- package/dist/updates/0.37.1/index.js +24 -0
- package/dist/updates/0.37.2/index.d.ts +2 -0
- package/dist/updates/0.37.2/index.js +20 -0
- package/dist/updates/index.js +5 -1
- package/package.json +1 -1
package/API.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
# includio-cms — Public API v0.37.
|
|
1
|
+
# includio-cms — Public API v0.37.2
|
|
2
2
|
|
|
3
3
|
> Auto-generated by `scripts/generate-api-md.ts`. Do not edit by hand.
|
|
4
4
|
|
|
5
|
-
Entry points: **23** · Stable: **
|
|
5
|
+
Entry points: **23** · Stable: **715** · Experimental: **4**
|
|
6
6
|
|
|
7
7
|
Tags:
|
|
8
8
|
- `@public` — frozen for v1.0; semver-protected.
|
|
@@ -13,17 +13,59 @@ Tags:
|
|
|
13
13
|
|
|
14
14
|
### `includio-cms`
|
|
15
15
|
|
|
16
|
+
- `article(input: ArticleInput): JsonLdNode`
|
|
17
|
+
- `interface ArticleInput`
|
|
18
|
+
- `blogPosting(input: ArticleInput): JsonLdNode`
|
|
19
|
+
- `breadcrumbList(items: Array<{ name: string; url: string }>): JsonLdNode`
|
|
16
20
|
- `interface EmailAdapter` — Contract for email adapters. Used by `better-auth` reset-password flow and form submission
|
|
21
|
+
- `faqPage(items: Array<{ question: string; answer: string }>): JsonLdNode`
|
|
22
|
+
- `interface GeoInput`
|
|
23
|
+
- `imageNode(img: string | MediaFile): string | JsonLdNode` — Normalize an image to a URL string or an ImageObject with dimensions.
|
|
24
|
+
- `jsonLdGraph(nodes: JsonLdNode | JsonLdNode[]): { '@context': 'https://schema.org'; '@graph': JsonLdNode[]; }` — Wrap node(s) into a schema.org
|
|
25
|
+
- `type JsonLdNode = Record<string, unknown> & { '@type': string | string[] }`
|
|
26
|
+
- `localBusiness(input: LocalBusinessInput): JsonLdNode`
|
|
27
|
+
- `interface LocalBusinessInput`
|
|
28
|
+
- `organization(input: OrganizationInput): JsonLdNode`
|
|
29
|
+
- `interface OrganizationInput`
|
|
30
|
+
- `interface PostalAddressInput`
|
|
31
|
+
- `pruneEmpty(value: T): T` — Recursively drop undefined/null/'' and empty arrays/objects.
|
|
17
32
|
- `type ResolvedSeo = { [K in keyof SeoFieldData]: K extends 'ogImage' ? string | MediaFile | undefined : string | unde...` — Normalized SEO data — localized values flattened by `language`. A stable
|
|
18
33
|
- `resolveSeo(entry: { seo?: Record<string, unknown> } | Record<string,..., language?: string): ResolvedSeo` — Resolve `entry.seo` into flat fields, flattening localized objects by
|
|
19
34
|
- `interface SendMailOptions`
|
|
35
|
+
- `slugifyFilename(base: string): string` — Slugify a media file base name (extension handled by the caller).
|
|
36
|
+
- `thing(type: string | string[], props: Record<string, unknown> = {}): JsonLdNode` — Build a schema.org node of `type` from `props`, pruning empty values.
|
|
37
|
+
- `webPage(input: WebPageInput): JsonLdNode`
|
|
38
|
+
- `interface WebPageInput`
|
|
39
|
+
- `website(input: WebSiteInput): JsonLdNode`
|
|
40
|
+
- `interface WebSiteInput`
|
|
20
41
|
|
|
21
42
|
### `includio-cms/core`
|
|
22
43
|
|
|
44
|
+
- `article(input: ArticleInput): JsonLdNode`
|
|
45
|
+
- `interface ArticleInput`
|
|
46
|
+
- `blogPosting(input: ArticleInput): JsonLdNode`
|
|
47
|
+
- `breadcrumbList(items: Array<{ name: string; url: string }>): JsonLdNode`
|
|
23
48
|
- `interface EmailAdapter` — Contract for email adapters. Used by `better-auth` reset-password flow and form submission
|
|
49
|
+
- `faqPage(items: Array<{ question: string; answer: string }>): JsonLdNode`
|
|
50
|
+
- `interface GeoInput`
|
|
51
|
+
- `imageNode(img: string | MediaFile): string | JsonLdNode` — Normalize an image to a URL string or an ImageObject with dimensions.
|
|
52
|
+
- `jsonLdGraph(nodes: JsonLdNode | JsonLdNode[]): { '@context': 'https://schema.org'; '@graph': JsonLdNode[]; }` — Wrap node(s) into a schema.org
|
|
53
|
+
- `type JsonLdNode = Record<string, unknown> & { '@type': string | string[] }`
|
|
54
|
+
- `localBusiness(input: LocalBusinessInput): JsonLdNode`
|
|
55
|
+
- `interface LocalBusinessInput`
|
|
56
|
+
- `organization(input: OrganizationInput): JsonLdNode`
|
|
57
|
+
- `interface OrganizationInput`
|
|
58
|
+
- `interface PostalAddressInput`
|
|
59
|
+
- `pruneEmpty(value: T): T` — Recursively drop undefined/null/'' and empty arrays/objects.
|
|
24
60
|
- `type ResolvedSeo = { [K in keyof SeoFieldData]: K extends 'ogImage' ? string | MediaFile | undefined : string | unde...` — Normalized SEO data — localized values flattened by `language`. A stable
|
|
25
61
|
- `resolveSeo(entry: { seo?: Record<string, unknown> } | Record<string,..., language?: string): ResolvedSeo` — Resolve `entry.seo` into flat fields, flattening localized objects by
|
|
26
62
|
- `interface SendMailOptions`
|
|
63
|
+
- `slugifyFilename(base: string): string` — Slugify a media file base name (extension handled by the caller).
|
|
64
|
+
- `thing(type: string | string[], props: Record<string, unknown> = {}): JsonLdNode` — Build a schema.org node of `type` from `props`, pruning empty values.
|
|
65
|
+
- `webPage(input: WebPageInput): JsonLdNode`
|
|
66
|
+
- `interface WebPageInput`
|
|
67
|
+
- `website(input: WebSiteInput): JsonLdNode`
|
|
68
|
+
- `interface WebSiteInput`
|
|
27
69
|
|
|
28
70
|
### `includio-cms/core/server`
|
|
29
71
|
|
|
@@ -332,8 +374,10 @@ Tags:
|
|
|
332
374
|
- `const Image: LegacyComponentType`
|
|
333
375
|
- `isImageFieldData(media: MediaFieldData): media is ImageFieldData`
|
|
334
376
|
- `isVideoFieldData(media: MediaFieldData): media is VideoFieldData`
|
|
377
|
+
- `const JsonLd: LegacyComponentType`
|
|
335
378
|
- `const Media: LegacyComponentType`
|
|
336
379
|
- `const Preview: LegacyComponentType`
|
|
380
|
+
- `const Seo: LegacyComponentType`
|
|
337
381
|
- `setCustomFields(defs: Map<string, CustomFieldDefinition>): <inferred>`
|
|
338
382
|
- `setPreferMp4(value: boolean): <inferred>`
|
|
339
383
|
- `setRemotes(data: typeof remotes): <inferred>`
|
|
@@ -343,6 +387,8 @@ Tags:
|
|
|
343
387
|
|
|
344
388
|
### `includio-cms/sveltekit/server`
|
|
345
389
|
|
|
390
|
+
- `buildRobots(options: RobotsOptions = {}): string` — Build a robots.txt document. Defaults to allow-all + optional sitemap link.
|
|
391
|
+
- `buildSitemap(entries: SitemapEntry[]): string` — Build a sitemap.xml document from a list of URL entries.
|
|
346
392
|
- `cmsLayoutLoad(event: RequestEvent): <inferred>` — Returns `cmsContext` from `event.locals` for use in a SvelteKit layout
|
|
347
393
|
- `countEntries(opts: CountEntriesOptions): Promise<number>` — Count entries matching the same filters as `resolveEntries`, without
|
|
348
394
|
- `type CountEntriesOptions = Omit< ResolveEntriesOptions, 'limit' | 'offset' | 'populate' | 'orderBy' | 'dataOrderBy' >`
|
|
@@ -365,7 +411,10 @@ Tags:
|
|
|
365
411
|
- `resolveEntry(opts: ResolveEntryOptions): Promise<Entry | null>` — Fetch a single populated Entry.
|
|
366
412
|
- `interface ResolveEntryOptions`
|
|
367
413
|
- `type ResolveStatus = 'published' | 'draft' | 'scheduled'` — Status filter for `resolveEntry`/`resolveEntries`/`countEntries`.
|
|
414
|
+
- `interface RobotsOptions`
|
|
415
|
+
- `interface RobotsRule`
|
|
368
416
|
- `sendMail(options: SendMailOptions): Promise<void>` — Send an email through the configured CMS email adapter. Thin convenience
|
|
417
|
+
- `interface SitemapEntry`
|
|
369
418
|
|
|
370
419
|
### `includio-cms/db-postgres`
|
|
371
420
|
|
|
@@ -688,24 +737,25 @@ Tags:
|
|
|
688
737
|
- `interface BookingStatusItem`
|
|
689
738
|
- `type BookingTx = Parameters<Parameters<ReturnType<typeof getBookingDb>['transaction']>[0]>[0]` — Exact drizzle transaction handle, derived from the booking db's `transaction`.
|
|
690
739
|
- `class BookingValidationError` — Thrown when a booking fails capacity (core) or the client `validateBooking` seam.
|
|
691
|
-
- `createBooking(input: CreateBookingInput, opts: { now?: Date } = {}): Promise<{ booking: typeof bookingsTable.$inferSelect; accessToken: string }>` — Create a booking (public self-serve or staff). Validates catalog capacity +
|
|
740
|
+
- `createBooking(input: CreateBookingInput, opts: { now?: Date; actor?: PriceActor } = {}): Promise<{ booking: typeof bookingsTable.$inferSelect; accessToken: string }>` — Create a booking (public self-serve or staff). Validates catalog capacity +
|
|
692
741
|
- `createBookingHandler(): { POST: RequestHandler }` — Public self-serve booking creation. Mount at `POST /api/booking`.
|
|
693
742
|
- `createDepositPayment(bookingId: string): Promise<{ redirectUrl: string | null; status: 'redirect' | 'manual' | 'error'...` — Create the deposit payment for a freshly-created booking: ask the first
|
|
694
|
-
- `createInstallmentPayment(bookingId: string,
|
|
743
|
+
- `createInstallmentPayment(bookingId: string, opts: InstallmentPaymentOptions): Promise<{ paymentLink: string | null }>` — Generate an installment/balance payment link. Records a pending ledger row keyed
|
|
695
744
|
- `createMaintenanceHandler(): { POST: RequestHandler }` — Secured external trigger for booking maintenance — the swap-in for the
|
|
696
745
|
- `createPortalHandler(): { GET: RequestHandler; POST: RequestHandler }` — Self-service portal. Mount at `GET|POST /api/booking/portal/[token]`.
|
|
697
746
|
- `createWebhookHandler(): { POST: RequestHandler }` — Payment-provider webhook. Mount at `POST /api/booking/webhook/[provider]`.
|
|
698
747
|
- `editBooking(bookingId: string, changes: BookingEditChanges, opts: { actor: 'staff' | 'portal'; now?: Date }): Promise<typeof bookingsTable.$inferSelect>` — Edit a booking (staff or portal). Recomputes totals per-participant + derived
|
|
699
748
|
- `getBookingDb(): <inferred>` — Returns the Drizzle handle, asserting the postgres adapter is in use.
|
|
700
749
|
- `getBookingMaintenanceStatus(): { running: boolean; lastRun: Date | null }`
|
|
750
|
+
- `interface InstallmentPaymentOptions`
|
|
701
751
|
- `listBookingAdjustments(bookingId: string): Promise<BookingAdjustment[]>` — List all adjustments (active + voided) for a booking, newest first.
|
|
702
752
|
- `pickEmailTemplate(kind: BookingEmailKind): string` — Template file name for a kind (1:1 — kept as a function for future indirection).
|
|
703
|
-
- `portalAddTrips(token: string, additions: Array<{ tripRef: string;
|
|
753
|
+
- `portalAddTrips(token: string, additions: Array<{ tripRef: string; assignments: string[] }>): <inferred>` — Self-service dobieranie wycieczki: doczytaj obecny stan, scal z `additions`
|
|
704
754
|
- `portalEdit(token: string, changes: BookingEditChanges): <inferred>` — Portal self-service edit (actor:'portal' guards). Throws → handler maps to 409/400.
|
|
705
|
-
- `portalPay(token: string
|
|
755
|
+
- `portalPay(token: string): <inferred>` — Portal: create a balance payment link. The amount is NOT taken from the request —
|
|
706
756
|
- `portalUpdateParticipant(token: string, assignmentId: string, data: Record<string, unknown>): <inferred>` — Portal self-service: customer fills their own participant data. Verifies the
|
|
707
757
|
- `portalView(token: string): <inferred>` — Portal view: booking + items + participants + ledger (no staff data).
|
|
708
|
-
- `priceFromExistingRows(tx: BookingTx, bookingId: string, config: ResolvedBookingConfig
|
|
758
|
+
- `priceFromExistingRows(tx: BookingTx, bookingId: string, config: ResolvedBookingConfig): Promise<ParticipantPriceResult>` — Price a booking from its CURRENTLY persisted people/items/assignments (no
|
|
709
759
|
- `processDueExpirations(now: Date = new Date()): Promise<{ abandoned: number; forfeited: number }>` — Auto-cancel expired bookings: abandoned drafts (deposit hold lapsed) and
|
|
710
760
|
- `processDueReminders(now: Date = new Date()): Promise<number>` — Send all due reminders (unsent, `scheduledFor <= now`) via the email adapter,
|
|
711
761
|
- `readTripCapacity(tripRef: string): Promise<number | null>` — Read a trip's seat capacity from the catalog collection.
|
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,48 @@
|
|
|
3
3
|
All notable changes to includio-cms are documented here.
|
|
4
4
|
Generated from `src/lib/updates/` — do not edit manually.
|
|
5
5
|
|
|
6
|
+
## 0.37.2 — 2026-07-16
|
|
7
|
+
|
|
8
|
+
Powierzchnia SEO/AEO rdzenia — reużywalne helpery, które raz naprawione w rdzeniu działają u każdego klienta (wyszły z wdrożenia SEO u pod-zwrotnikami). W tym samym patchu domyka się też łatka bezpieczeństwa bookingu (kwota płatności).
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- **Buildery JSON-LD (schema.org) w `includio-cms/core`.** Typowane, czyste funkcje: `organization`, `localBusiness`, `website`, `webPage`, `breadcrumbList`, `article`, `blogPosting`, `faqPage` + generyk `thing` i `jsonLdGraph`. Opt-in — nic nie emitują domyślnie. `undefined`/puste wartości wypadają z outputu, więc częściowe dane dają poprawny minimalny JSON-LD. Bespoke typy (np. `TouristTrip + Offer`, `Product`) składasz po stronie projektu przez `thing("TouristTrip", { ... })` — rdzeń zostaje generyczny.
|
|
12
|
+
- **Komponent `<JsonLd>` w `includio-cms/sveltekit`.** Renderuje `<script type="application/ld+json">` w `<svelte:head>` (SSR), escapuje `<` → breakout ze `<script>` jest niemożliwy. Przyjmuje obiekt albo tablicę (np. wynik `jsonLdGraph`).
|
|
13
|
+
- **`buildSitemap` / `buildRobots` w `includio-cms/sveltekit/server`.** Czyste buildery: `buildSitemap(entries)` → string `sitemap.xml` (URL-e XML-escaped, `lastmod` `Date`→ISO lub string, opcjonalne `changefreq`/`priority`), `buildRobots(options)` → `robots.txt` (domyślnie allow-all + link do sitemapy, konfigurowalne reguły). Projekt posiada route i wpina helper: `export const GET = async () => new Response(buildSitemap(entries), { headers: { "content-type": "application/xml" } });`.
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
- **Slugifikacja nazw plików mediów.** Upload i zmiana nazwy przepuszczają basename przez `slugifyFilename` (nowy helper w `includio-cms/core`): `"IMG-1234 (1).jpeg"` → `img-1234-1.jpeg`, polskie znaki transliterowane, spacje/wielkość/nawiasy usunięte. Tylko nowe pliki — istniejące URL-e w bazie nietknięte (bez migracji). Cap 200 znaków, path-traversal niemożliwy (slug nie zawiera `/` ani `.`).
|
|
17
|
+
- **`<Seo>` dopełniony i wyeksportowany z `includio-cms/sveltekit`.** Wcześniej komponent istniał, ale nie był eksportowany (martwy). Bugfix: `og:url` i `og:type` używają `property=` zamiast `name=` (wymóg Open Graph). Nowe propsy: `ogType` (domyślnie `website`; `article` dokłada `article:published_time`/`modified_time`/`author`/`section`/`tag`), `noindex` (→ `<meta name="robots" content="noindex">`), `siteName` (→ `og:site_name`). `ogImage` typu `MediaFile` emituje `og:image:width`/`height`/`alt`.
|
|
18
|
+
- **Typografia — łamanie sierot przyimkowych.** `fixOrphans` wstawia NBSP nie tylko po jednoliterowych spójnikach (`i/w/z/o/u/a/e`), ale też po krótkich przyimkach (`do na od po we za ze ku nad pod bez dla przy`). Lista konfigurowalna parametrem `words` + eksport `DEFAULT_ORPHAN_WORDS`. Domyślnie konserwatywnie (bez spójników typu `oraz/lub`), żeby nie NBSP-ować za dużo.
|
|
19
|
+
- **Bezpieczeństwo (booking): kwota dopłaty przestaje pochodzić z żądania HTTP.** Domknięcie łuku 0.37.1 (cena szła z katalogu, ale kwota wpłaty — nie): `portalPay` podawał `amount` z ciała żądania prosto do `createInstallmentPayment`, bez konfrontacji z saldem — przeglądarka dyktowała kwotę (nadpłata do ręcznego zwrotu albo link na grosz). Bramka kwotowa oparta na aktorze (bliźniak bramki cenowej): `portal` NIE dyktuje (serwer obciąża `totals.balance`, fail-closed gdy brak salda), `staff` może. Portalowa wpłata zapisuje się teraz z `createdBy` aktora — nie zahardkodowanym `staff` (koniec kłamstwa w audycie).
|
|
20
|
+
- **Booking: `ledger.balance` przestaje gubić rabaty.** `portalView` liczył `ledger` z `totals.gross` → `balance = gross − paid`, pomijając `discount`/`surcharge`/`manualDiscount`; portal pokazywał dwa różne salda i przycisk płatności brał gorzej policzone. Teraz `toPortalView` karmi `ledgerSummary` wartością netto (`netOf(totals)`), więc `ledger.balance === totals.balance`.
|
|
21
|
+
|
|
22
|
+
### Breaking
|
|
23
|
+
- **`createInstallmentPayment(bookingId, amount)` → `createInstallmentPayment(bookingId, { actor, amount? })`.** `actor: "portal" | "staff"` jest wymagany. Dla `portal` kwota jest ignorowana (serwer obciąża saldo); dla `staff` podajesz `amount`. `portalPay(token, amount)` → `portalPay(token)` (kwota nie jest już parametrem). Dotyczy tylko konsumentów wołających te funkcje bezpośrednio — ścieżka przez handler portalu/remote admina zaktualizowana w rdzeniu.
|
|
24
|
+
|
|
25
|
+
## 0.37.1 — 2026-07-14
|
|
26
|
+
|
|
27
|
+
Dwie łatki cenowe w bookingu. (1) Cena rezerwacji przestaje pochodzić z żądania HTTP — do 0.37.0 publiczne endpointy przyjmowały `unitPrice` prosto z ciała żądania i nikt nie konfrontował go z katalogiem, więc wyprawę dało się zarezerwować za grosz. (2) Data wyceny zostaje zamrożona na rezerwacji — dotąd każde jej dotknięcie (edycja, ale też zaksięgowanie korekty) przeliczało reguły zależne od czasu wg DZISIEJSZEJ daty, przez co early-bird sprzed miesięcy cicho znikał i klient robił się winien pieniądze.
|
|
28
|
+
|
|
29
|
+
### Added
|
|
30
|
+
- `defineBooking({ resolvePrice })` — serwerowy szew zwracający cenę bazową (dorosłego) pozycji katalogu w groszach. Rdzeń nie zna kształtu katalogu, więc cenę podaje projekt: `resolvePrice: async (tripRef) => Math.round(Number((await resolveEntry({ id: tripRef, status: "published" }))?.price ?? 0) * 100)`.
|
|
31
|
+
- Model zaufania oparty na aktorze: `createBooking(input, { actor })` przyjmuje `public | portal | staff`. Domyślny aktor to `public` (niezaufany) — pominięcie parametru nie otwiera dziury. Zalogowana obsługa (`staff`) nadal może podać cenę ręcznie: korekta i rabat negocjowany telefonicznie zostają możliwe.
|
|
32
|
+
- Typ `PricedCreateInput` wymusza kolejność: rezerwacji nie da się zbudować z pozycji, której nie wyceniła bramka — pilnuje tego kompilator, nie dyscyplina.
|
|
33
|
+
|
|
34
|
+
### Fixed
|
|
35
|
+
- **Bezpieczeństwo (data wyceny).** Nowa kolumna `bookings.priced_at` zamraża moment wyceny. `editBooking` i `priceFromExistingRows` (ścieżka korekt — `adjustments.ts:65` i `:155`) liczą teraz early-bird na tę datę, nie na `new Date()`. Wcześniej wystarczyło, że obsługa zaksięguje dopłatę ustaloną telefonicznie tydzień przed wylotem, a rabat za wczesną rezerwację przepadał. `priceFromExistingRows` straciło parametr `now` — kompilator nie pozwoli już podać tam zegara.
|
|
36
|
+
- Rezerwacje sprzed 0.37.1 mają `priced_at = NULL` i wyceniają się z `created_at` — dokładnie tym, czym było wtedy `now`. Zero migracji danych, zero zmiany zachowania.
|
|
37
|
+
- **Bezpieczeństwo (ceny).** Trzy publiczne drogi wpuszczały cenę z zewnątrz: tworzenie rezerwacji (`createBookingHandler`), dokładanie wycieczki przez portal (`add-trips`) i portalowa edycja pozycji (`portalEdit` → `editBooking`). Bramka stoi teraz w rdzeniu (`createBooking`/`editBooking`), nie w handlerach, więc każdy przyszły publiczny endpoint jest chroniony domyślnie.
|
|
38
|
+
- Cena pozycji już zapisanej w rezerwacji jest brana z jej `unitPriceSnapshot`, a nie przeliczana z katalogu — dotknięcie rezerwacji przez portal nie podniesie klientowi ceny, gdy cennik w międzyczasie poszedł w górę.
|
|
39
|
+
- Odczyt katalogu w `editBooking` odbywa się przed otwarciem transakcji: szew klienta robi własne I/O, a wołany pod blokadą `FOR UPDATE` trzymałby wiersz rezerwacji i sięgał po drugie połączenie z tej samej puli.
|
|
40
|
+
- Ceny ujemne i niecałkowite (ułamek grosza) są odrzucane — również dla obsługi.
|
|
41
|
+
|
|
42
|
+
### Breaking
|
|
43
|
+
- Nowa kolumna `bookings.priced_at` — wymagana migracja (`drizzle-kit push`). Nullable, bez backfillu.
|
|
44
|
+
- **`defineBooking({ resolvePrice })` jest teraz wymagany, gdy booking wystawia publiczne endpointy** (`createBookingHandler`, portal). Bez niego rdzeń odmawia wyceny (fail-closed) zamiast po cichu zaufać cenie z żądania. Instalacje używające wyłącznie panelu admina działają bez zmian.
|
|
45
|
+
- `BookingItemInput.unitPrice` jest opcjonalne i **honorowane wyłącznie dla `actor: "staff"`**. Dla klienta końcowego jest ignorowane — cenę ustala katalog.
|
|
46
|
+
- Publiczne API klienta: `createBookingClient().addTrips(token, additions)` oraz `useBooking().addTrips(additions)` nie przyjmują już `unitPrice` w pozycjach (`{ tripRef, assignments }`). Storefronty przekazujące cenę muszą ją usunąć z ładunku — była i tak ignorowana po stronie serwera.
|
|
47
|
+
|
|
6
48
|
## 0.37.0 — 2026-06-17
|
|
7
49
|
|
|
8
50
|
Moduły-nakładki + context switcher: shop i booking jako równorzędne, opcjonalne moduły rozszerzające rdzeń CMS (model „WooCommerce na WordPressie"). Zunifikowany rejestr `config.modules`, przełącznik kontekstu w adminie (Treści / Sklep / Rezerwacje) dla czytelniejszego UI, per-moduł dashboard. Shop przepięty na nowy kontrakt `CMSModule` (dowód generyczności #1), booking dodany jako drugi moduł biblioteki z pełnym rdzeniem domenowym (wycena/state machine/księga płatności) — dowód #2. Booking write-flow + portal self-service domknięte: tworzenie (public+staff), zadatek + webhook idempotentny, pełna edycja self-service (recompute/pojemność/„nie poniżej wpłaconego”), maile + przypomnienia, cron bez-infra (in-process loop + secured endpoint), strefa obsługi w adminie, scaffold `--booking`. Plan: `docs/superpowers/plans/2026-06-18-booking-write-flow-portal.md`, spec: `docs/superpowers/specs/2026-06-18-booking-write-flow-portal-design.md`.
|
package/DOCS.md
CHANGED
package/ROADMAP.md
CHANGED
|
@@ -35,8 +35,39 @@
|
|
|
35
35
|
- [x] `[feature]` `[P1]` Slug first-class + SEO v1.0 freeze (0.26.0) — `resolveSeo` public (`includio-cms/core`), `seoFieldDescriptor` SSOT (Zod+TS drift-guard), admin URL un-hardcode (`slugPath.ts` pure resolver, no `seo`-field requirement), `SlugField`/`SeoField`/`SeoFieldData` `@public` v1.0-frozen <!-- files: src/lib/core/fields/slugPath.ts, src/lib/core/fields/seoFieldDescriptor.ts, src/lib/core/fields/resolveSeo.ts, src/lib/types/fields.ts, src/lib/admin/client/collection/collection-entries.svelte, src/lib/updates/0.26.0/ -->
|
|
36
36
|
- [x] `[feature]` `[P2]` Soft-delete zamówień admin-only (0.34.0) — ukrycie zamówienia z listy bez utraty danych (`deleted_at`/`deleted_by`), kosz + przywracanie, guard `decideOrderDeletion` (safe-statuses + faktura), auto-restore przy płatności, zwolnienie rezerwacji stocku, `requireRole('admin')` <!-- files: src/lib/shop/server/orders.ts, src/lib/db-postgres/schema/shop/order.ts, src/lib/admin/remote/shop.remote.ts, src/lib/admin/client/shop/{shop-order-detail-page,shop-orders-list-page,restore-order-cell}.svelte, src/lib/updates/0.34.0/ -->
|
|
37
37
|
|
|
38
|
+
## 0.37.1 — Bramka cenowa (bezpieczeństwo)
|
|
39
|
+
|
|
40
|
+
- [x] `[fix]` `[breaking]` `[P0]` **Data wyceny zamrożona na rezerwacji** (`bookings.priced_at`). Dotąd cena bazowa była zamrożona, ale data — nie: `editBooking` i `priceFromExistingRows` (ścieżka **korekt**) przeliczały reguły zależne od czasu wg `new Date()`. Obsługa księgowała dopłatę ustaloną telefonicznie tydzień przed wylotem → `daysUntil(anchor, now)` spadało poniżej progu → **early-bird sprzed miesięcy cicho znikał, a klient robił się winien pieniądze**. Bomba tykała sama, bez żadnej zmiany cennika. Rezerwacje sprzed 0.37.1: `priced_at = NULL` → fallback na `created_at` (zero migracji danych). Pełny snapshot **wartości** polityki (`pricing_snapshot`) zostaje w 0.38.0 §4.4 — wymaga świadomej zmiany cennika przez klientkę, więc da się ją uprzedzić.
|
|
41
|
+
- [x] `[fix]` `[breaking]` `[P0]` **Cena rezerwacji przestaje pochodzić z żądania HTTP.** Do 0.37.0 trzy publiczne drogi wpuszczały `unitPrice` z ciała żądania i nikt nie konfrontował go z katalogiem (`create-handler.ts:37`, portalowe `add-trips`, portalowe `edit` → `editBooking`) — wyprawę dało się zarezerwować za grosz. Bramka stoi teraz w rdzeniu (`createBooking`/`editBooking`), oparta na aktorze: `public`/`portal` nie dyktują ceny (nowa pozycja → szew `resolvePrice` z katalogu, istniejąca → jej `unitPriceSnapshot`), `staff` nadal może korygować ręcznie. Domyślny aktor to `public`, więc przyszły publiczny endpoint jest bezpieczny bez pamiętania o niczym. Fail-closed: brak `resolvePrice` = odmowa wyceny.
|
|
42
|
+
|
|
43
|
+
## 0.37.2 — SEO/AEO surface + kwota płatności
|
|
44
|
+
|
|
45
|
+
> Domknięcie tego samego łuku co 0.37.1: **cena** przestała pochodzić z żądania HTTP, ale **kwota płatności** wciąż pochodzi.
|
|
46
|
+
> Zgłoszone z pod-zwrotnikami (2026-07-14) — klient z rabatem pakietowym płacił w portalu za dużo. Konsument załatał się u siebie
|
|
47
|
+
> (`totals.balance` zamiast `ledger.balance`), ale przyczyna siedzi w rdzeniu i każdy kolejny konsument wdepnie w to samo.
|
|
48
|
+
|
|
49
|
+
- [x] `[fix]` `[P0]` **Kwota dopłaty przestaje pochodzić z żądania HTTP.** `portalPay(token, amount)` podaje `amount` prosto do `createInstallmentPayment(bookingId, amount)` (`payments.ts:149`), a ta wkłada go do `totalGross` adaptera i insertuje `booking_payments` — **bez żadnej konfrontacji z `totals.balance`**. Przeglądarka dyktuje, ile zapłaci: może za dużo (nadpłata do zwrotu ręcznego), może grosz (link płatności na 0,01 zł). Bramka z 0.37.1 stanęła na `unitPrice`, ale nie na kwocie wpłaty. Fix: kwotę wyznacza/weryfikuje serwer względem `totals.balance` (aktor `portal` nie dyktuje kwoty, `staff` może — symetrycznie do bramki cenowej), fail-closed przy rozjeździe. Przy okazji: portalowa wpłata zapisuje się z `createdBy: 'staff'` (`payments.ts:187`), co kłamie w audycie. <!-- files: src/lib/booking/server/portal.ts, src/lib/booking/server/payments.ts -->
|
|
50
|
+
- [x] `[fix]` `[P0]` **`ledger.balance` gubi rabaty — i kusi nazwą.** `portalView` woła `ledgerSummary(booking.totals.gross, payments)` (`server/portal.ts:48`), a `ledgerSummary` liczy `balance = gross - paid` (`ledger.ts:87`), z pominięciem `discount` / `surcharge` / `manualDiscount`. Autorytatywne jest `totals.balance` (`netOf(base) - paid`, `totals.ts:73`) i portalView zwraca je obok — więc widok podaje konsumentowi **dwa różne salda, a to gorzej policzone nosi bardziej oczywistą nazwę**. Efekt u konsumenta: portal pokazywał „Pozostało 532 zł" i „Do dopłaty 610 zł" na jednym ekranie, a przycisk płatności brał tę drugą. Fix: `ledgerSummary` liczy saldo z netto (albo `portalView` przestaje zwracać `ledger.balance` i zostawia sam `overpaid`). Test regresyjny: rezerwacja z rabatem → `ledger.balance === totals.balance`. <!-- files: src/lib/booking/ledger.ts, src/lib/booking/server/portal.ts, src/lib/booking/totals.ts -->
|
|
51
|
+
|
|
52
|
+
> **SEO/AEO surface** (bundlowane w tym samym patchu, 2026-07-16). Reużywalne w rdzeniu — raz naprawione, działa u każdego klienta. Spec: `docs/superpowers/specs/2026-07-15-seo-aeo-core-surface-design.md`, plan: `docs/superpowers/plans/2026-07-16-seo-aeo-core-surface.md`.
|
|
53
|
+
|
|
54
|
+
- [x] `[feature]` `[P1]` **Buildery JSON-LD + `<JsonLd>`** — typowane, czyste buildery schema.org w `includio-cms/core` (`organization`/`localBusiness`/`website`/`webPage`/`breadcrumbList`/`article`/`blogPosting`/`faqPage` + generyk `thing`/`jsonLdGraph`) i komponent `<JsonLd>` (SSR w `<svelte:head>`, escape `<`). Opt-in. <!-- files: src/lib/core/fields/jsonLd/, src/lib/sveltekit/components/json-ld.svelte -->
|
|
55
|
+
- [x] `[fix]` `[P1]` **Slugifikacja nazw plików mediów** — `slugifyFilename` w core; `files-local` slugifikuje basename przy uploadzie/rename (tylko nowe pliki, non-breaking). <!-- files: src/lib/core/fields/slugifyFilename.ts, src/lib/files-local/index.ts -->
|
|
56
|
+
- [x] `[fix]` `[P1]` **`<Seo>` dopełniony + wyeksportowany** — export z `sveltekit`; bugfix `og:url`/`og:type` `name→property`; `ogType`/`noindex`/`siteName`/`article` props; og:image width/height/alt z `MediaFile`. <!-- files: src/lib/sveltekit/components/seo.svelte, src/lib/sveltekit/index.ts -->
|
|
57
|
+
- [x] `[fix]` `[P2]` **fixOrphans: przyimki wieloliterowe** — domyślny zestaw rozszerzony o 2–3-literowe przyimki, konfigurowalny `words` param + `DEFAULT_ORPHAN_WORDS`. <!-- files: src/lib/core/server/fields/utils/fixOrphans.ts -->
|
|
58
|
+
- [x] `[feature]` `[P1]` **`buildSitemap`/`buildRobots`** — czyste buildery w `sveltekit/server`; projekt posiada route. <!-- files: src/lib/sveltekit/server/sitemap.ts -->
|
|
59
|
+
|
|
38
60
|
## 0.38.0 — Moduły: kontrakt vs polityka
|
|
39
61
|
|
|
62
|
+
> **WSTRZYMANE (2026-07-14).** Łuk jest dojrzały, ale klientka nie zobaczy z niego nic, a produktyzacja
|
|
63
|
+
> (`module_settings`, `AdminRouter`, kupony) zwraca się dopiero przy **drugim kliencie z bookingiem**.
|
|
64
|
+
> Wracamy, gdy taki się pojawi — wtedy abstrakcje wyjdą z dwóch realnych przypadków, nie z jednego.
|
|
65
|
+
> Wyjątek już zrobiony: kawałek 3a (jądro reguł) leży zrebase'owany na `feat/pricing-rules-kernel`.
|
|
66
|
+
> Poprawka do wprowadzenia przy powrocie: pozycja o typach wypraw twierdzi, że „moduł ma wnosić je sam" —
|
|
67
|
+
> **to jest błędne**. `tripType` to pojęcie domenowe klienta (klient nr 3 może wynajmować kajaki).
|
|
68
|
+
> Moduł może wnieść MECHANIZM (rejestr typów pól + generator pola `blocks` + adapter + headless front),
|
|
69
|
+
> nie POJĘCIE.
|
|
70
|
+
|
|
40
71
|
Spec: `docs/superpowers/specs/2026-07-13-moduly-kontrakt-vs-polityka-design.md`. Kolejność i uzasadnienia tam.
|
|
41
72
|
|
|
42
73
|
- [x] `[feature]` `[P0]` Kawałek 3a — jądro reguł w rdzeniu (`core/rules`) + cennik jako kategorie uczestnika; golden testy parytetu ze starym silnikiem. Branch `feat/pricing-rules-kernel`.
|
|
@@ -72,6 +103,7 @@ Spec: `docs/superpowers/specs/2026-07-13-moduly-kontrakt-vs-polityka-design.md`.
|
|
|
72
103
|
|
|
73
104
|
## Backlog
|
|
74
105
|
|
|
106
|
+
- [ ] `[feature]` `[P2]` **Panel SEO w sidebarze admina (Yoast-like)** — osobny brainstorm/spec. Guided, niestechniczny panel (odwrotność przytłaczającego Yoasta, on-brand): globalna konfiguracja SEO, per-wpisowy box (snippet preview, noindex toggle, focus keyword → wtedy świadome rozszerzenie pola `seo`), pobranie sitemapy (nakładka na `buildSitemap`). **Przekierowania (redirects)** prawdopodobnie własny spec (tabela DB + hook 301/302 + CRUD). Szwy pod to = SEO surface 0.37.2.
|
|
75
107
|
- [ ] `[fix]` `[P1]` Wyciek puli połączeń DB w dev (HMR) — pojedynczy proces `vite dev` konsumenta trzymał **100 połączeń** do Postgresa (cały `max_connections`), po czym baza odbijała wszystko: `FATAL: sorry, too many clients already` (zaobserwowane 2026-07-13 na pod-zwrotnikami). Hipoteza: każde przeładowanie modułu przez HMR tworzy nowy klient `postgres.js` (pula domyślnie 10), a stary nie jest zamykany — 10 przeładowań wyczerpuje limit. Sprawdzić zamykanie klienta w `import.meta.hot.dispose` / singleton na `globalThis` w adapterze. Dotyka każdego, kto dłużej pracuje w dev. <!-- files: src/lib/db-postgres/index.ts -->
|
|
76
108
|
- [ ] `[chore]` `[P1]` CMS doctor — `validateLayout()` jest napisany i przetestowany, ale nie jest wołany z żadnego miejsca produkcyjnego: literówka w nazwie pola w tabie przechodzi po cichu (pole ląduje w orphanach). Podpiąć go do `defineConfig` obok `validatePreviews`, dołożyć `pnpm cms check` w CLI i zestaw reguł zdrowia configu (martwe pola, `pathTemplate` bez `previewUrl`/`previews`, itp.). <!-- files: src/lib/core/fields/layoutUtils.ts, src/lib/sveltekit/config.ts, src/lib/core/config/, src/lib/cli/index.ts -->
|
|
77
109
|
- [ ] `[feature]` `[P2]` Date/datetime field — przebudowa na shadcn-svelte (bits-ui Calendar/DatePicker) zamiast natywnego inputu. Zgłoszone w QA Etap 5a (4/5); funkcjonalnie OK, odłożone jako osobny redesign pola daty. <!-- files: src/lib/admin/components/fields/date-field.svelte, datetime-field.svelte -->
|
|
@@ -24,6 +24,7 @@ export declare const listBookings: import("@sveltejs/kit").RemoteQueryFunction<{
|
|
|
24
24
|
currency: string;
|
|
25
25
|
customer: Record<string, unknown>;
|
|
26
26
|
totals: import("../../db-postgres/schema/booking/index.js").BookingTotals;
|
|
27
|
+
pricedAt: Date | null;
|
|
27
28
|
accessToken: string;
|
|
28
29
|
staffNotes: string | null;
|
|
29
30
|
language: string | null;
|
|
@@ -60,6 +61,7 @@ export declare const getBooking: import("@sveltejs/kit").RemoteQueryFunction<str
|
|
|
60
61
|
currency: string;
|
|
61
62
|
customer: Record<string, unknown>;
|
|
62
63
|
totals: import("../../db-postgres/schema/booking/index.js").BookingTotals;
|
|
64
|
+
pricedAt: Date | null;
|
|
63
65
|
accessToken: string;
|
|
64
66
|
staffNotes: string | null;
|
|
65
67
|
language: string | null;
|
|
@@ -172,6 +174,7 @@ export declare const editBookingStaff: import("@sveltejs/kit").RemoteCommand<{
|
|
|
172
174
|
updatedAt: Date;
|
|
173
175
|
customer: Record<string, unknown>;
|
|
174
176
|
totals: import("../../db-postgres/schema/booking/index.js").BookingTotals;
|
|
177
|
+
pricedAt: Date | null;
|
|
175
178
|
staffNotes: string | null;
|
|
176
179
|
balanceDueAt: Date | null;
|
|
177
180
|
holdExpiresAt: Date | null;
|
|
@@ -196,7 +196,10 @@ export const listBookingCatalog = query(async () => {
|
|
|
196
196
|
/** Staff: create a booking from the admin panel. @public */
|
|
197
197
|
export const createBookingStaff = command(z.any(), async (input) => {
|
|
198
198
|
requireAuth();
|
|
199
|
-
|
|
199
|
+
// `actor: 'staff'` — zalogowana obsługa może podać cenę ręcznie (korekta, rabat
|
|
200
|
+
// negocjowany telefonicznie). Publiczne ścieżki NIE mają tego prawa: bez tej flagi
|
|
201
|
+
// rdzeń bierze cenę wyłącznie z katalogu (`resolvePrice`).
|
|
202
|
+
return createBooking(input, { actor: 'staff' });
|
|
200
203
|
});
|
|
201
204
|
/** Staff: edit a booking's items/customer (full edit power). @public */
|
|
202
205
|
export const editBookingStaff = command(z.object({ bookingId: z.string().uuid(), changes: z.any() }), async ({ bookingId, changes }) => {
|
|
@@ -206,7 +209,7 @@ export const editBookingStaff = command(z.object({ bookingId: z.string().uuid(),
|
|
|
206
209
|
/** Staff: generate an installment/balance payment link for any amount. @public */
|
|
207
210
|
export const addPaymentLink = command(z.object({ bookingId: z.string().uuid(), amount: z.number().int().positive() }), async ({ bookingId, amount }) => {
|
|
208
211
|
requireAuth();
|
|
209
|
-
const result = await createInstallmentPayment(bookingId, amount);
|
|
212
|
+
const result = await createInstallmentPayment(bookingId, { actor: 'staff', amount });
|
|
210
213
|
if (result.paymentLink) {
|
|
211
214
|
await sendBookingEmail(bookingId, 'payment-link', {
|
|
212
215
|
amount,
|
|
@@ -29,10 +29,13 @@ export declare function createBookingClient(opts?: BookingClientOptions): {
|
|
|
29
29
|
updateParticipant: (token: string, participantId: string, data: Record<string, unknown>) => Promise<unknown>;
|
|
30
30
|
/** Drugi czynnik: weryfikuj numer telefonu; sukces ustawia sesję portalu (cookie). */
|
|
31
31
|
verifyPhone: (token: string, phone: string) => Promise<unknown>;
|
|
32
|
-
/**
|
|
32
|
+
/**
|
|
33
|
+
* Self-service: dołóż wycieczki istniejącym osobom (recompute salda).
|
|
34
|
+
*
|
|
35
|
+
* Cena **nie jest** parametrem — ustala ją serwer z katalogu (`resolvePrice`).
|
|
36
|
+
*/
|
|
33
37
|
addTrips: (token: string, additions: Array<{
|
|
34
38
|
tripRef: string;
|
|
35
|
-
unitPrice: number;
|
|
36
39
|
assignments: string[];
|
|
37
40
|
}>) => Promise<unknown>;
|
|
38
41
|
};
|
|
@@ -37,7 +37,11 @@ export function createBookingClient(opts = {}) {
|
|
|
37
37
|
updateParticipant: (token, participantId, data) => req(`/api/booking/portal/${token}?action=participant`, jsonInit('POST', { participantId, data })),
|
|
38
38
|
/** Drugi czynnik: weryfikuj numer telefonu; sukces ustawia sesję portalu (cookie). */
|
|
39
39
|
verifyPhone: (token, phone) => req(`/api/booking/portal/${token}?action=verify-phone`, jsonInit('POST', { phone })),
|
|
40
|
-
/**
|
|
40
|
+
/**
|
|
41
|
+
* Self-service: dołóż wycieczki istniejącym osobom (recompute salda).
|
|
42
|
+
*
|
|
43
|
+
* Cena **nie jest** parametrem — ustala ją serwer z katalogu (`resolvePrice`).
|
|
44
|
+
*/
|
|
41
45
|
addTrips: (token, additions) => req(`/api/booking/portal/${token}?action=add-trips`, jsonInit('POST', { additions }))
|
|
42
46
|
};
|
|
43
47
|
}
|
package/dist/booking/config.d.ts
CHANGED
|
@@ -50,6 +50,25 @@ export interface BookingConfig {
|
|
|
50
50
|
surfacedCollections?: string[];
|
|
51
51
|
/** Deposit that secures a reservation, in minor units (default 0). */
|
|
52
52
|
depositAmount?: number;
|
|
53
|
+
/**
|
|
54
|
+
* **Szew serwerowy: cena bazowa (dorosłego) pozycji katalogu, w groszach.**
|
|
55
|
+
*
|
|
56
|
+
* Rdzeń nie zna kształtu katalogu (`tripRef` jest dla niego nieprzezroczysty), więc
|
|
57
|
+
* cenę musi podać projekt. Wołany **wyłącznie na serwerze** przy każdej rezerwacji
|
|
58
|
+
* składanej przez klienta końcowego — dzięki temu przeglądarka nie ma na cenę wpływu.
|
|
59
|
+
*
|
|
60
|
+
* **Wymagany, gdy booking wystawia publiczne endpointy** (`createBookingHandler`,
|
|
61
|
+
* portal). Bez niego rdzeń odmawia wyceny (fail-closed) zamiast zaufać cenie z żądania.
|
|
62
|
+
* Obsługa w adminie (`actor: 'staff'`) nadal może podać cenę ręcznie — korekta jest
|
|
63
|
+
* zamierzoną funkcją.
|
|
64
|
+
*
|
|
65
|
+
* @example
|
|
66
|
+
* resolvePrice: async (tripRef) => {
|
|
67
|
+
* const entry = await resolveEntry({ id: tripRef, status: 'published' });
|
|
68
|
+
* return Math.round(Number(entry?.price ?? 0) * 100); // PLN → grosze
|
|
69
|
+
* }
|
|
70
|
+
*/
|
|
71
|
+
resolvePrice?: (tripRef: string) => number | Promise<number>;
|
|
53
72
|
/** Discount rules (legacy per-line engine; booking now prices per-participant). */
|
|
54
73
|
pricing?: PricingConfig;
|
|
55
74
|
/**
|
|
@@ -127,6 +146,7 @@ export interface ResolvedBookingConfig {
|
|
|
127
146
|
catalogCollection: string | null;
|
|
128
147
|
surfacedCollections: string[];
|
|
129
148
|
depositAmount: number;
|
|
149
|
+
resolvePrice: ((tripRef: string) => number | Promise<number>) | null;
|
|
130
150
|
pricing: PricingConfig;
|
|
131
151
|
participantPricing: (() => ParticipantPricingConfig | Promise<ParticipantPricingConfig>) | null;
|
|
132
152
|
arrivalDateField: string;
|
package/dist/booking/config.js
CHANGED
|
@@ -5,6 +5,7 @@ export function resolveBookingConfig(config) {
|
|
|
5
5
|
catalogCollection: config.catalogCollection ?? null,
|
|
6
6
|
surfacedCollections: config.surfacedCollections ?? [],
|
|
7
7
|
depositAmount: config.depositAmount ?? 0,
|
|
8
|
+
resolvePrice: config.resolvePrice ?? null,
|
|
8
9
|
pricing: config.pricing ?? { rules: [] },
|
|
9
10
|
participantPricing: config.participantPricing ?? null,
|
|
10
11
|
arrivalDateField: config.arrivalDateField ?? 'arrivalDate',
|
package/dist/booking/draft.d.ts
CHANGED
|
@@ -24,8 +24,15 @@ export interface ItemAssignmentInput {
|
|
|
24
24
|
export interface BookingItemInput {
|
|
25
25
|
tripRef: string;
|
|
26
26
|
departureDate: Date | null;
|
|
27
|
-
/**
|
|
28
|
-
|
|
27
|
+
/**
|
|
28
|
+
* Cena bazowa (dorosłego) w groszach, zamrażana na pozycji.
|
|
29
|
+
*
|
|
30
|
+
* **Honorowana wyłącznie dla `actor: 'staff'`** (ręczna korekta w adminie). Dla klienta
|
|
31
|
+
* końcowego (`public`/`portal`) jest ignorowana — cenę ustala rdzeń z katalogu przez szew
|
|
32
|
+
* `resolvePrice`, bo inaczej każdy mógłby zamówić wyprawę za grosz. Pomiń, gdy cena ma
|
|
33
|
+
* pochodzić z katalogu.
|
|
34
|
+
*/
|
|
35
|
+
unitPrice?: number;
|
|
29
36
|
/** Who takes this trip (+ their per-trip data). Count derives from this. */
|
|
30
37
|
assignments: ItemAssignmentInput[];
|
|
31
38
|
}
|
|
@@ -64,8 +71,20 @@ export interface BookingDraft {
|
|
|
64
71
|
balanceDueAt: Date | null;
|
|
65
72
|
holdExpiresAt: Date;
|
|
66
73
|
}
|
|
74
|
+
/**
|
|
75
|
+
* Pozycja po przejściu przez bramkę cenową — cena jest już ustalona (z katalogu, ze
|
|
76
|
+
* snapshotu albo od obsługi). Typ istnieje po to, by **nie dało się** zbudować rezerwacji
|
|
77
|
+
* z pozycji, której nikt nie wycenił: kompilator pilnuje, że bramka poszła pierwsza.
|
|
78
|
+
*/
|
|
79
|
+
export type PricedBookingItem = BookingItemInput & {
|
|
80
|
+
unitPrice: number;
|
|
81
|
+
};
|
|
82
|
+
/** Ładunek create z pozycjami już wycenionymi. */
|
|
83
|
+
export type PricedCreateInput = Omit<CreateBookingInput, 'items'> & {
|
|
84
|
+
items: PricedBookingItem[];
|
|
85
|
+
};
|
|
67
86
|
/** Build the per-participant pricing inputs from a create payload. */
|
|
68
|
-
export declare function pricingInputsFromCreate(input:
|
|
87
|
+
export declare function pricingInputsFromCreate(input: PricedCreateInput, identityFields: FieldDef[]): {
|
|
69
88
|
participants: PricingParticipant[];
|
|
70
89
|
trips: PricedTrip[];
|
|
71
90
|
};
|
|
@@ -74,4 +93,4 @@ export declare function pricingInputsFromCreate(input: CreateBookingInput, ident
|
|
|
74
93
|
* rows/totals to persist. Prices per-participant (age bands + volume ladder +
|
|
75
94
|
* early-bird), anchored to the arrival date in `ctx.anchor`.
|
|
76
95
|
*/
|
|
77
|
-
export declare function buildBookingDraft(input:
|
|
96
|
+
export declare function buildBookingDraft(input: PricedCreateInput, config: ResolvedBookingConfig, ctx: DraftPricingContext, now: Date): BookingDraft;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure amount gate for installment/balance payment links — the payment-side twin
|
|
3
|
+
* of the 0.37.1 price gate. Keeps the *amount* from being dictated by an untrusted
|
|
4
|
+
* HTTP request. All amounts are in **minor units** (grosze).
|
|
5
|
+
*/
|
|
6
|
+
export type PaymentActor = 'portal' | 'staff';
|
|
7
|
+
/** Thrown when a requested payment amount is not allowed for the actor. */
|
|
8
|
+
export declare class PaymentAmountError extends Error {
|
|
9
|
+
constructor(message: string);
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Resolve the amount to charge on an installment/balance link.
|
|
13
|
+
*
|
|
14
|
+
* - `portal` (untrusted): the server dictates the amount = current outstanding
|
|
15
|
+
* `balance`; any `requestedAmount` from the browser is ignored. Fail-closed
|
|
16
|
+
* when nothing is due (`balance <= 0`).
|
|
17
|
+
* - `staff` (trusted): honors `requestedAmount` (positive integer minor units) —
|
|
18
|
+
* staff may bill a custom amount, e.g. a partial installment or a prepayment.
|
|
19
|
+
*
|
|
20
|
+
* @throws {PaymentAmountError} portal with nothing due, or staff with a
|
|
21
|
+
* non-positive / fractional / missing amount.
|
|
22
|
+
*/
|
|
23
|
+
export declare function resolveInstallmentAmount(balance: number, actor: PaymentActor, requestedAmount?: number): number;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure amount gate for installment/balance payment links — the payment-side twin
|
|
3
|
+
* of the 0.37.1 price gate. Keeps the *amount* from being dictated by an untrusted
|
|
4
|
+
* HTTP request. All amounts are in **minor units** (grosze).
|
|
5
|
+
*/
|
|
6
|
+
/** Thrown when a requested payment amount is not allowed for the actor. */
|
|
7
|
+
export class PaymentAmountError extends Error {
|
|
8
|
+
constructor(message) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = 'PaymentAmountError';
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Resolve the amount to charge on an installment/balance link.
|
|
15
|
+
*
|
|
16
|
+
* - `portal` (untrusted): the server dictates the amount = current outstanding
|
|
17
|
+
* `balance`; any `requestedAmount` from the browser is ignored. Fail-closed
|
|
18
|
+
* when nothing is due (`balance <= 0`).
|
|
19
|
+
* - `staff` (trusted): honors `requestedAmount` (positive integer minor units) —
|
|
20
|
+
* staff may bill a custom amount, e.g. a partial installment or a prepayment.
|
|
21
|
+
*
|
|
22
|
+
* @throws {PaymentAmountError} portal with nothing due, or staff with a
|
|
23
|
+
* non-positive / fractional / missing amount.
|
|
24
|
+
*/
|
|
25
|
+
export function resolveInstallmentAmount(balance, actor, requestedAmount) {
|
|
26
|
+
if (actor === 'staff') {
|
|
27
|
+
if (!Number.isInteger(requestedAmount) || requestedAmount <= 0) {
|
|
28
|
+
throw new PaymentAmountError('Kwota wpłaty musi być dodatnią liczbą całkowitą (grosze).');
|
|
29
|
+
}
|
|
30
|
+
return requestedAmount;
|
|
31
|
+
}
|
|
32
|
+
// portal: browser does not dictate the amount — the server charges the balance.
|
|
33
|
+
if (balance <= 0) {
|
|
34
|
+
throw new PaymentAmountError('Brak salda do zapłaty.');
|
|
35
|
+
}
|
|
36
|
+
return balance;
|
|
37
|
+
}
|
|
@@ -36,7 +36,9 @@ export async function addBookingAdjustment(bookingId, input, opts) {
|
|
|
36
36
|
if (!booking)
|
|
37
37
|
throw new Error(`Booking ${bookingId} not found`);
|
|
38
38
|
assertEditAllowed(booking.status);
|
|
39
|
-
|
|
39
|
+
// Bez `now`: przeliczenie idzie z zamrożonej daty wyceny rezerwacji. Zaksięgowanie
|
|
40
|
+
// dopłaty nie może odebrać klientowi early-birda sprzed miesięcy.
|
|
41
|
+
const result = await priceFromExistingRows(tx, bookingId, config);
|
|
40
42
|
const active = await tx
|
|
41
43
|
.select()
|
|
42
44
|
.from(bookingAdjustmentsTable)
|
|
@@ -109,7 +111,8 @@ export async function voidBookingAdjustment(adjustmentId, opts) {
|
|
|
109
111
|
.update(bookingAdjustmentsTable)
|
|
110
112
|
.set({ voidedAt: now, voidedBy: opts.actor })
|
|
111
113
|
.where(eq(bookingAdjustmentsTable.id, adjustmentId));
|
|
112
|
-
|
|
114
|
+
// Wycofanie korekty — również z zamrożonej daty wyceny, nie z dzisiejszej.
|
|
115
|
+
const result = await priceFromExistingRows(tx, adj.bookingId, config);
|
|
113
116
|
const active = await tx
|
|
114
117
|
.select()
|
|
115
118
|
.from(bookingAdjustmentsTable)
|
|
@@ -3,6 +3,7 @@ import { type CreateBookingInput, type PersonInput, type ItemAssignmentInput } f
|
|
|
3
3
|
import { type ParticipantPriceResult } from '../participant-pricing.js';
|
|
4
4
|
import type { ResolvedBookingConfig } from '../config.js';
|
|
5
5
|
import { type BookingTx } from './reminders.js';
|
|
6
|
+
import { type PriceActor } from './price-source.js';
|
|
6
7
|
/** Edit payload: replace people + items (+assignments) and/or customer details. */
|
|
7
8
|
export interface BookingEditChanges {
|
|
8
9
|
persons?: PersonInput[];
|
|
@@ -22,6 +23,7 @@ export interface BookingEditChanges {
|
|
|
22
23
|
*/
|
|
23
24
|
export declare function createBooking(input: CreateBookingInput, opts?: {
|
|
24
25
|
now?: Date;
|
|
26
|
+
actor?: PriceActor;
|
|
25
27
|
}): Promise<{
|
|
26
28
|
booking: typeof bookingsTable.$inferSelect;
|
|
27
29
|
accessToken: string;
|
|
@@ -41,5 +43,9 @@ export declare function editBooking(bookingId: string, changes: BookingEditChang
|
|
|
41
43
|
* edit). Shared by adjustment recompute — mirrors the "no items change" branch
|
|
42
44
|
* of {@link editBooking}. Must run inside a transaction that has locked the
|
|
43
45
|
* booking row.
|
|
46
|
+
*
|
|
47
|
+
* **Wycenia na zamrożoną datę rezerwacji (`pricedAt`), nie na dziś.** To jest ścieżka
|
|
48
|
+
* korekt: obsługa księguje dopłatę ustaloną telefonicznie, a przeliczenie nie może przy
|
|
49
|
+
* okazji odebrać klientowi rabatu za wczesną rezerwację.
|
|
44
50
|
*/
|
|
45
|
-
export declare function priceFromExistingRows(tx: BookingTx, bookingId: string, config: ResolvedBookingConfig
|
|
51
|
+
export declare function priceFromExistingRows(tx: BookingTx, bookingId: string, config: ResolvedBookingConfig): Promise<ParticipantPriceResult>;
|