Skip to content

feat(booking-engine): add request-first booking workflow - #347

Closed
agustinjch wants to merge 88 commits into
TelivityAI:mainfrom
agustinjch:feat/booking-requests
Closed

feat(booking-engine): add request-first booking workflow#347
agustinjch wants to merge 88 commits into
TelivityAI:mainfrom
agustinjch:feat/booking-requests

Conversation

@agustinjch

Copy link
Copy Markdown
Collaborator

Summary

  • add a separate booking_requests aggregate and the property-level bookingMode: instant | request switch
  • add the three-step guest flow for dates, configurable application questions, and required | optional | disabled saved-card collection
  • add the staff review queue with independent accept/deny and payment actions, including partial saved-card charges, external payments, refunds, retentions, extras, and stay amendments
  • add transactional email, audit/outbox persistence, immutable quote and accepted-price snapshots, tenant scoping, permissions, and end-to-end coverage

Safety and compatibility

  • request submission does not create a reservation, deduct inventory, or charge the guest
  • acceptance revalidates availability and price, is idempotent, and can create the reservation with zero paid
  • all payments are explicitly staff-initiated; there are no automatic captures
  • zero availability remains a waitlist concern
  • existing properties keep bookingMode=instant, and the instant booking/deposit path remains unchanged
  • Stripe card data is handled through SetupIntents; HAIP stores tokenized references only

Verification

  • pnpm test — 2,172 passing tests across 258 files (14 skipped)
  • pnpm build
  • incremental booking-request migration replayed against PostgreSQL with a non-superuser role
  • real Stripe test-mode SetupIntent and request submission verified in the browser with a Stripe test Visa
  • final focused review: no Critical or Important findings

Closes #332

@agustinjch

agustinjch commented Aug 26, 2026

Copy link
Copy Markdown
Collaborator Author

Thanks for the feedback. I have pushed a full remediation pass focused on protecting the default hotel flow while keeping booking requests fully opt-in.

What changed:

  • Fixed Stripe refund reconciliation for instant bookings, including cumulative refunds, negative ledger movements, and folio balance recalculation.
  • Hardened Stripe webhook ownership and correlation. Unrelated Stripe events remain harmless no-ops; malformed or contradictory HAIP-owned events now fail so Stripe can retry them.
  • Added regression coverage proving that instant booking remains the default and request-mode persistence is activated only when a property explicitly enables it.
  • Made payment installments, allocations, refunds, returns, retentions, and webhook consequences idempotent and safe under concurrent execution.
  • Fixed durable webhook failures so unsuccessful delivery remains pending and retryable.
  • Preserved the configured card-collection policy and separated booking-application identity from individual Stripe card-setup attempts.
  • Rejected duplicate ancillary services across request and instant-booking APIs.
  • Fixed dashboard issues involving duplicate financial submissions, realtime cache invalidation, and local processed-at timestamps.
  • Added transactional auditing for booking configuration and stricter currency/minor-unit validation.
  • Replaced the runtime raw SQL introduced by this PR with Drizzle-based queries and schema operations.
  • Hardened the migration and backfill path for concurrent writers and rolling deployments.

Validation completed on head 084380a:

  • 2,235 tests passing across 259 files in the same environment used by CI.
  • Full build and workspace typecheck pass.
  • Lint completes with zero errors.
  • PostgreSQL-backed default-flow and request-mode release gates pass.
  • The final diff contains no newly introduced runtime raw SQL.
  • A complete independent review found no remaining Critical, Important, or Minor issues.

The branch is ready for another review.

@telivity-otaip

Copy link
Copy Markdown
Collaborator

Focused follow-ups from this work are split into separate PRs for review:

Co-authored-by: Agus agustin.jch@gmail.com

@telivity-otaip

Copy link
Copy Markdown
Collaborator

Focused follow-ups from this work are split into separate PRs for review:

Contributor: @agustinjch

@agustinjch

Copy link
Copy Markdown
Collaborator Author

Thanks again for the original vertical slice — it made the full workflow and infrastructure impact visible, and it remains the source of the focused PRs listed above.

My understanding is that #347 should not be merged after #349#353 and #348. Doing so would restore the booking-request implementation and migrations directly into core, duplicate the package work, and reintroduce the migration-prefix conflict with #350. Once the focused PRs are complete, this PR should be closed as superseded.

