Initial Commit
This commit is contained in:
109
openspec/archive/2026-05-17-add-booking/design.md
Normal file
109
openspec/archive/2026-05-17-add-booking/design.md
Normal file
@@ -0,0 +1,109 @@
|
||||
# Design — @suaveplan/booking
|
||||
|
||||
## Architecture overview
|
||||
|
||||
The booking engine is a pipeline composed of six pluggable stages, each owned by an isolated feature module:
|
||||
|
||||
```text
|
||||
Availability → Conflict detection → Pricing → Rules → State machine → Events
|
||||
```
|
||||
|
||||
Every stage is registry-driven (`registerDetector`, `registerAvailabilityStrategy`) so callers can extend behaviour without forking. The pipeline orchestrator is `BookingEngine`, which holds in-memory `Map<id, Bookable | Booking | BookingHold>` registries plus an in-package `Hooks<BookingHookMap>` instance and an `EventBus`.
|
||||
|
||||
## File layout
|
||||
|
||||
```text
|
||||
src/
|
||||
index.ts — explicit named root barrel
|
||||
error-codes/
|
||||
error-codes.ts | error-codes.test.ts | error-codes.md | index.ts
|
||||
hooks/
|
||||
hooks.ts | hooks.test.ts | hooks.md | hooks.types.ts | index.ts
|
||||
utils/
|
||||
utils.ts | utils.test.ts | utils.md | index.ts
|
||||
constants/
|
||||
constants.ts | constants.md | index.ts
|
||||
types/
|
||||
bookable.types.ts (+ .md)
|
||||
booking.types.ts (+ .md)
|
||||
booking-hooks.ts (+ .md)
|
||||
capacity.types.ts (+ .md)
|
||||
config.types.ts (+ .md)
|
||||
events.types.ts (+ .md)
|
||||
layout.types.ts (+ .md)
|
||||
pricing.types.ts (+ .md)
|
||||
rules.types.ts (+ .md)
|
||||
temporal.types.ts (+ .md)
|
||||
temporal/
|
||||
windows/(windows.ts + test + md + index)
|
||||
recurrence/(…)
|
||||
slots/(…)
|
||||
conflict/
|
||||
detector/(detector.ts + .types.ts + test + md + index)
|
||||
exact-match/(…) | range-overlap/(…) | slot-capacity/(…) | provider-overlap/(…)
|
||||
pricing/
|
||||
calculator/(calculator.ts + .types.ts + test + md + index)
|
||||
seasonal/(…) | dynamic/(…)
|
||||
rules/
|
||||
evaluator/(evaluator.ts + .types.ts + test + md + index)
|
||||
conditions/(…)
|
||||
availability/
|
||||
resolver/(resolver.ts + .types.ts + test + md + index)
|
||||
strategies/(strategies.ts + .types.ts + test + md + index)
|
||||
domains/
|
||||
shared.ts | shared.test.ts | shared.md | index.ts
|
||||
seats/(seats.ts + .types.ts + test + md + index)
|
||||
accommodation/(…) | appointments/(…) | resources/(…)
|
||||
activities/(…) | transport/(…)
|
||||
schemas/
|
||||
booking.schema.ts | booking.schema.test.ts | booking.schema.md | index.ts
|
||||
config.schema.ts | config.schema.test.ts | config.schema.md
|
||||
core/
|
||||
engine/(engine.ts + .types.ts + test + md + index)
|
||||
state-machine/(state-machine.ts + test + md + index)
|
||||
event-bus/(event-bus.ts + test + md + index)
|
||||
conformance.spec-conformance.test.ts — barrel + subpath conformance
|
||||
tests/
|
||||
setup/polyfills.ts
|
||||
```
|
||||
|
||||
## Key decisions
|
||||
|
||||
### 1. In-package `Hooks<TMap>` (vs. depending on `@suaveplan/events/hooks`)
|
||||
|
||||
Genesis `@suaveplan/events` does not yet expose `/hooks` or `/hooks.types`. Rather than block this migration on a separate `events-hooks` change, the upstream Hooks impl is re-implemented in-package under `src/hooks/` with the same public surface. The `BookingHookMap` is defined locally so `Hooks<BookingHookMap>` works without external types. When events ships its hooks subpath, the in-package impl can be removed in a follow-up; the contract preserved here matches the upstream surface exactly so consumers do not need to change.
|
||||
|
||||
### 2. Typed errors over plain `throw new Error`
|
||||
|
||||
Every boundary that previously raised a plain `Error` now raises a `BookingError` extending `SuaveplanError<BookingErrorCode>`. Codes are stable namespaced strings (`"BOOKING.ENGINE_NOT_CONFIGURED"`). Optional discriminants — `bookingId`, `holdId`, `mode`, `strategy` — are attached without polluting the base `context` map; consumers can read them on the typed instance after a narrow.
|
||||
|
||||
The `Hooks` sync-path `console.warn` (upstream behaviour when a handler returns a Promise) is upgraded to a `BookingError({ code: BOOKING.HOOK_RETURNED_PROMISE_IN_SYNC })`. Universal-tier libraries should not log to stdout; the typed error lets callers decide how to handle the bug.
|
||||
|
||||
### 3. Module-level mutable registries preserved
|
||||
|
||||
Upstream uses module-level `Map<>` registries for conflict detectors and availability strategies. The migration preserves this for parity (downstream callers depend on the global mutation). A future change can pivot to scoped registries; that is an out-of-scope behaviour change.
|
||||
|
||||
### 4. Determinism is upstream's responsibility
|
||||
|
||||
Upstream uses `Date.now()` and `Math.random()` inside `generateId` for in-memory id generation. The migration preserves both. Tests assert structural properties (prefix, uniqueness across N calls) rather than exact strings; a future change can thread an injectable `IdGenerator` from `@suaveplan/types` once consumers need deterministic ids.
|
||||
|
||||
### 5. Subpath exports
|
||||
|
||||
Each feature module gets a subpath in `package.json` `exports` so consumers like `@suaveplan/r3f-booking` can import only `@suaveplan/booking/temporal/windows/windows` and skip the engine + schemas + rule evaluator. Tree-shaking is verified at build time (preserveModules + sideEffects:false).
|
||||
|
||||
## Public surface inventory
|
||||
|
||||
- Engine + types (`createBookingEngine`, `BookingEngine`, `BookingEngineInterface`, `AvailabilityQuery`, `AvailabilityResult`, `ConflictResult`, `PriceCalculationRequest`)
|
||||
- State machine (`StateMachine`, `createStateMachine`, `TransitionRequest`, `TransitionResult`)
|
||||
- Event bus (`EventBus`, `createEventBus`)
|
||||
- Hooks (`Hooks`, `HookMap`, `HookContext`, `HookHandler`, `HookHandlerOptions`, `HookResult`, `RegisteredHook`, `Hookable`)
|
||||
- Temporal (windows: 9 fns; recurrence: 3 fns; slots: 4 fns)
|
||||
- Conflict (detector: 4 fns + types; 4 built-ins)
|
||||
- Pricing (calculator: 5 fns + types; seasonal: 4 fns; dynamic: 4 fns + types)
|
||||
- Rules (evaluator: 2 fns; conditions: 8 factories)
|
||||
- Availability (resolver: 4 fns + types; strategies: 3 built-ins + `SlotAvailability` type)
|
||||
- Domains (base + 6 presets + 6 meta types)
|
||||
- Schemas (6 entity + 1 config + 5 validate/safe-validate helpers)
|
||||
- Errors (`BOOKING_CODES`, `BookingError`, `BookingErrorCode`, `BookingErrorOptions`)
|
||||
- Utils (5 helpers) + Constants (15 entries)
|
||||
- All upstream type names preserved verbatim (50+)
|
||||
152
openspec/archive/2026-05-17-add-booking/proposal.md
Normal file
152
openspec/archive/2026-05-17-add-booking/proposal.md
Normal file
@@ -0,0 +1,152 @@
|
||||
# Add @suaveplan/booking — universal JSON-configurable booking engine
|
||||
|
||||
**Package**: `@suaveplan/booking`
|
||||
**Status**: PROPOSED
|
||||
**Type**: migration (upstream `../suaveplan/packages/security/booking`)
|
||||
|
||||
- **Design**: ./design.md
|
||||
- **Spec delta**: ./specs/booking/spec.md
|
||||
|
||||
## Why
|
||||
|
||||
`STATUS.md` lists `@suaveplan/booking` as Pending. The upstream package is a substantial, universal-tier, runtime-pure library (51 source files across 11 feature groups, 27 test files, ~99% upstream coverage) that owns a problem genesis currently has no answer for: a JSON-driven booking engine that runs identically in Bun, Node, browsers, and Web Workers and covers six distinct domains (seats, accommodation, appointments, resources, activities, transport) through a single pipeline (availability → conflict detection → pricing → rules → state machine → events). No genesis package covers this surface today, and a string of pending downstream packages (`@suaveplan/r3f-booking`, the `api-plugin-bank`/`api-plugin-banking` plugins, future commerce-tier UIs) depend on the `BookingSystemConfig`, `Bookable`, `Booking`, and `BookingEngine` contracts. Migrating it now unblocks every booking-aware package downstream.
|
||||
|
||||
The migration upgrades the package to genesis quality: every plain `throw new Error(...)` becomes a `BookingError` extending `SuaveplanError` with a stable `BOOKING.*` code registry; the inline `@suaveplan/events/hooks` dependency (not migrated on genesis) is replaced by a small in-package `Hooks<TMap>` implementation that ships under `src/hooks/` with the same public surface the upstream engine consumed; subpath exports become exhaustive (one per public module) so consumers tree-shake; every implementation file gets a sibling `.md`; coverage moves from upstream's ~99% to 100/100/100/100; the package starts at `0.1.0`. The enterprise audit (`../suaveplan/enterprise/booking.md`) lists ambitious additions (property-based tests, full OTel observability, persistence-tier integration, `AbortSignal` propagation, feature flags) — the typed-error upgrade and the in-package `Hooks` are in scope here; the other audit items are explicitly deferred to follow-up changes.
|
||||
|
||||
## What changes
|
||||
|
||||
- ADDED: `BookingEngine<TBookableMeta, TBookingMeta>` and `createBookingEngine` — the pipeline orchestrator with `configure`, `registerBookable`, `getBookable`, `listBookables`, `checkAvailability`, `getAvailableSlots`, `createHold`, `getHold`, `confirmBooking`, `cancelBooking`, `modifyBooking`, `calculatePrice`, `joinWaitlist`, `on`, `off`, `getBooking`, and the `bookingHooks` getter exposing the in-package `Hooks<BookingHookMap>`.
|
||||
- ADDED: `StateMachine` and `createStateMachine` — configurable status-transition state machine with `canTransition`, `getAvailableTransitions`, `getAutoTransitions`, `getAllStatuses`. `TransitionRequest`/`TransitionResult` types preserved.
|
||||
- ADDED: `EventBus` and `createEventBus` — typed booking event bus with `on`/`off`/`emit`/`clear`/`listenerCount`/`eventTypes`.
|
||||
- ADDED: `Hooks<TMap extends HookMap>` — in-package data-transform pipeline (replaces the upstream `@suaveplan/events/hooks` dependency that does not exist on genesis). Public surface preserves `hook`/`unhook`/`unhookAll`/`applyHooks`/`applyHooksAsync`/`applyHooksWithResult`/`hookNames`/`hookCount`/`hasHook`. Types `HookMap`/`HookContext`/`HookHandler`/`HookHandlerOptions`/`HookResult`/`RegisteredHook` re-exported.
|
||||
- ADDED: Temporal — `windowsOverlap`, `windowsOverlapWithBuffer`, `mergeWindows`, `splitWindow`, `windowDurationMs`, `windowContains`, `createWindow`, `isValidWindow`, `windowsAdjacent`; `expandRecurrence`, `isRecurring`, `createRecurrenceRule`; `generateTimeSlots`, `generateDailySlots`, `filterAvailableSlots`, `reduceSlotCapacity`.
|
||||
- ADDED: Conflict detection — pluggable `registerDetector`/`detectConflicts`/`getRegisteredModes`/`clearDetectorRegistry` registry with four built-ins (`detectExactMatch`, `detectRangeOverlap`, `detectSlotCapacity`, `detectProviderOverlap`). `OverlapResult`/`ConflictDetectionMode`/`ConflictDetectorFn`/`CapacityContext` types exported.
|
||||
- ADDED: Pricing — `calculatePrice`, `calculateBasePrice`, `applyTiers`, `applyModifiers`, `calculateTax`; `applySeasonalRate`, `calculateSeasonalMultiplier`, `findApplicableRates`, `getSeasonName`; `applyDynamicPricing`, `calculateDynamicMultiplier`, `createDynamicContext`, `findApplicableDynamicRules`. `PriceCalculationContext`/`PricingPipelineStep`/`DynamicPricingContext` types exported.
|
||||
- ADDED: Rules — `evaluateRule`, `evaluateRules` evaluator with 11 operators (`eq`, `neq`, `gt`, `gte`, `lt`, `lte`, `in`, `not_in`, `between`, `regex`, default deny); built-in condition factories `createMinDurationCondition`, `createMaxDurationCondition`, `createAdvanceBookingCondition`, `createQuantityRangeCondition`, `createAttributeCondition`, `createWeekendCondition`, `createStatusCondition`, `createCustomerLimitCondition`. `EvaluationContext`/`ConditionEvaluatorFn` types exported.
|
||||
- ADDED: Availability — pluggable `registerAvailabilityStrategy`/`resolveAvailability`/`getRegisteredStrategies`/`clearStrategyRegistry` with three built-ins (`exactMatchStrategy`, `rangeOverlapStrategy`, `capacityStrategy`). `AvailabilityStrategyFn`/`SlotAvailability` types exported.
|
||||
- ADDED: Domain presets — `createBaseConfig`, `createSeatConfig`, `createAccommodationConfig`, `createAppointmentConfig`, `createResourceConfig`, `createActivityConfig`, `createTransportConfig`. Domain meta types `SeatBookableMeta`, `AccommodationBookableMeta`, `AppointmentBookableMeta`, `ResourceBookableMeta`, `ActivityBookableMeta`, `TransportBookableMeta` exported.
|
||||
- ADDED: Schemas — `bookableSchema`, `bookingSchema`, `bookingRequestSchema`, `holdRequestSchema`, `timeWindowSchema`, `validateBookable`, `validateBooking`, `validateBookingRequest`; `bookingSystemConfigSchema`, `validateConfig`, `safeValidateConfig`.
|
||||
- ADDED: Utils — `parseDurationMs` (ISO-8601 duration), `clampNumber`, `deepFreeze`, `generateId`, `isNonEmptyString`.
|
||||
- ADDED: Constants — `DEFAULT_HOLD_DURATION_MINUTES`, `DEFAULT_MAX_HOLD_DURATION_MINUTES`, `DEFAULT_MAX_ADVANCE_DAYS`, `DEFAULT_SCHEMA_VERSION`, `DEFAULT_CURRENCY`, `DEFAULT_TAX_STRATEGY`, `DEFAULT_ROUNDING_MODE`, `DEFAULT_ROUNDING_PRECISION`, `DEFAULT_AVAILABILITY_REFRESH_MS`, `DEFAULT_CACHE_TTL_MS`, `DEFAULT_SLOT_DURATION_MS`, `DEFAULT_LOCALE`, `DEFAULT_DATE_FORMAT`, `DEFAULT_TIME_FORMAT`, `DEFAULT_FIRST_DAY_OF_WEEK`.
|
||||
- ADDED: `BOOKING_CODES` registry via `defineCodes("BOOKING", [...])` and `BookingError extends SuaveplanError<BookingErrorCode>` carrying optional `bookingId`/`holdId`/`mode`/`strategy` discriminants. Codes: `ENGINE_NOT_CONFIGURED`, `BOOKING_NOT_FOUND`, `BOOKING_CONFLICT`, `TRANSITION_NOT_ALLOWED`, `UNKNOWN_CONFLICT_MODE`, `UNKNOWN_AVAILABILITY_STRATEGY`, `HOOK_RETURNED_PROMISE_IN_SYNC`.
|
||||
- ADDED: All public types — `Bookable`, `BookableId`, `BookableQuery`, `BookableQueryResult`, `Booking`, `BookingId`, `BookingModification`, `BookingRequest`, `CancellationResult`, `CancellationTerms`, `HoldRequest`, `WaitlistRequest`, `BookingHold`, `CapacityPool`, `OverbookingStrategy`, `WaitlistEntry`, `BookingSystemConfig` and every sub-type (`TenantConfig`, `EngineConfig`, `StrategyRegistration`, `HoldConfig`, `WaitlistConfig`, `NotificationConfig`, `NotificationTrigger`, `StatusTransition`, `AttributeDefinition`, `BookableTypeDefinition`, `LayoutConfig`, `LocaleConfig`, `AvailabilityConfig`, `ConflictDetectorConfig`, `PricingEngineConfig`), `BookingEvent`, `BookingEventHandler`, `BookingEventType`, `EventSubscription`, `LayoutColorScheme`, `LayoutNode`, `NodePosition`, `BookingPrice`, `DynamicPricingRule`, `PriceModifier`, `PriceRule`, `PriceTier`, `SeasonalRate`, `BookingRule`, `RuleAction`, `RuleCondition`, `RuleEvaluationResult`, `RecurrenceRule`, `TimeSlot`, `TimeWindow`, `BookingHookMap`, `BookingCreatingHookData`, `BookingCancellingHookData`, `BookingCreatingHandler`, `BookingCancellingHandler`, `AvailabilityQuery`, `AvailabilityResult`, `BookingEngineInterface`, `ConflictResult`, `PriceCalculationRequest`, `TransitionRequest`, `TransitionResult`.
|
||||
- ADDED: Subpath exports per feature module (29 subpaths). Tree-shakeable.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- **Property-based tests via fast-check.** Listed in the enterprise audit. The upstream test suite covers every operator, allocation invariant, conflict mode, and pipeline branch at ~99% coverage with example-based tests; reaching 100% on the migrated surface is the gate, and fast-check will be folded in once the deterministic suite is green. Reason: deterministic suite hits the 100% gate first; property tests are an additive quality layer, not a parity requirement.
|
||||
- **OpenTelemetry observability surface (spans/counters/histograms on every public entry point).** Listed in the enterprise audit. Reason: the universal-tier package has no in-process observability today, and adding it requires designing a `LoggerLike`/`MeterLike` dependency injection contract that the upstream package does not have. Belongs to a follow-up `add-booking-observability` change after the enterprise observability story for universal-tier packages stabilises.
|
||||
- **`AbortSignal` propagation through every async path.** The package surface is currently synchronous (the only async path is `Hooks.applyHooksAsync`); upstream does not thread `AbortSignal`. Adding cancellation to a sync surface requires public-API changes outside the parity scope. Reason: behaviour-preserving migration; cancellation is a separate concern.
|
||||
- **Persistence-tier integration (`StorageLike`/`DatabaseLike` backed bookable+booking repositories).** Upstream uses an in-memory `Map` for both. Reason: the parity contract is the in-memory engine; persistence belongs to `@suaveplan/booking-persistence` follow-up that consumes this package.
|
||||
- **Feature-flag-gated rollouts of risky new behaviours.** Listed in the enterprise audit. Reason: the migration adds no new risky behaviours; the typed-error replacement is a strict additive narrowing.
|
||||
- **i18n keys for error messages.** Errors currently carry English strings. Reason: `@suaveplan/error` exposes an i18n resolver hook; wiring booking-specific message keys is a follow-up when the genesis i18n catalog for booking lands.
|
||||
- **Migration to `@suaveplan/events/hooks` once that subpath ships.** Genesis `@suaveplan/events` does not yet expose `/hooks` or `/hooks.types`. The migration ships a small `Hooks<TMap>` implementation inside `src/hooks/` with the same public API; once events ships the hooks subpath, a follow-up change can replace the inline impl with the dependency import without breaking consumers. Reason: dependency unavailability — see §0b parity rule.
|
||||
- **Benchmarks via mitata.** §9 of CLAUDE.md asks for `src/__benchmarks__/`. Reason: deferred — coverage and gates are the primary acceptance criteria; benchmarks land in a follow-up alongside the observability surface.
|
||||
- **`@suaveplan/schema-registry` integration for versioned config schemas.** Listed in the enterprise audit. Reason: schema-registry consumers don't exist on genesis yet for this package; will land when first network-tier consumer surfaces.
|
||||
|
||||
## Source mapping
|
||||
|
||||
This change derives from `../suaveplan/enterprise/booking.md` and the upstream source at `../suaveplan/packages/security/booking/src/`.
|
||||
|
||||
| Requirement | Source lines | Notes |
|
||||
|---|---|---|
|
||||
| Universal-tier purity (zero browser/Node/React deps) | enterprise/booking.md L37; CLAUDE.md cross-cutting | Reinforced here |
|
||||
| Pluggable booking engine pipeline (availability → conflict → pricing → rules → state machine → events) | upstream `src/core/engine/engine.ts` L1-L441 | Verbatim behaviour, typed errors |
|
||||
| Configurable status state machine | upstream `src/core/state-machine/state-machine.ts` L1-L98 | Verbatim |
|
||||
| Typed booking event bus (pub/sub for 15 event types) | upstream `src/core/event-bus/event-bus.ts` L1-L96 | Verbatim |
|
||||
| In-package `Hooks<TMap>` data-transform pipeline | upstream `src/types/booking-hooks.ts` + `@suaveplan/events/hooks` | Migration replaces external dep with in-package impl (see Out of Scope) |
|
||||
| Temporal window operations (overlap, merge, split, duration, contains, adjacent, validity) | upstream `src/temporal/windows/windows.ts` L1-L125 | Verbatim |
|
||||
| Recurrence rule expansion (daily/weekly/monthly/yearly + exceptions + days-of-week) | upstream `src/temporal/recurrence/recurrence.ts` L1-L100 | Verbatim |
|
||||
| Time-slot generation (fixed interval + daily-window helpers + filtering + capacity reduction) | upstream `src/temporal/slots/slots.ts` L1-L94 | Verbatim |
|
||||
| Pluggable conflict-detector registry with four built-in modes | upstream `src/conflict/detector/detector.ts` + `src/conflict/{exact-match,range-overlap,slot-capacity,provider-overlap}/*.ts` | Verbatim, typed errors |
|
||||
| Price calculation pipeline (base → tiers → modifiers → tax → rounding) | upstream `src/pricing/calculator/calculator.ts` L1-L179 | Verbatim |
|
||||
| Seasonal rate resolver (wrap-around year, multiplier, season name lookup) | upstream `src/pricing/seasonal/seasonal.ts` L1-L80 | Verbatim |
|
||||
| Dynamic pricing rules (occupancy / advance days / demand score triggers) | upstream `src/pricing/dynamic/dynamic.ts` L1-L62 | Verbatim |
|
||||
| Rule evaluator with 11 operators and condition aggregation logic (`all`/`any`) | upstream `src/rules/evaluator/evaluator.ts` L1-L153 | Verbatim |
|
||||
| Condition factories (min/max duration, advance booking, quantity range, attribute, weekend, status, customer-limit) | upstream `src/rules/conditions/conditions.ts` L1-L103 | Verbatim |
|
||||
| Pluggable availability strategy registry with three built-ins | upstream `src/availability/resolver/resolver.ts` + `src/availability/strategies/strategies.ts` | Verbatim, typed errors |
|
||||
| Six domain presets returning fully populated `BookingSystemConfig` | upstream `src/domains/shared.ts` + `src/domains/{seats,accommodation,appointments,resources,activities,transport}/*.ts` | Verbatim — defaults preserved |
|
||||
| Zod validation schemas (bookable, booking, booking request, hold request, time window, config) | upstream `src/schemas/booking.schema.ts` + `src/schemas/config.schema.ts` | Verbatim |
|
||||
| Utility helpers (`parseDurationMs`, `clampNumber`, `deepFreeze`, `generateId`, `isNonEmptyString`) | upstream `src/utils/utils.ts` L1-L79 | Verbatim |
|
||||
| Default constants (15 entries) | upstream `src/constants/index.ts` L1-L21 | Verbatim |
|
||||
| Typed error hierarchy `BookingError` extending `SuaveplanError` with stable `BOOKING.*` codes | enterprise/booking.md L43, L61 (cross-cutting "structured errors") | Migration upgrade: replaces every `throw new Error(...)` |
|
||||
| Subpath exports per public module (29 subpaths) | enterprise/booking.md L37 (tree-shakeable) | Expanded from upstream's 28 |
|
||||
|
||||
### Requirements with no source line
|
||||
|
||||
| Requirement | Justification |
|
||||
|---|---|
|
||||
| 100% test coverage (istanbul, all four axes) | Genesis-wide policy (CLAUDE.md §7) — upstream was at ~99% |
|
||||
| Sibling `.md` per implementation file (≥200 words, basic + advanced examples) | Genesis-wide policy (CLAUDE.md §4) — upstream had partial docs |
|
||||
| Test files import runner primitives from `@suaveplan/testing/runner` | Genesis-wide policy (AGENTS.md §7) — upstream imported `vitest` directly |
|
||||
| Explicit named re-exports in every feature-module barrel (no `export *`) | Genesis-wide policy (AGENTS.md §6a) |
|
||||
| Package version starts at `0.1.0` | Genesis-wide policy (CLAUDE.md "Editing") — upstream was `1.0.6` |
|
||||
| In-package `Hooks<TMap>` replaces external `@suaveplan/events/hooks` import | Dependency not migrated on genesis (§0b) — see Out of Scope for the migration path |
|
||||
|
||||
### Parity Checklist
|
||||
|
||||
| Capability | Upstream | Migrated | Notes |
|
||||
|---|---|---|---|
|
||||
| `BookingEngine` full surface (configure, registerBookable, getBookable, listBookables, checkAvailability, getAvailableSlots, createHold, getHold, confirmBooking, cancelBooking, modifyBooking, calculatePrice, joinWaitlist, on, off, getBooking, bookingHooks) | ✅ | ✅ | Verbatim |
|
||||
| `StateMachine` full surface | ✅ | ✅ | Verbatim |
|
||||
| `EventBus` full surface (15 event types) | ✅ | ✅ | Verbatim |
|
||||
| Temporal: windows / recurrence / slots (16 functions total) | ✅ | ✅ | Verbatim |
|
||||
| Conflict: 4 built-in detectors + pluggable registry | ✅ | ⬆️ Enhanced | `Unknown conflict detection mode` raises `BookingError({ code: BOOKING.UNKNOWN_CONFLICT_MODE })` |
|
||||
| Pricing: calculator + seasonal + dynamic (15 functions total) | ✅ | ✅ | Verbatim |
|
||||
| Rules: evaluator (11 operators) + 8 condition factories | ✅ | ✅ | Verbatim |
|
||||
| Availability: 3 built-in strategies + pluggable registry | ✅ | ⬆️ Enhanced | `Unknown availability strategy` raises `BookingError({ code: BOOKING.UNKNOWN_AVAILABILITY_STRATEGY })` |
|
||||
| Domain presets (seats / accommodation / appointments / resources / activities / transport + base) | ✅ | ✅ | Verbatim defaults |
|
||||
| Zod schemas (bookable / booking / booking-request / hold-request / time-window / config) | ✅ | ✅ | Verbatim |
|
||||
| Utils (`parseDurationMs`, `clampNumber`, `deepFreeze`, `generateId`, `isNonEmptyString`) | ✅ | ✅ | Verbatim |
|
||||
| Default constants (15 entries) | ✅ | ✅ | Verbatim |
|
||||
| All public types (~50 type exports) | ✅ | ✅ | Verbatim — every name preserved |
|
||||
| `@suaveplan/events/hooks` dependency | ✅ | ⬆️ Replaced | In-package `Hooks<TMap>` ships under `src/hooks/`; same public surface |
|
||||
| Plain `throw new Error(...)` from boundaries | ✅ | ⬆️ Replaced | All raised errors extend `SuaveplanError` with stable `BOOKING.*` codes |
|
||||
| Subpath exports per public module | ✅ | ⬆️ Enhanced | 29 subpaths (up from upstream 28; adds `./hooks/hooks`) |
|
||||
| Property-based tests (fast-check) | 🚫 upstream (audit asks) | 🚫 deferred | Deterministic suite hits 100% first; fast-check added in follow-up |
|
||||
| OTel observability surface | 🚫 upstream (audit asks) | 🚫 deferred | Requires `MeterLike`/`LoggerLike` DI design; follow-up change |
|
||||
| `AbortSignal` propagation | 🚫 upstream (audit asks) | 🚫 deferred | Sync surface; cancellation belongs to async-rewrite follow-up |
|
||||
| Persistence-tier integration | 🚫 upstream (audit asks) | 🚫 deferred | Belongs to `@suaveplan/booking-persistence` |
|
||||
| Feature flags | 🚫 upstream (audit asks) | 🚫 deferred | No risky new behaviours introduced by this migration |
|
||||
| i18n message keys | 🚫 upstream (audit asks) | 🚫 deferred | Wired when the booking i18n catalog lands |
|
||||
| `@suaveplan/schema-registry` integration | 🚫 upstream (audit asks) | 🚫 deferred | No network-tier consumer yet |
|
||||
| Benchmarks (mitata) | 🚫 upstream (audit asks) | 🚫 deferred | Coverage gate is primary criterion; benches land alongside observability |
|
||||
| `BookingError` discriminants (`bookingId`/`holdId`/`mode`/`strategy`) | 🚫 upstream | ⬆️ Added | Genesis convention for typed errors |
|
||||
|
||||
### Reference docs consulted
|
||||
|
||||
- ✅ `../suaveplan/packages/security/booking/src/` — all 51 implementation files read
|
||||
- ✅ `../suaveplan/packages/security/booking/README.md` — public API documentation
|
||||
- ✅ `../suaveplan/packages/security/booking/package.json` — dep graph + subpath exports (28 entries)
|
||||
- ✅ `../suaveplan/enterprise/booking.md` — enterprise audit, in full
|
||||
- ✅ `../suaveplan/packages/universal/events/src/hooks/` — upstream Hooks impl + types (re-implemented in-package)
|
||||
- 🚫 `../suaveplan/openspec/specs/booking/spec.md` — does not exist (upstream has no formal spec; parity contract is the source itself)
|
||||
- 🚫 `../suaveplan/openspec/changes/*/proposal.md` touching booking — none found beyond `enterprise-booking`
|
||||
|
||||
## Verification approach
|
||||
|
||||
Every requirement in the spec delta carries a `**Verified by:**` line naming the test files that pin its scenarios. The test layout mirrors the source layout (one `<feature>.test.ts` per `<feature>.ts`):
|
||||
|
||||
- `src/error-codes/error-codes.test.ts` — every `BOOKING.*` code reachable from a real call site
|
||||
- `src/hooks/hooks.test.ts` — `Hooks<TMap>` register/unhook/apply/applyAsync/applyWithResult/abort/once/priority/hookCount/hasHook
|
||||
- `src/temporal/windows/windows.test.ts`, `src/temporal/recurrence/recurrence.test.ts`, `src/temporal/slots/slots.test.ts` — temporal operations
|
||||
- `src/conflict/detector/detector.test.ts`, `src/conflict/exact-match/exact-match.test.ts`, `src/conflict/range-overlap/range-overlap.test.ts`, `src/conflict/slot-capacity/slot-capacity.test.ts`, `src/conflict/provider-overlap/provider-overlap.test.ts`
|
||||
- `src/pricing/calculator/calculator.test.ts`, `src/pricing/seasonal/seasonal.test.ts`, `src/pricing/dynamic/dynamic.test.ts`
|
||||
- `src/rules/evaluator/evaluator.test.ts`, `src/rules/conditions/conditions.test.ts`
|
||||
- `src/availability/resolver/resolver.test.ts`, `src/availability/strategies/strategies.test.ts`
|
||||
- `src/domains/shared.test.ts`, `src/domains/{seats,accommodation,appointments,resources,activities,transport}/<domain>.test.ts`
|
||||
- `src/schemas/booking.schema.test.ts`, `src/schemas/config.schema.test.ts`
|
||||
- `src/core/engine/engine.test.ts`, `src/core/state-machine/state-machine.test.ts`, `src/core/event-bus/event-bus.test.ts`
|
||||
- `src/utils/utils.test.ts`
|
||||
|
||||
## Impact
|
||||
|
||||
### Affected dependents
|
||||
|
||||
Run `bunx turbo ls --filter=...@suaveplan/booking` to enumerate.
|
||||
|
||||
- No dependents — this is a new universal-tier leaf package being added to genesis. Every potential downstream (`@suaveplan/r3f-booking`, the `api-plugin-bank`/`api-plugin-banking` plugins) is still in the `Pending` list and will adopt this package when migrated.
|
||||
|
||||
## Migration impact
|
||||
|
||||
None — additive only. No existing genesis package imports `@suaveplan/booking`.
|
||||
308
openspec/archive/2026-05-17-add-booking/specs/booking/spec.md
Normal file
308
openspec/archive/2026-05-17-add-booking/specs/booking/spec.md
Normal file
@@ -0,0 +1,308 @@
|
||||
<!-- markdownlint-disable MD024 -->
|
||||
|
||||
# Capability Spec Delta: booking (add-booking)
|
||||
|
||||
**Package**: `@suaveplan/booking`
|
||||
**Status**: PROPOSED
|
||||
|
||||
> Every requirement below carries a `**Verified by:**` line at authoring time.
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Typed error hierarchy
|
||||
|
||||
The package SHALL expose a `BookingError` class extending `SuaveplanError<BookingErrorCode>` and a `BOOKING_CODES` registry created via `defineCodes("BOOKING", [...])`. Every failure path raised from the package's public surface MUST throw a `BookingError` (never plain `Error`), and the registry MUST cover at minimum: `ENGINE_NOT_CONFIGURED`, `BOOKING_NOT_FOUND`, `BOOKING_CONFLICT`, `TRANSITION_NOT_ALLOWED`, `UNKNOWN_CONFLICT_MODE`, `UNKNOWN_AVAILABILITY_STRATEGY`, `HOOK_RETURNED_PROMISE_IN_SYNC`.
|
||||
|
||||
#### Scenario: every registered code is reachable from a production path
|
||||
|
||||
- **WHEN** the test suite walks every entry in `BOOKING_CODES`
|
||||
- **THEN** at least one production call site raises a `BookingError` carrying that code
|
||||
|
||||
#### Scenario: discriminants narrow on instanceof
|
||||
|
||||
- **WHEN** a caller catches a thrown error and uses `instanceof BookingError`
|
||||
- **THEN** TypeScript narrows `error.code` to `BookingErrorCode` and optional `bookingId`/`holdId`/`mode`/`strategy` discriminants are readable
|
||||
|
||||
**Verified by:** `src/error-codes/error-codes.test.ts::"BOOKING_CODES contains every documented code"` AND `src/error-codes/error-codes.test.ts::"every code is reachable from a production path"` AND `src/error-codes/error-codes.test.ts::"BookingError carries optional discriminants"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: In-package data-transform Hooks pipeline
|
||||
|
||||
The package SHALL ship a `Hooks<TMap extends HookMap>` class under `src/hooks/` that supports registering handlers with priority + once + label options, synchronous and asynchronous chain execution, abort, and detailed result reporting. The synchronous path MUST throw a `BookingError({ code: BOOKING.HOOK_RETURNED_PROMISE_IN_SYNC })` if a handler returns a thenable while running under `applyHooks` (replacing the upstream `console.warn`).
|
||||
|
||||
#### Scenario: handlers transform data in priority order
|
||||
|
||||
- **WHEN** two handlers are registered for `"booking:creating"` with `priority: 10` and `priority: 0`
|
||||
- **THEN** the priority-10 handler runs first and its return value is passed as input to the priority-0 handler
|
||||
|
||||
#### Scenario: returning undefined passes data through
|
||||
|
||||
- **WHEN** a handler returns `undefined`
|
||||
- **THEN** the next handler in the chain receives the data unchanged
|
||||
|
||||
#### Scenario: abort stops the chain
|
||||
|
||||
- **WHEN** a handler calls `context.abort()`
|
||||
- **THEN** no further handlers run and `applyHooksWithResult` reports `aborted: true`
|
||||
|
||||
#### Scenario: once handlers auto-remove
|
||||
|
||||
- **WHEN** a handler is registered with `{ once: true }` and the chain runs once
|
||||
- **THEN** `hookCount` for that hook decreases by one and a second `applyHooks` does not invoke the handler
|
||||
|
||||
#### Scenario: async return inside sync apply is rejected
|
||||
|
||||
- **WHEN** a handler returns a `Promise` from inside `applyHooks` (the sync entry point)
|
||||
- **THEN** a `BookingError({ code: BOOKING.HOOK_RETURNED_PROMISE_IN_SYNC })` is thrown
|
||||
|
||||
**Verified by:** `src/hooks/hooks.test.ts::"runs handlers in priority order"` AND `src/hooks/hooks.test.ts::"returning undefined passes data through"` AND `src/hooks/hooks.test.ts::"context.abort stops the chain"` AND `src/hooks/hooks.test.ts::"once handlers auto-remove"` AND `src/hooks/hooks.test.ts::"sync applyHooks rejects async return"` AND `src/hooks/hooks.test.ts::"applyHooksAsync awaits async handlers"` AND `src/hooks/hooks.test.ts::"hookCount and hasHook reflect registry state"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Temporal window operations
|
||||
|
||||
The package SHALL expose `windowsOverlap`, `windowsOverlapWithBuffer`, `mergeWindows`, `splitWindow`, `windowDurationMs`, `windowContains`, `createWindow`, `isValidWindow`, `windowsAdjacent` with half-open-interval semantics and an optional boundary-turnover flag for `windowsOverlap`.
|
||||
|
||||
#### Scenario: half-open overlap
|
||||
|
||||
- **WHEN** two windows share an exact boundary (`a.endTime === b.startTime`)
|
||||
- **THEN** `windowsOverlap(a, b)` returns `false` (half-open intervals do not overlap at boundaries)
|
||||
|
||||
#### Scenario: buffer-extended overlap
|
||||
|
||||
- **WHEN** `windowsOverlapWithBuffer(proposed, existing, 3600000)` is called and `existing` is within one hour of `proposed`
|
||||
- **THEN** the call returns `true`
|
||||
|
||||
#### Scenario: merge collapses adjacent windows
|
||||
|
||||
- **WHEN** `mergeWindows([w1, w2])` is called and `w1.endTime === w2.startTime`
|
||||
- **THEN** the result contains a single window spanning `w1.startTime` to `w2.endTime`
|
||||
|
||||
**Verified by:** `src/temporal/windows/windows.test.ts::"windowsOverlap respects half-open intervals"` AND `src/temporal/windows/windows.test.ts::"windowsOverlap with boundary turnover excludes touching windows"` AND `src/temporal/windows/windows.test.ts::"windowsOverlapWithBuffer extends existing"` AND `src/temporal/windows/windows.test.ts::"mergeWindows collapses adjacent"` AND `src/temporal/windows/windows.test.ts::"splitWindow produces fixed-interval slices"` AND `src/temporal/windows/windows.test.ts::"windowContains/windowDurationMs/windowsAdjacent/createWindow/isValidWindow behave as documented"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Recurrence expansion
|
||||
|
||||
The package SHALL expose `createRecurrenceRule`, `isRecurring`, `expandRecurrence` covering daily, weekly, monthly, yearly frequencies with `daysOfWeek` filtering, `exceptions` date matching, and a hard 1000-iteration guard.
|
||||
|
||||
#### Scenario: weekly with daysOfWeek filters
|
||||
|
||||
- **WHEN** `expandRecurrence` is given a weekly rule with `daysOfWeek: [1, 3]` over a 3-week window
|
||||
- **THEN** only Monday and Wednesday occurrences appear in the result
|
||||
|
||||
#### Scenario: exception dates exclude matching occurrences
|
||||
|
||||
- **WHEN** the recurrence rule's `exceptions` list contains a date inside the expansion window
|
||||
- **THEN** no occurrence is emitted for that date
|
||||
|
||||
**Verified by:** `src/temporal/recurrence/recurrence.test.ts::"expands daily / weekly / monthly / yearly frequencies"` AND `src/temporal/recurrence/recurrence.test.ts::"weekly daysOfWeek filters"` AND `src/temporal/recurrence/recurrence.test.ts::"exception dates exclude matching occurrences"` AND `src/temporal/recurrence/recurrence.test.ts::"isRecurring and createRecurrenceRule helpers"` AND `src/temporal/recurrence/recurrence.test.ts::"hard iteration guard caps runaway rules"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Time-slot generation
|
||||
|
||||
The package SHALL expose `generateTimeSlots`, `generateDailySlots`, `filterAvailableSlots`, `reduceSlotCapacity`. `generateTimeSlots` only emits slots whose end fits inside the window; `reduceSlotCapacity` floors capacity at zero.
|
||||
|
||||
#### Scenario: slot capacity reduces but cannot go negative
|
||||
|
||||
- **WHEN** `reduceSlotCapacity(slot, slot.capacityRemaining + 5)` is called
|
||||
- **THEN** the returned slot's `capacityRemaining` is `0`
|
||||
|
||||
**Verified by:** `src/temporal/slots/slots.test.ts::"generateTimeSlots emits only fitting slots"` AND `src/temporal/slots/slots.test.ts::"generateDailySlots emits per-day windows"` AND `src/temporal/slots/slots.test.ts::"filterAvailableSlots applies minimum capacity"` AND `src/temporal/slots/slots.test.ts::"reduceSlotCapacity floors at zero"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Conflict-detection registry and four built-in modes
|
||||
|
||||
The package SHALL expose `registerDetector`, `detectConflicts`, `getRegisteredModes`, `clearDetectorRegistry`. `detectConflicts` MUST raise `BookingError({ code: BOOKING.UNKNOWN_CONFLICT_MODE, mode })` when the configured mode is not registered. Four built-ins MUST exist: `detectExactMatch`, `detectRangeOverlap`, `detectSlotCapacity`, `detectProviderOverlap`. `detectRangeOverlap` MUST honour both `bufferDuration` and `allowSameBoundaryTurnover`.
|
||||
|
||||
#### Scenario: unknown mode raises typed error
|
||||
|
||||
- **WHEN** `detectConflicts(proposed, [], { mode: "made-up", … })` is called with no detector registered for `made-up`
|
||||
- **THEN** a `BookingError({ code: BOOKING.UNKNOWN_CONFLICT_MODE, mode: "made-up" })` is thrown
|
||||
|
||||
#### Scenario: range overlap respects boundary turnover
|
||||
|
||||
- **WHEN** `detectRangeOverlap` is called with `allowSameBoundaryTurnover: true` and the existing window ends exactly at the proposed start
|
||||
- **THEN** the result reports no conflict for the boundary-touch case
|
||||
|
||||
#### Scenario: provider overlap honours buffer
|
||||
|
||||
- **WHEN** `detectProviderOverlap` is called with `bufferDuration: "PT15M"` and an existing booking ending 10 minutes before the proposed start
|
||||
- **THEN** the existing booking is reported as conflicting
|
||||
|
||||
**Verified by:** `src/conflict/detector/detector.test.ts::"unknown mode raises BOOKING.UNKNOWN_CONFLICT_MODE"` AND `src/conflict/detector/detector.test.ts::"register/get/clear lifecycle"` AND `src/conflict/exact-match/exact-match.test.ts::"matches only on exact start+end"` AND `src/conflict/range-overlap/range-overlap.test.ts::"detects overlap with buffer"` AND `src/conflict/range-overlap/range-overlap.test.ts::"allowSameBoundaryTurnover excludes boundary-touching"` AND `src/conflict/slot-capacity/slot-capacity.test.ts::"reports conflict when total exceeds capacity"` AND `src/conflict/provider-overlap/provider-overlap.test.ts::"honours bufferDuration"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Pricing pipeline
|
||||
|
||||
The package SHALL expose `calculatePrice`, `calculateBasePrice`, `applyTiers`, `applyModifiers`, `calculateTax` (calculator), `applySeasonalRate`, `calculateSeasonalMultiplier`, `findApplicableRates`, `getSeasonName` (seasonal), `applyDynamicPricing`, `calculateDynamicMultiplier`, `createDynamicContext`, `findApplicableDynamicRules` (dynamic). `calculateBasePrice` MUST handle `fixed`, `per_unit`, `per_duration`, `tiered`, and unknown pricing models (unknown falls back to per-unit). `applyModifiers` MUST classify `discount` / `surcharge` / `fee` / `tax` modifiers. Rounding MUST honour `round` / `floor` / `ceil` modes.
|
||||
|
||||
#### Scenario: tiered tier picks the bracket matching quantity
|
||||
|
||||
- **WHEN** `applyTiers` is called with tiers `[{min:1,max:5,amount:10},{min:6,max:20,amount:8}]` and quantity 7
|
||||
- **THEN** the result is `7 * 8 = 56`
|
||||
|
||||
#### Scenario: dynamic rules trigger on threshold
|
||||
|
||||
- **WHEN** `findApplicableDynamicRules` is called with `occupancyRate: 0.9` and a rule `{triggerType:"occupancy_threshold", threshold:0.8, multiplier:1.5}`
|
||||
- **THEN** the rule appears in the result
|
||||
|
||||
#### Scenario: seasonal wrap-around-year window
|
||||
|
||||
- **WHEN** `findApplicableRates` is called with a season `Dec 1 → Feb 28` and a window in January
|
||||
- **THEN** the season is reported as applicable
|
||||
|
||||
**Verified by:** `src/pricing/calculator/calculator.test.ts::"pipeline runs base → tiers → modifiers → tax → rounding"` AND `src/pricing/calculator/calculator.test.ts::"per_duration uses unitDuration"` AND `src/pricing/calculator/calculator.test.ts::"applyModifiers classifies discount/surcharge/fee/tax"` AND `src/pricing/calculator/calculator.test.ts::"rounding honours round/floor/ceil"` AND `src/pricing/seasonal/seasonal.test.ts::"wrap-around-year season matches January window"` AND `src/pricing/seasonal/seasonal.test.ts::"getSeasonName returns first applicable"` AND `src/pricing/dynamic/dynamic.test.ts::"occupancy_threshold / advance_days / demand_score triggers"` AND `src/pricing/dynamic/dynamic.test.ts::"applyDynamicPricing multiplies"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Rule evaluator and condition factories
|
||||
|
||||
The package SHALL expose `evaluateRule`, `evaluateRules` (`all` / `any` logic, priority sort, modifier application, deny action), and eight built-in condition factories (`createMinDurationCondition`, `createMaxDurationCondition`, `createAdvanceBookingCondition`, `createQuantityRangeCondition`, `createAttributeCondition`, `createWeekendCondition`, `createStatusCondition`, `createCustomerLimitCondition`). The evaluator MUST support 11 operators: `eq`, `neq`, `gt`, `gte`, `lt`, `lte`, `in`, `not_in`, `between`, `regex`, and an unknown-operator default returning `false`.
|
||||
|
||||
#### Scenario: deny action collects failed rule
|
||||
|
||||
- **WHEN** an enabled rule with `actionRef.actionType: "deny"` matches its conditions
|
||||
- **THEN** `evaluateRules` reports `passed: false` and includes the rule in `failedRules`
|
||||
|
||||
#### Scenario: any vs all condition logic
|
||||
|
||||
- **WHEN** `evaluateRule` is called with `conditionLogic: "any"` and at least one condition matches
|
||||
- **THEN** the rule evaluates true even when other conditions fail
|
||||
|
||||
**Verified by:** `src/rules/evaluator/evaluator.test.ts::"11 operators each return documented result"` AND `src/rules/evaluator/evaluator.test.ts::"any vs all condition logic"` AND `src/rules/evaluator/evaluator.test.ts::"deny action collects failed rule"` AND `src/rules/evaluator/evaluator.test.ts::"apply_modifier action records modifierRef"` AND `src/rules/conditions/conditions.test.ts::"every factory produces the documented condition shape"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Availability resolver registry and three built-in strategies
|
||||
|
||||
The package SHALL expose `registerAvailabilityStrategy`, `resolveAvailability`, `getRegisteredStrategies`, `clearStrategyRegistry`. `resolveAvailability` MUST raise `BookingError({ code: BOOKING.UNKNOWN_AVAILABILITY_STRATEGY, strategy })` when the strategy id is not registered. Three built-ins MUST exist: `exactMatchStrategy`, `rangeOverlapStrategy`, `capacityStrategy`.
|
||||
|
||||
#### Scenario: unknown strategy raises typed error
|
||||
|
||||
- **WHEN** `resolveAvailability("nope", bookable, query, [])` is called with no `nope` strategy registered
|
||||
- **THEN** a `BookingError({ code: BOOKING.UNKNOWN_AVAILABILITY_STRATEGY, strategy: "nope" })` is thrown
|
||||
|
||||
#### Scenario: capacity strategy honours quantity request
|
||||
|
||||
- **WHEN** `capacityStrategy` is called with overlapping bookings totalling 95 and a request quantity 10
|
||||
- **THEN** the result reports `available: false` and `remainingCapacity: 5`
|
||||
|
||||
**Verified by:** `src/availability/resolver/resolver.test.ts::"unknown strategy raises BOOKING.UNKNOWN_AVAILABILITY_STRATEGY"` AND `src/availability/resolver/resolver.test.ts::"register/get/clear lifecycle"` AND `src/availability/strategies/strategies.test.ts::"exactMatchStrategy / rangeOverlapStrategy / capacityStrategy behave as documented"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Six domain presets
|
||||
|
||||
The package SHALL expose `createBaseConfig` plus six preset factories — `createSeatConfig`, `createAccommodationConfig`, `createAppointmentConfig`, `createResourceConfig`, `createActivityConfig`, `createTransportConfig` — each returning a fully populated `BookingSystemConfig` whose `engineRef.domainPreset` matches the preset name, whose `conflictRef.mode` matches the upstream default, and whose `bookableTypeRegistry` includes the upstream required/optional attributes verbatim.
|
||||
|
||||
#### Scenario: overrides win over preset defaults
|
||||
|
||||
- **WHEN** `createSeatConfig("t", "T", { engineRef: { ...defaults, maxBookingsPerCustomer: 5 } })` is called
|
||||
- **THEN** the returned config's `engineRef.maxBookingsPerCustomer` is `5`
|
||||
|
||||
**Verified by:** `src/domains/shared.test.ts::"createBaseConfig populates every field with documented defaults"` AND `src/domains/seats/seats.test.ts::"createSeatConfig sets domainPreset and exact_match mode"` AND `src/domains/accommodation/accommodation.test.ts::"createAccommodationConfig sets range_overlap with PT4H buffer"` AND `src/domains/appointments/appointments.test.ts::"createAppointmentConfig sets provider_overlap with PT15M buffer"` AND `src/domains/resources/resources.test.ts::"createResourceConfig sets slot_capacity"` AND `src/domains/activities/activities.test.ts::"createActivityConfig sets slot_capacity and enableWaitlist"` AND `src/domains/transport/transport.test.ts::"createTransportConfig sets exact_match and enableWaitlist"` AND `src/domains/shared.test.ts::"overrides win over preset defaults"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Zod schemas at validation boundaries
|
||||
|
||||
The package SHALL expose `bookableSchema`, `bookingSchema`, `bookingRequestSchema`, `holdRequestSchema`, `timeWindowSchema`, `validateBookable`, `validateBooking`, `validateBookingRequest`, `bookingSystemConfigSchema`, `validateConfig`, `safeValidateConfig`. `validate*` MUST throw on invalid input; `safeValidateConfig` MUST return a `{success, data|error}` discriminated result.
|
||||
|
||||
#### Scenario: validateBooking rejects malformed datetime
|
||||
|
||||
- **WHEN** `validateBooking` is called with `windowRef.startTime` set to an invalid date string
|
||||
- **THEN** a Zod parse error is thrown
|
||||
|
||||
#### Scenario: safeValidateConfig returns success false on bad shape
|
||||
|
||||
- **WHEN** `safeValidateConfig({})` is called
|
||||
- **THEN** the result is `{ success: false, error: ZodError }`
|
||||
|
||||
**Verified by:** `src/schemas/booking.schema.test.ts::"validates well-formed booking, bookable, request, hold"` AND `src/schemas/booking.schema.test.ts::"validateBooking rejects malformed datetime"` AND `src/schemas/config.schema.test.ts::"validates a populated BookingSystemConfig"` AND `src/schemas/config.schema.test.ts::"safeValidateConfig returns success:false on bad shape"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Booking engine orchestration
|
||||
|
||||
The package SHALL expose `BookingEngine<TBookableMeta, TBookingMeta>` and `createBookingEngine` factory. The engine MUST throw `BookingError({ code: BOOKING.ENGINE_NOT_CONFIGURED })` when any orchestration method is called before `configure`; MUST throw `BookingError({ code: BOOKING.BOOKING_NOT_FOUND, bookingId })` when modifying or cancelling an unknown id; MUST throw `BookingError({ code: BOOKING.BOOKING_CONFLICT, bookingId? })` when `confirmBooking` finds overlapping bookings; MUST run `booking:creating` and `booking:cancelling` hooks through the in-package `Hooks<BookingHookMap>` pipeline; and MUST emit lifecycle events through the `EventBus` (`hold:created`, `booking:confirmed`, `booking:cancelled`, `booking:modified`, `waitlist:joined`).
|
||||
|
||||
#### Scenario: orchestration before configure throws
|
||||
|
||||
- **WHEN** `engine.registerBookable(b)` is called before `engine.configure(cfg)`
|
||||
- **THEN** a `BookingError({ code: BOOKING.ENGINE_NOT_CONFIGURED })` is thrown
|
||||
|
||||
#### Scenario: confirmBooking surfaces conflict as typed error
|
||||
|
||||
- **WHEN** `engine.confirmBooking(req)` is called and an overlapping booking exists
|
||||
- **THEN** a `BookingError({ code: BOOKING.BOOKING_CONFLICT })` is thrown
|
||||
|
||||
#### Scenario: hook transforms reach the persisted booking
|
||||
|
||||
- **WHEN** a `booking:creating` hook lowercases `customerRef` and `engine.confirmBooking(req)` runs
|
||||
- **THEN** the persisted `Booking.customerRef` reflects the lowercased value
|
||||
|
||||
#### Scenario: cancellation respects the state machine
|
||||
|
||||
- **WHEN** `engine.cancelBooking` is called for a booking whose status has no `→ cancelled` transition allowed for `customer`
|
||||
- **THEN** a `BookingError({ code: BOOKING.TRANSITION_NOT_ALLOWED, bookingId })` is thrown
|
||||
|
||||
**Verified by:** `src/core/engine/engine.test.ts::"orchestration before configure raises BOOKING.ENGINE_NOT_CONFIGURED"` AND `src/core/engine/engine.test.ts::"confirmBooking surfaces conflict as BOOKING.BOOKING_CONFLICT"` AND `src/core/engine/engine.test.ts::"cancelBooking raises BOOKING.BOOKING_NOT_FOUND for missing id"` AND `src/core/engine/engine.test.ts::"cancelBooking enforces state machine and raises BOOKING.TRANSITION_NOT_ALLOWED"` AND `src/core/engine/engine.test.ts::"hook transforms reach the persisted booking"` AND `src/core/engine/engine.test.ts::"hold and booking lifecycle emit events"` AND `src/core/engine/engine.test.ts::"checkAvailability/getAvailableSlots/listBookables behave as documented"` AND `src/core/engine/engine.test.ts::"calculatePrice rounds per pricingRef config"` AND `src/core/engine/engine.test.ts::"joinWaitlist appends entries"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Status state machine
|
||||
|
||||
The package SHALL expose `StateMachine` and `createStateMachine`. `canTransition` MUST report disallowed cases with a message identifying whether the transition is undefined, role-restricted, or requires a reason. `getAvailableTransitions(from, role)` and `getAutoTransitions(from)` MUST filter the configured `StatusTransition[]` correctly.
|
||||
|
||||
#### Scenario: requiresReason without reason is disallowed
|
||||
|
||||
- **WHEN** `canTransition({from:"confirmed", to:"cancelled", role:"customer"})` is called for a transition whose `requiresReason: true`
|
||||
- **THEN** the result is `{ allowed: false }` and the message names the missing reason
|
||||
|
||||
**Verified by:** `src/core/state-machine/state-machine.test.ts::"undefined transitions are disallowed"` AND `src/core/state-machine/state-machine.test.ts::"role-restricted transitions are disallowed for wrong role"` AND `src/core/state-machine/state-machine.test.ts::"requiresReason without reason is disallowed"` AND `src/core/state-machine/state-machine.test.ts::"getAvailableTransitions / getAutoTransitions / getAllStatuses filter correctly"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Typed booking event bus
|
||||
|
||||
The package SHALL expose `EventBus` and `createEventBus`. `on` MUST return a unique subscription id, `off` MUST accept either the id or the handler reference, `emit` MUST wrap the payload into a `BookingEvent { eventType, timestamp, tenantId, payload }`, `clear` MUST drop every subscription, and `listenerCount`/`eventTypes` MUST report the current registry.
|
||||
|
||||
#### Scenario: off by id and off by handler both unsubscribe
|
||||
|
||||
- **WHEN** `on(t, h)` returns `id`, then `off(t, id)` is called
|
||||
- **THEN** the handler does not receive subsequent emits, and `listenerCount(t)` is zero
|
||||
|
||||
**Verified by:** `src/core/event-bus/event-bus.test.ts::"emit wraps payload into BookingEvent"` AND `src/core/event-bus/event-bus.test.ts::"off by id removes the subscription"` AND `src/core/event-bus/event-bus.test.ts::"off by handler removes the subscription"` AND `src/core/event-bus/event-bus.test.ts::"clear drops every subscription"` AND `src/core/event-bus/event-bus.test.ts::"listenerCount and eventTypes report registry state"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Utility helpers
|
||||
|
||||
The package SHALL expose `parseDurationMs` parsing ISO-8601 durations (`P…D…T…H…M…S`), `clampNumber`, `deepFreeze` (recursive), `generateId` (prefix-suffix base36), `isNonEmptyString`. `parseDurationMs("")` MUST return `0`. `deepFreeze` MUST recurse into nested objects.
|
||||
|
||||
#### Scenario: parseDurationMs handles compound duration
|
||||
|
||||
- **WHEN** `parseDurationMs("P1DT2H30M")` is called
|
||||
- **THEN** the result is `86400000 + 7200000 + 1800000`
|
||||
|
||||
**Verified by:** `src/utils/utils.test.ts::"parseDurationMs handles empty / date-only / time-only / compound"` AND `src/utils/utils.test.ts::"clampNumber clamps to range"` AND `src/utils/utils.test.ts::"deepFreeze recurses"` AND `src/utils/utils.test.ts::"generateId produces unique prefixed ids"` AND `src/utils/utils.test.ts::"isNonEmptyString narrows correctly"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Tier purity and subpath exports
|
||||
|
||||
The package SHALL be universal-tier (no browser, Node, or React APIs in `src/`) and SHALL ship one subpath export per public feature module so consumers tree-shake. The root barrel `src/index.ts` MUST export every public symbol explicitly (no `export *`); every feature-module `index.ts` MUST use explicit named re-exports.
|
||||
|
||||
#### Scenario: each subpath resolves to the module's built artifacts
|
||||
|
||||
- **WHEN** the build runs and consumers import via a documented subpath (e.g. `@suaveplan/booking/pricing/calculator/calculator`)
|
||||
- **THEN** only that module's bundle is loaded (verified by package.json `exports` map shape and build output)
|
||||
|
||||
**Verified by:** `src/conformance.spec-conformance.test.ts::"every src/<feature>/index.ts uses explicit named exports"` AND `src/conformance.spec-conformance.test.ts::"every public subpath is declared in package.json exports"` AND `src/conformance.spec-conformance.test.ts::"src tree contains no browser/Node-only globals"`
|
||||
|
||||
<!-- markdownlint-enable MD024 -->
|
||||
69
openspec/archive/2026-05-17-add-booking/tasks.md
Normal file
69
openspec/archive/2026-05-17-add-booking/tasks.md
Normal file
@@ -0,0 +1,69 @@
|
||||
# Tasks — Add @suaveplan/booking
|
||||
|
||||
## 1. Spec authoring
|
||||
|
||||
- [x] 1.1 Read `../suaveplan/enterprise/booking.md` end-to-end
|
||||
- [x] 1.2 Read `openspec/specs/booking/spec.md` (does not exist — new package)
|
||||
- [x] 1.3 Read `openspec/AGENTS.md` Sections 13 and 14
|
||||
- [x] 1.4 Draft `proposal.md` from `openspec/templates/proposal.md`
|
||||
- [x] 1.5 Fill in `## Source mapping` table
|
||||
- [x] 1.6 Draft `specs/booking/spec.md` from `openspec/templates/spec-delta.md`
|
||||
- [x] 1.7 Every requirement in the spec delta carries a `**Verified by:**` line naming concrete `it(...)` titles
|
||||
|
||||
## 2. Implementation
|
||||
|
||||
- [x] 2.1 Scaffold `packages/security/booking/` (package.json, tsconfig.lib.json, vite.config.ts, vitest.config.ts, tests/setup/polyfills.ts)
|
||||
- [x] 2.2 Implement `src/error-codes/` — `BOOKING_CODES` registry + `BookingError` class with discriminants
|
||||
- [x] 2.3 Implement `src/types/` — every public type module verbatim (bookable, booking, booking-hooks, capacity, config, events, layout, pricing, rules, temporal)
|
||||
- [x] 2.4 Implement `src/hooks/` — in-package `Hooks<TMap>` with sync/async/result variants + types
|
||||
- [x] 2.5 Implement `src/utils/` and `src/constants/`
|
||||
- [x] 2.6 Implement `src/temporal/` — windows, recurrence, slots
|
||||
- [x] 2.7 Implement `src/conflict/` — detector registry + 4 built-ins
|
||||
- [x] 2.8 Implement `src/pricing/` — calculator, seasonal, dynamic
|
||||
- [x] 2.9 Implement `src/rules/` — evaluator + condition factories
|
||||
- [x] 2.10 Implement `src/availability/` — resolver registry + 3 strategies
|
||||
- [x] 2.11 Implement `src/domains/` — base + 6 preset factories
|
||||
- [x] 2.12 Implement `src/schemas/` — Zod schemas for entities + config
|
||||
- [x] 2.13 Implement `src/core/` — engine, state-machine, event-bus
|
||||
- [x] 2.14 Write root `src/index.ts` with explicit named re-exports
|
||||
- [x] 2.15 Write per-feature `index.ts` barrels with explicit named re-exports
|
||||
|
||||
## 3. Tests
|
||||
|
||||
- [x] 3.1 Unit tests per feature module covering every scenario in the spec delta
|
||||
- [x] 3.2 Spec-conformance: `src/conformance.spec-conformance.test.ts` pinning barrel + subpath + tier-purity invariants
|
||||
- [x] 3.3 Every `it(...)` carries `{ timeout: 5000 }`
|
||||
- [x] 3.4 Every test imports runner primitives from `@suaveplan/testing/runner`
|
||||
|
||||
## 4. Documentation
|
||||
|
||||
- [x] 4.1 Sibling `.md` for every implementation `.ts` (≥200 words, basic + advanced examples, full API reference)
|
||||
- [x] 4.2 README at package root linking every co-located `.md` under `## Modules`
|
||||
- [x] 4.3 Update root `STATUS.md`, `MANIFEST.md`, `MISSING.yml`, `SUMMARY.md` for `@suaveplan/booking`
|
||||
|
||||
## 5. Pre-archive verification (5-gate triple)
|
||||
|
||||
- [x] 5.1 `bun run check` / `bunx biome check`
|
||||
- [x] 5.2 `bunx turbo lint typecheck test --filter=@suaveplan/booking`
|
||||
- [x] 5.3 `bunx turbo test:coverage --filter=@suaveplan/booking` — 100/100/100/100
|
||||
- [x] 5.4 Spec-conformance: every requirement in spec.md has a real test that pins it
|
||||
- [x] 5.5 `bunx turbo build --filter=@suaveplan/booking`
|
||||
|
||||
## 6. Pre-archive 9-row checklist
|
||||
|
||||
- [x] 6.1 Generics threaded (`BookingEngine<TBookableMeta, TBookingMeta>`, `Hooks<TMap>`)
|
||||
- [x] 6.2 Zod schemas wired at every declared boundary
|
||||
- [x] 6.3 Every error code in `BOOKING_CODES` thrown by at least one production path
|
||||
- [x] 6.4 N/A — no strict-mode toggle in this package
|
||||
- [x] 6.5 N/A — OTel observability surface deferred per Out of Scope
|
||||
- [x] 6.6 N/A — no lifecycle dedup primitives in this surface
|
||||
- [x] 6.7 No retroactive tick — every box reflects work done in this change
|
||||
- [x] 6.8 No `export *` in any feature-module barrel
|
||||
- [x] 6.9 No direct `vitest` / `bun:test` imports in test files
|
||||
|
||||
## 7. Archive
|
||||
|
||||
- [x] 7.1 Fold spec delta into `openspec/specs/booking/spec.md`
|
||||
- [x] 7.2 Move `openspec/changes/add-booking/` → `openspec/archive/2026-05-17-add-booking/`
|
||||
- [x] 7.3 Refresh `MANIFEST.md` via `bun scripts/folder-reorg/generate-manifest.ts`
|
||||
- [x] 7.4 Refresh `MISSING.yml` via `bun scripts/generate-missing.ts`
|
||||
Reference in New Issue
Block a user