21 KiB
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>andcreateBookingEngine— the pipeline orchestrator withconfigure,registerBookable,getBookable,listBookables,checkAvailability,getAvailableSlots,createHold,getHold,confirmBooking,cancelBooking,modifyBooking,calculatePrice,joinWaitlist,on,off,getBooking, and thebookingHooksgetter exposing the in-packageHooks<BookingHookMap>. - ADDED:
StateMachineandcreateStateMachine— configurable status-transition state machine withcanTransition,getAvailableTransitions,getAutoTransitions,getAllStatuses.TransitionRequest/TransitionResulttypes preserved. - ADDED:
EventBusandcreateEventBus— typed booking event bus withon/off/emit/clear/listenerCount/eventTypes. - ADDED:
Hooks<TMap extends HookMap>— in-package data-transform pipeline (replaces the upstream@suaveplan/events/hooksdependency that does not exist on genesis). Public surface preserveshook/unhook/unhookAll/applyHooks/applyHooksAsync/applyHooksWithResult/hookNames/hookCount/hasHook. TypesHookMap/HookContext/HookHandler/HookHandlerOptions/HookResult/RegisteredHookre-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/clearDetectorRegistryregistry with four built-ins (detectExactMatch,detectRangeOverlap,detectSlotCapacity,detectProviderOverlap).OverlapResult/ConflictDetectionMode/ConflictDetectorFn/CapacityContexttypes exported. - ADDED: Pricing —
calculatePrice,calculateBasePrice,applyTiers,applyModifiers,calculateTax;applySeasonalRate,calculateSeasonalMultiplier,findApplicableRates,getSeasonName;applyDynamicPricing,calculateDynamicMultiplier,createDynamicContext,findApplicableDynamicRules.PriceCalculationContext/PricingPipelineStep/DynamicPricingContexttypes exported. - ADDED: Rules —
evaluateRule,evaluateRulesevaluator with 11 operators (eq,neq,gt,gte,lt,lte,in,not_in,between,regex, default deny); built-in condition factoriescreateMinDurationCondition,createMaxDurationCondition,createAdvanceBookingCondition,createQuantityRangeCondition,createAttributeCondition,createWeekendCondition,createStatusCondition,createCustomerLimitCondition.EvaluationContext/ConditionEvaluatorFntypes exported. - ADDED: Availability — pluggable
registerAvailabilityStrategy/resolveAvailability/getRegisteredStrategies/clearStrategyRegistrywith three built-ins (exactMatchStrategy,rangeOverlapStrategy,capacityStrategy).AvailabilityStrategyFn/SlotAvailabilitytypes exported. - ADDED: Domain presets —
createBaseConfig,createSeatConfig,createAccommodationConfig,createAppointmentConfig,createResourceConfig,createActivityConfig,createTransportConfig. Domain meta typesSeatBookableMeta,AccommodationBookableMeta,AppointmentBookableMeta,ResourceBookableMeta,ActivityBookableMeta,TransportBookableMetaexported. - 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_CODESregistry viadefineCodes("BOOKING", [...])andBookingError extends SuaveplanError<BookingErrorCode>carrying optionalbookingId/holdId/mode/strategydiscriminants. 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,BookingSystemConfigand 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/MeterLikedependency injection contract that the upstream package does not have. Belongs to a follow-upadd-booking-observabilitychange after the enterprise observability story for universal-tier packages stabilises. AbortSignalpropagation through every async path. The package surface is currently synchronous (the only async path isHooks.applyHooksAsync); upstream does not threadAbortSignal. 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/DatabaseLikebacked bookable+booking repositories). Upstream uses an in-memoryMapfor both. Reason: the parity contract is the in-memory engine; persistence belongs to@suaveplan/booking-persistencefollow-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/errorexposes 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/hooksonce that subpath ships. Genesis@suaveplan/eventsdoes not yet expose/hooksor/hooks.types. The migration ships a smallHooks<TMap>implementation insidesrc/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-registryintegration 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.mdtouching booking — none found beyondenterprise-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— everyBOOKING.*code reachable from a real call sitesrc/hooks/hooks.test.ts—Hooks<TMap>register/unhook/apply/applyAsync/applyWithResult/abort/once/priority/hookCount/hasHooksrc/temporal/windows/windows.test.ts,src/temporal/recurrence/recurrence.test.ts,src/temporal/slots/slots.test.ts— temporal operationssrc/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.tssrc/pricing/calculator/calculator.test.ts,src/pricing/seasonal/seasonal.test.ts,src/pricing/dynamic/dynamic.test.tssrc/rules/evaluator/evaluator.test.ts,src/rules/conditions/conditions.test.tssrc/availability/resolver/resolver.test.ts,src/availability/strategies/strategies.test.tssrc/domains/shared.test.ts,src/domains/{seats,accommodation,appointments,resources,activities,transport}/<domain>.test.tssrc/schemas/booking.schema.test.ts,src/schemas/config.schema.test.tssrc/core/engine/engine.test.ts,src/core/state-machine/state-machine.test.ts,src/core/event-bus/event-bus.test.tssrc/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, theapi-plugin-bank/api-plugin-bankingplugins) is still in thePendinglist and will adopt this package when migrated.
Migration impact
None — additive only. No existing genesis package imports @suaveplan/booking.