Before closing it, I would make sure the following generic hardening present here is not lost in the split:

  • generic payment endpoint permissions/safe response protections and their regression tests;
  • accepted-pricing protections in the Folios UI;
  • WebSocket property-room rejoin/recovery after reconnect;
  • the fuller accessible Modal hardening;
  • booking-request migration safety, schema, and remediation tests.

The late financial/Stripe fixes appear materially represented in #348, and the normal instant-booking refund repair is preserved. The items above can be moved into #348 where directly required or into small independent core-hardening PRs.

One administrative detail: #347 currently contains Closes #332. Because this PR will be closed without merging, please move Closes #332 to #348 (or close the RFC manually once the package lands).

With that handoff complete, I am happy for #347 to serve as the historical/source PR and be closed as superseded, with the contribution credit preserved across the split PRs.

cursor Bot pushed a commit that referenced this pull request Aug 27, 2026
…y specs

- Add a flag-OFF default-install regression spec: only core migrations run,
  HAIP_BOOKING_REQUESTS is unset, AppModule boots without the
  booking-requests module/tables, request mode is rejected, and instant
  booking + deposit capture + partial/full refund still work. Runs
  automatically under `pnpm test` (apps/api's normal *.spec.ts glob), so it
  is wired into CI without any workflow changes.
- Extract the ephemeral-database subprocess helpers shared by that spec and
  the existing default-flow regression spec into
  regression-database-utils.ts (createdb/dropdb, sanitized child-process
  errors) instead of duplicating them.
- Port PR #347's booking-request-schema.spec.ts and
  booking-request-migration-safety.spec.ts into the package, scoped to the
  tables/migrations this package now owns (the duplicate push-schema DDL
  those specs cross-checked no longer exists — it moved into this package's
  migrations).
- Port PR #347's booking-request-remediation.postgres.spec.ts, replaying
  migration 0032's SQL directly (instead of through push-schema) since the
  ledger-based migrator can't be re-run against a manually-reverted schema.
  Kept opt-in via BOOKING_REQUEST_REMEDIATION_LIVE_PG like the original.
- Document remaining scope (vertical-slice move, push-schema de-pollution)
  in the package README instead of attempting it in this pass.
- Sync README/test-stats test counts (2211 tests, 263 files).

Co-authored-by: telivity-otaip <telivity-otaip@users.noreply.github.com>
telivity-otaip added a commit that referenced this pull request Aug 28, 2026
* Add opt-in booking-requests package with core seams and UI gates

Introduce @telivityhaip/booking-requests as a deploy-time optional module
(HAIP_BOOKING_REQUESTS=true) with separate migrations, Stripe handler
delegation, and dashboard/booking widget feature flags. Core instant booking
paths stay unchanged when the flag is off.

Includes booking request API/controllers, schema split (0022-0032), email
transport hardening, webhook logicalEventId dedup, and CI release-gate job.

Co-authored-by: telivity-otaip <telivity-otaip@users.noreply.github.com>

* refactor(booking-requests): slim package PR to booking-requests scope

Remove email transport, webhook dedup, shared utils, test-count sync, and
core webhook migration changes that land in separate focused PRs. Document
booking-requests in README packages section and optional enablement steps.

Depends on #349, #350, #351, #352, and #353.

Co-authored-by: telivity-otaip <telivity-otaip@users.noreply.github.com>

* fix(ci): restore core seams, push-schema columns, and README test counts

Re-include prerequisite core changes so typecheck and docker seed pass.
Add booking-requests to CI/Docker builds. Sync README to 2120 tests / 252 files.

Co-authored-by: telivity-otaip <telivity-otaip@users.noreply.github.com>

* fix(ci): add charge amendment columns to push-schema and webhook spec

push-schema now creates adjusts_charge_id and source_key so seed and
docker init succeed. Webhook logicalEventId spec uses reservation.created
from the core WEBHOOK_EVENTS catalog.

Co-authored-by: telivity-otaip <telivity-otaip@users.noreply.github.com>

* fix(docker): lazy-load booking-requests only when feature flag is on

Static imports pulled @telivityhaip/booking-requests into the default demo
image and crashed startup with missing @nestjs/common. Gate the optional
package behind HAIP_BOOKING_REQUESTS and read the flag from core seams.

Co-authored-by: telivity-otaip <telivity-otaip@users.noreply.github.com>

* fix(docker): preload optional booking-requests modules async

Avoid loading @telivityhaip/booking-requests at startup when
HAIP_BOOKING_REQUESTS is off (docker demo). Preload before Nest bootstrap
when the flag is enabled; read the flag from core payment seams in
DatabaseModule.

Co-authored-by: telivity-otaip <telivity-otaip@users.noreply.github.com>

* fix(api): type bootstrap cache as DynamicModule array

Co-authored-by: telivity-otaip <telivity-otaip@users.noreply.github.com>

* fix(booking-requests): address Agustin packaging/safety review

- Ship runnable migrator + SQL in dist; production db:migrate via node
- Wire VITE_HAIP_BOOKING_REQUESTS through Docker/compose/release
- Reject bookingMode=request when HAIP_BOOKING_REQUESTS is off
- Ledger + checksum for package migrations (auto-commit for PG enums)
- Drop unused stripeHandlerToken from root module options
- Fix EmailResult outcomeUnknown after #349 status contract
- Point regression/e2e installs at run-migrations.js (post-#350)
- Harden push-schema CLI path resolution; sync README (2175/259)

Co-authored-by: telivity-otaip <telivity-otaip@users.noreply.github.com>

* fix(database): keep logical_event_id out of push-schema baseline

#350 moved webhook logical_event_id to migration 0022. Remove the column
and unique index from push-schema so the pre-0022 upgrade regression
passes again.

Co-authored-by: telivity-otaip <telivity-otaip@users.noreply.github.com>

* chore: sync README test counts to 2180/260

Matches CI after push-schema logical_event_id cleanup (all packages green).

Co-authored-by: telivity-otaip <telivity-otaip@users.noreply.github.com>

* test(booking-requests): flag-off regression gate + port PR #347 safety specs

- Add a flag-OFF default-install regression spec: only core migrations run,
  HAIP_BOOKING_REQUESTS is unset, AppModule boots without the
  booking-requests module/tables, request mode is rejected, and instant
  booking + deposit capture + partial/full refund still work. Runs
  automatically under `pnpm test` (apps/api's normal *.spec.ts glob), so it
  is wired into CI without any workflow changes.
- Extract the ephemeral-database subprocess helpers shared by that spec and
  the existing default-flow regression spec into
  regression-database-utils.ts (createdb/dropdb, sanitized child-process
  errors) instead of duplicating them.
- Port PR #347's booking-request-schema.spec.ts and
  booking-request-migration-safety.spec.ts into the package, scoped to the
  tables/migrations this package now owns (the duplicate push-schema DDL
  those specs cross-checked no longer exists — it moved into this package's
  migrations).
- Port PR #347's booking-request-remediation.postgres.spec.ts, replaying
  migration 0032's SQL directly (instead of through push-schema) since the
  ledger-based migrator can't be re-run against a manually-reverted schema.
  Kept opt-in via BOOKING_REQUEST_REMEDIATION_LIVE_PG like the original.
- Document remaining scope (vertical-slice move, push-schema de-pollution)
  in the package README instead of attempting it in this pass.
- Sync README/test-stats test counts (2211 tests, 263 files).

Co-authored-by: telivity-otaip <telivity-otaip@users.noreply.github.com>

* feat(booking-requests): package-owned port for booking_mode/payment_method_collection/form_questions

Adds BOOKING_REQUEST_CONFIG_FIELDS_PORT + DrizzleBookingRequestConfigFieldsAdapter
so the package owns reading/writing booking_engine_config's request-mode-only
columns via its own Drizzle table fragment, instead of core declaring them.
Wired into BookingRequestModule.forRoot() (global) so it's injectable into
core's BookingEngineConfigService without that module importing this package.

Co-authored-by: telivity-otaip <telivity-otaip@users.noreply.github.com>

* chore(database): remove request-only DDL from core push-schema/drizzle

Removes booking_mode/payment_method_collection/form_questions ALTERs on
booking_engine_config, audit_logs.booking_request_id (+ its timeline index),
and the request-shape unique indexes/checks on payments
(payments_property_request_*_unique, booking_request_parent_positive_check,
booking_request_child_shape_check) from core's push-schema.ts and Drizzle
schema. These are now declared and migrated exclusively by
packages/booking-requests. payments.booking_request_id/idempotency_key,
reservations.accepted_pricing_snapshot, and charges
adjusts_charge_id/source_key stay in core as documented in
push-schema-kept-fields.spec.ts.

Also adds the DRIZZLE injection token to @telivityhaip/database so packages
outside apps/api (booking-requests) can inject the shared Drizzle client
without importing apps/api.

Co-authored-by: telivity-otaip <telivity-otaip@users.noreply.github.com>

* refactor(booking-requests): move Nest vertical slice into package

Own controllers/services/DTOs behind package ports; strip request-only
audit DDL and payment indexes from core schema; keep thin config/payment hooks.

- Nest controllers, DTOs, services, Stripe handler, and pricing/money/
  state/db/ledger/reconciler/template helpers now live in
  packages/booking-requests/src (http/ + domain/), with their unit specs.
  BookingRequestModule.forRoot(...) is a real DynamicModule owning those
  controllers/providers directly, not a facade over apps/api classes.
- apps/api/src/modules/booking-request/ keeps only the e2e, authorization,
  default-flow-regression, flag-off-instant-booking.regression, and
  transaction-seams specs plus regression-database-utils.ts.
- apps/api/src/booking-requests.bootstrap.ts wires every package-local port
  (folio, webhook, email, reservation, rate-plan, guest, ancillary,
  availability, booking-engine, booking-engine-config, plus guard-bridge
  ports) to the concrete core singleton via `useExisting`.
- Guard bridge classes (BookingKeyGuardBridge, BookingEngineScopeGuardBridge,
  BookingThrottleGuardBridge) resolve the "guards landmine": @UseGuards(...)
  is populated from decorator metadata, a separate path from `providers`, so
  a bare useExisting binding on an abstract port class is silently dropped —
  the bridges are real injectable classes referenced in @UseGuards(...).
- DRIZZLE token declaration moved to @telivityhaip/database; apps/api's
  DatabaseModule still @Global-provides it and merges in the package's
  optional schema only when HAIP_BOOKING_REQUESTS is on.
- Relocated pure/shared pieces to packages/shared: SAVED_PAYMENT_METHOD_GATEWAY
  / PAYMENT_GATEWAY / BOOKING_REQUEST_STRIPE_HANDLER interfaces, IsMoneyString,
  canonical calendar date validators, stayDates, AuditActor helpers,
  RequirePermissions/Public decorators, stripe-financial-state helpers, and
  the pure payment-ledger math (remainingCapturedAmount/sumRefundChildren) —
  bookingRequestPaymentSumWhere stays in core payment-ledger.
- Schema de-pollution: removed audit_logs.booking_request_id (+ its timeline
  index) and the request-shape payments unique indexes/checks from core
  push-schema/drizzle; kept booking_engine_config.booking_mode /
  payment_method_collection / form_questions, payments.booking_request_id /
  idempotency_key, reservations.accepted_pricing_snapshot, and charges
  adjusts_charge_id/source_key as thin config/payment hooks core still reads
  directly. packages/database/src/push-schema-kept-fields.spec.ts guards the
  contract; packages/booking-requests declares its own local audit table
  extension for the timeline index it still owns.
- EventEmitterModule.forRoot() re-enabled `wildcard: true` — required for the
  webhook fan-out (@onevent('**')) to receive booking-request events.
- README's "Package boundary" section replaces the old "Remaining work"
  deferrals list with an accurate description of the current split.

Co-authored-by: telivity-otaip <telivity-otaip@users.noreply.github.com>

* fix(docker): ship shared node_modules for Nest peer in prod image

Shared now requires @nestjs/common at runtime after the booking-requests
boundary move; copy packages/shared/node_modules into the API image so
demo smoke can boot. Sync published test counts to 2222/266.

Co-authored-by: telivity-otaip <telivity-otaip@users.noreply.github.com>

* fix(api): load AppModule after booking-requests preload

When HAIP_BOOKING_REQUESTS=true, AppModule evaluates bookingRequestsModules()
at import time; dynamic-import AppModule after preload so flag-on dev/prod
boot does not crash before Nest starts.
@telivity-otaip

Copy link
Copy Markdown
Collaborator

Closing as superseded by #348, which merged to main at e71537b.

The request-first booking workflow ships as the optional @telivityhaip/booking-requests package (flag-gated, real package boundary, Agustin’s packaging review addressed). This PR’s monolithic integration path is no longer the merge target.

Thank you @agustinjch — contributor credit remains in packages/booking-requests/README.md. No further action needed on this PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

RFC: support request-first direct bookings with staff approval

2 participants