Changelog
What's new, straight from the repo.
v0.6.0 — 2026-05-20
Added
- Channel manager: Sprint N foundation complete — abstraction (adapter contract, push pipeline, pull-feed, inbound + confidence scoring, drift detection, dashboard) + CSV-stub adapter prove the round-trip end-to-end; 1195 API tests pass; deferred items tracked at `docs/superpowers/specs/channel-manager-deferred.md`. Real Google Hotel Center, OTA adapters, and GBP land in Sprints N+1 onward.
- Channel manager: Prisma models (Channel, ChannelMapping, RoomTypeChannelMapping, RatePlanChannelMapping, ChannelCredential, OAuthToken, Allocation, ChannelPushJob, ChannelStateSnapshot, ChannelReservation) + 10 enums; 9 hotel-scoped models added to RLS allowlist
- Channel manager: Reservation gains optional `sourceChannelId`, `channelReservationId`, `commissionPct`, `paymentMode` columns
- Channel manager: 6 new permissions (channel.read, channel.admin, channel.credential.write, channel.mapping.write, channel.reservation.review, channel.error.notify); credential.write defaults to OWNER-only
- Channel manager: csv-stub channel row seeded for end-to-end test coverage
- Channel manager: ChannelAdapter interface + supporting types (capabilities, payloads, PushOutcome) — foundation for per-channel adapter modules
- Channel manager: AdapterRegistry for per-channel adapter lookup with duplicate-key and missing-key guards
- Channel manager: ChannelModule wired with 4 BullMQ queues (channel-push, channel-pull, channel-inbound-reservation, oauth-refresh) and AdapterRegistry exported
- Channel manager: ChannelCredentialService — envelope-encrypted credential write/read with SHA256 payload digest for change detection
- Channel manager: OAuthTokenService — envelope-encrypted access + refresh token storage (with per-token IV columns), expiry-window query, and failure counter
- Channel manager: OAuthRefreshWorker — BullMQ processor (concurrency 2) that refreshes tokens expiring within 20 min via `adapter.refreshOAuthToken()`; cron scheduled every 10 min in ChannelModule
- Channel manager: ChannelMappingService — atomic upsert of channel + room-type + rate-plan mappings; missing entries are deactivated (not deleted) for audit
- Channel manager: CsvStubAdapter — first reference adapter, proves push/pull/inbound round-trip end to end via CSV files under /tmp/cnykra-channel-csv-stub/
- Channel manager: CsvStubModule self-registers the adapter on boot
- Channel manager: ChannelPushService — debounced (10s coalesce) enqueue for rate/inventory/restriction with capability filtering
- Channel manager: ChannelPushWorker — adapter dispatch, status persistence, AUTH_EXPIRED retry, DLQ on terminal failure
- Channel manager: domain-event listeners (rate/inventory/restriction changed) on the BullMQ domain-events queue — translate PMS write events into channel push enqueues
- Channel manager: ChannelFeedService.issueToken() rotates tokens (raw shown once, hash persisted on ChannelMapping)
- Channel manager: public pull-feed endpoint GET /channels/:channelKey/feed.:format?hotel=:slug&token=:t — opaque bcrypt-hashed token, 60s Redis cache
- Channel manager: ConfidenceScorer — pure scoring of incoming reservations (HIGH/MEDIUM/LOW + reasons); blocker-wins precedence
- Channel manager: IncomingReservationService — transactional ingest, confidence-based auto-import vs NEEDS_REVIEW, modification + cancellation handling, idempotent on (channelId, externalReservationId)
- Channel manager: public POST /channels/:channelKey/webhook (rawBody-aware) + channel-inbound-reservation BullMQ worker — adapter-verified inbound, idempotent via jobId, calls IncomingReservationService.ingest
- Channel manager: ChannelAdapter.verifyInboundWebhook contract extended with optional `cancellations` field (backward-compatible)
- Channel manager: inbound cancellation routing — CSV-stub adapter parses `# cancel ID reason` rows, webhook controller calls IncomingReservationService.cancel synchronously; response now includes `cancelled` count
- Channel manager: ChannelPullWorker — 15-min cron, calls adapter.pullCurrentState for opt-in channels (syncDirection != OUTBOUND_ONLY + supportsDriftPull), persists ChannelStateSnapshot on diff, emits channel.drift.detected on WARN/CRIT
- Channel manager: DriftDetector — pure diff of CNYKRA local state vs adapter pullCurrentState; INFO/WARN/CRIT severity per rate %, inventory mismatch, stop-sell mismatch
- Channel manager: ChannelController — GET /channels (list with mapping state), POST /channels/:id/enable + disable; permission-gated by channel.read / channel.admin
- Channel manager: ChannelMappingController — GET/PUT /channels/:id/mapping (full detail with room-type + rate-plan rows, pullFeedTokenHash stripped), POST /channels/:id/feed-token (raw token shown once)
- Channel manager: ChannelDriftController — GET /channels/drift (open or resolved snapshots), POST /channels/drift/:id/resolve (KEEP_CNYKRA / ACCEPT_CHANNEL / IGNORED; records action, write-back deferred to a later sprint)
- Channel manager: ChannelSyncHealthController — GET /channels/sync-health (filterable by status), POST /channels/:jobId/retry (re-enqueues with exponential backoff, resets status to PENDING)
- Channel manager: ChannelCredentialController — PUT /channels/:id/credentials (adapter-validated), GET /channels/:id/oauth/authorize-url, public GET /channels/oauth/callback (handles code exchange, stores ChannelCredential + OAuthToken)
- Channel manager: ChannelReservationController — GET /channel-reservations (filterable by status), GET /:id, POST /:id/{import,reject,snooze}; permission-gated by channel.read / channel.reservation.review
- Dashboard: /channel-manager top-level route with 5-tab layout (Channels, Mapping, Sync Health, Drift, Inbox); sidebar entry gated by channel.read
- Dashboard: Channels tab — card grid showing channel status (connected/not), inventory mode, sync direction, last push; Connect/Disable actions gated by channel.admin
- Dashboard: Mapping tab — per-channel editor for inventory mode, sync direction, safety buffer, room-type and rate-plan mappings; saves via PUT /api/channels/:id/mapping
- Dashboard: Sync Health tab — filter ChannelPushJob rows by status, retry FAILED/DLQ jobs (channel.admin gated)
- Dashboard: Drift tab — open/resolved snapshot views; per-snapshot diff table + accept-channel / keep-CNYKRA / ignore actions (channel.admin gated)
- Dashboard: Channel manager Inbox tab — review NEEDS_REVIEW reservations, see confidence reasons + normalized + raw payload, Import/Reject (with reason)/Snooze 24h (channel.reservation.review gated)
- Tests: end-to-end CSV-stub integration spec — outbound rate push → worker → CSV file written; inbound CSV webhook → ingestion worker → ChannelReservation row (covers the abstraction end-to-end in Sprint N)
- Design Tokens: `@cnykra/design-tokens` package — brand palette (dark-canvas marketing colors, brand blue #2541F5), typography scale (9 steps from displayXl 80px to monoMicro 12px), spacing scale (4px base), and radii; all token objects frozen; consumed by apps/marketing and planned dashboard refactor.
- Design Tokens: `tokens.css` Tailwind v4 `@theme inline` block exposing `--color-*`, `--font-*`, and `--radius-*` custom properties for app stylesheets to consume alongside `@import "tailwindcss"`.
- Marketing: scaffold `apps/marketing` workspace (Astro 4 + React + Tailwind v4) — foundation for cnykra.com apex marketing site.
- Marketing: Astro config with Cloudflare Pages adapter, MDX, sitemap, and Tailwind v4 vite plugin.
- Marketing: self-host Geist, Geist Mono, and Instrument Serif fonts as woff2 — no Google Fonts runtime dependency.
- Marketing: global stylesheet (`src/styles/global.css`) wiring Tailwind v4 + `@cnykra/design-tokens` + tw-animate-css + dark canvas defaults, reduced-motion safe.
- Marketing: `BaseLayout.astro` with meta tags, canonical, OG / Twitter Card, font preloads, and Cloudflare Web Analytics beacon gated to prod.
- Marketing: brand logo (blue/ink/white variants) and favicon copied from dashboard for consistent identity.
- Marketing: `Logo`, `Nav` (desktop), `MobileDrawer` (no-JS `<details>`), and sticky `Header` components.
- Marketing: `Footer` with 4 columns (Product / Solutions / Resources / Company), verified Cnykra Technologies business address, 6 legal links, and trademark disclosure.
- Marketing: Vitest config + Astro Container API snapshot tests for Header and Footer (8 tests guarding business address, trademark line, 4 columns, 9 product links, 6 legal links).
- Marketing: Astro content collections (`blog`, `products`, `legal`) with Zod-validated frontmatter; legal carries `legalReviewStatus` enum (draft / in_review / cleared) for the Phase 5 lawyer-review gate.
- Marketing: placeholder pages — home (`/`, hero per spec H1), product (`/products/property-management`), legal (`/legal/privacy`, noindex), 404, 500. Full content lands in Phases 3 and 5.
- Marketing: ESLint flat config (`eslint-plugin-astro` + `eslint-plugin-jsx-a11y`) — lint passes clean.
- Marketing: `wrangler.toml` for the `cnykra-marketing` Cloudflare Pages project, `_redirects` placeholder, and Lighthouse-CI config asserting Performance/SEO/A11y/Best-Practices ≥ 0.95 on home, product, and legal pages.
- Marketing CI/CD: `deploy-dev-marketing.yml` + `deploy-live-marketing.yml` workflows running typecheck → lint → test → build → Lighthouse-CI → Cloudflare Pages deploy on every push (live workflow also purges Cloudflare cache).
- Web: Resort theme bundle skeleton registered in the theme registry (placeholders re-use Classic components until task-by-task Resort implementation lands)
- Web: Resort theme — SharedNav (tropical-green bg, 40px logo, pill-shaped white CTA), SharedFooter (tropical-green bg, sand text, pill-shaped social link buttons)
- Web: Resort theme — HeroSection with subtle bottom-to-top white gradient overlay (no dark wash), text bottom-left anchored, pill-shaped CTA with soft shadow
- Web: Resort theme — AboutSection (centered, airy spacing), RoomsSection (20px-radius soft-shadow cards, coral "from" labels, pill Book CTA), AmenitiesSection (24px-radius shadowed tiles)
- Web: Resort theme — GallerySection (20px-radius soft-shadow tiles), TestimonialsSection (16px-radius cards with coral stars), LocationSection (20px-radius framed map with soft shadow)
- Web: Resort theme — ContactSection (pill-shaped contact buttons with soft shadow), CtaSection (full-bleed brand block with pill CTA)
- Web: Resort theme — BookPage, CheckoutPage, ConfirmationPage, UnavailablePage (functional logic preserved from Classic; Resort styling — pill-shaped CTAs, soft-shadow cards, sage/coral accents)
- Web: `?theme=<id>` URL search-param override in `useThemeId()` — lets the dashboard preview and the thumbnail-capture script switch themes without saving; runtime-guarded by `isThemeId`
- Web: `scripts/capture-theme-thumbnails.mjs` — Playwright-based script to regenerate the dashboard's theme picker thumbnails by screenshotting each theme at a real hotel's landing page
- Web: Resort theme fully active — registered in registry, all 9 sections + 4 booking pages + shared nav/footer implemented; registry assertion test covers Resort
- Dashboard: Resort theme card in `/website?tab=branding` is now selectable. All four themes (Classic, Modern, Boutique, Resort) are now live.
Changed
- WhatsApp: GuestMatcherService is now exported from WhatsAppModule so the channel module can reuse it for inbound reservation guest-matching
- PMS: rate / restriction / stop-sale writes now emit domain events (rate.changed, restriction.changed) consumed by ChannelModule listeners; delete writes emit with empty perNightAmounts / perDate to signal "cleared for this range"; ranges over 365 days emit dateRange only (empty per-date array) — adapters must handle both forms
- Channel sync: inventory.changed emission is deferred — no pure availability model exists yet (CNYKRA derives availability from Room + Reservation); pipeline scaffold remains in place
- Schema: OAuthToken gains encryptedAccessTokenIv + encryptedRefreshTokenIv columns (AES-GCM IV stored alongside ciphertext, matching ChannelCredential pattern)
- Schema: ChannelCredential gains encryptedIv (missed in spec; AES-GCM requires per-encryption IV alongside ciphertext)
- Crypto: EnvelopeCryptoService can be constructed with an alternate master-key env var; channel module gets its own CHANNEL_CREDENTIAL_MASTER_KEY-backed instance via the 'CHANNEL_ENVELOPE_CRYPTO' provider token
- Design Tokens: typography export now preserves literal key types (autocomplete-friendly); postinstall now builds dist/ so consumers find `@cnykra/design-tokens` immediately after `npm ci`.
- Marketing: Astro `output` switched from `static` to `hybrid` and removed the Sharp image service config — required for the `@astrojs/cloudflare` adapter (will let future `/api/*` Pages Functions live alongside prerendered marketing pages).
- Marketing: `@astrojs/sitemap` integration removed from astro.config — crashes in `astro:build:done` under hybrid+Cloudflare. Sitemap reintroduction is a Phase 6 (SEO) concern.
- Marketing: `typecheck` npm script simplified to `astro check` only — the `&& tsc --noEmit` tail was redundant and broke on `.astro` imports from `.ts` test files (plain tsc has no Astro module plugin).
- DevDeps: added `playwright` at the repo root (only used by the thumbnail-capture script; omitted from Docker production builds via `npm ci --omit=dev`)
Fixed
- Channel manager: domain-event listeners now consume the EventBusService payload envelope ({type, hotelId, payload}) instead of expecting the raw payload shape — aligns with repo's existing event-bus convention
- Channel manager: ChannelReservation.channelId now has a foreign key to Channel.id (consistency with ChannelMapping/ChannelCredential); env validator rejects too-long master keys, empty-string GOOGLE_OAUTH_*, removes dead try/catch
Security
- Env: new required var `CHANNEL_CREDENTIAL_MASTER_KEY` (32-byte base64) for envelope-encrypted channel credentials
v0.5.0 — 2026-05-19
Added
- Web: Boutique theme bundle skeleton registered in the theme registry (placeholders re-use Classic components until task-by-task Boutique implementation lands)
- Web: Boutique theme — SharedNav (warm sand bg, 44px logo, terracotta button) and SharedFooter (inky bg, italic display brand name, terracotta accent line, uppercase letterspaced social links)
- Web: Boutique theme — HeroSection with image-LEFT / text-RIGHT 50/50 split layout (collapses to vertical stack on <768px), serif italic subhead in terracotta, "Learn more" text-link below CTA
- Web: Boutique theme — AboutSection, RoomsSection (no-radius hardline cards with terracotta "from" labels), AmenitiesSection (grid of bordered cells, hairline divider style)
- Web: Boutique theme — GallerySection (alternating tall/wide editorial tiles), TestimonialsSection (italic pull-quotes with hairline-rule top), LocationSection (1.5fr map weighting + framed border)
- Web: Boutique theme — ContactSection (oversized serif anchor links, italic email accent), CtaSection (italic "Reserve your stay" eyebrow, full-bleed brand block with serif headline)
- Web: Boutique theme — BookPage, CheckoutPage, ConfirmationPage, UnavailablePage (functional logic preserved from Classic; Boutique editorial styling — serif display, hard-edged inputs and 2px-radius CTAs, terracotta brand fallback)
- Web: Boutique theme fully active — registered in registry, all 9 sections + 4 booking pages + shared nav/footer implemented; CSS-counter numbered section headings ("01 — Rooms"); registry assertion test covers Boutique
- Dashboard: Boutique theme card in `/website?tab=branding` is now selectable (Resort remains disabled until Plan 4)
- Web: Modern theme fully active — registered in registry, all 9 sections + 4 booking pages + shared nav/footer implemented; registry assertion test covers Modern
- Dashboard: Modern theme card in `/website?tab=branding` is now selectable (Boutique/Resort remain disabled until Plans 3-4)
- Web: Modern theme — HeroSection with oversized sans-serif headline (clamp(40px, 8vw, 96px)) and brand-colour-block fallback when no background image is set
- Web: Modern theme — AboutSection, RoomsSection (bordered cards, no shadow), AmenitiesSection (larger icon scale)
- Web: Modern theme — SharedNav (sticky with backdrop-blur, 28px logo) and SharedFooter (near-black, oversized brand display, social link row)
- Web: Modern theme — GallerySection (taller tiles), TestimonialsSection (bordered cards), LocationSection (12px-radius map)
- Web: Modern theme — ContactSection (oversized type, link-style entries), CtaSection (full-bleed brand block with clamp-scaled headline)
- Web: Modern theme — BookPage, CheckoutPage, ConfirmationPage, UnavailablePage (functional logic preserved from Classic; Modern visual styling applied uniformly across forms, buttons, cards)
- Dashboard: inbox context panel (guest + active reservation summary) + SSE handlers for whatsapp.* event types (real-time list/detail/status updates)
- Dashboard: inbox list + detail + message bubbles + reply composer + 24-hour-window banner. Optimistic send insertion; status-icon updates via TanStack invalidation.
- Dashboard: `use-whatsapp.ts` hooks for account, conversations (infinite), single conversation + context, send/claim/unclaim/resolve/reopen/snooze/mark-read/link-guest mutations
- Dashboard: new top-level `/inbox` route + sidebar entry, both gated by `whatsapp.inbox.read`. Three-pane shell scaffolded; data wiring follows.
- Crypto: `EnvelopeCryptoService` (AES-256-GCM) for envelope-encrypting per-hotel secrets; new `CryptoModule` registered in `app.module`. New env var `WHATSAPP_TOKEN_MASTER_KEY` (32-byte base64).
- Auth (WhatsApp inbox Sprint 1): 8 new permissions in the catalog — 7 WhatsApp (`whatsapp.account.read`, `whatsapp.account.admin_connect`, `whatsapp.account.delete`, `whatsapp.inbox.read`, `whatsapp.inbox.reply`, `whatsapp.inbox.assign_self`, `whatsapp.inbox.assign_any`) plus a generic `reservation.read`. `INBOX_AGENT` role added to `guest.read` defaults. `INBOX_AGENT` also added to the `UserRole` TypeScript union and the `DEFAULTS_BY_ROLE` lookup.
- Schema (WhatsApp inbox Sprint 1): `WhatsAppAccount`, `Conversation`, `Message` models + `ConversationStatus`/`MessageDirection`/`MessageType`/`MessageStatus` enums; new `INBOX_AGENT` value on `UserRole`
- WhatsApp: `WhatsAppAccountResolver` — resolves per-hotel WhatsApp credentials with env-var platform fallback. Also exposes `resolveHotelByPhoneNumberId` for inbound webhook routing.
- WhatsApp: `WhatsAppMetaClient` — typed wrapper over Meta Graph API v21.0 for send-text, media URL fetch, and media download. `MetaApiError` exposes Meta's error code, message, and raw payload.
- WhatsApp: `GuestMatcherService` — normalizes E.164 phone strings and matches conversations to existing `Guest` records on creation; returns null on no-match or multi-match (ambiguous).
- WhatsApp: `WhatsAppModule` skeleton with `GET/DELETE /whatsapp/account` and `POST /whatsapp/account/admin-connect` (env-flag gated `WHATSAPP_ADMIN_CONNECT_ENABLED`) for Sprint 1 hotel onboarding
- WhatsApp: `GET /whatsapp/webhook` verification handshake + `POST /whatsapp/webhook` with `X-Hub-Signature-256` HMAC check; valid payloads enqueue to the `whatsapp-inbound` BullMQ queue
- WhatsApp: `WhatsAppInboundWorker` — BullMQ consumer that dedupes by `wamid`, resolves hotel by `phone_number_id`, persists conversations + messages, broadcasts SSE events, and enqueues push notifications (text + status callbacks; media in next commit)
- WhatsApp: inbound media (image/document/audio/video/sticker) downloaded from Meta, stored to R2 (`whatsapp/{hotelId}/{wamid}.{ext}`), and served via `GET /whatsapp/media/:messageId` signed-URL proxy
- WhatsApp: `WhatsAppService.sendText` + `POST /whatsapp/conversations/:id/messages` — outbound text via Meta Cloud API, optimistic PENDING → SENT/FAILED, conversation SNOOZED→OPEN on send
- WhatsApp: conversation lifecycle endpoints — claim/unclaim/assign/resolve/reopen/snooze/link-guest/mark-read, all gated by per-permission and per-hotel checks; concurrent-claim race guarded by predicate `updateMany`
- WhatsApp: `GET /whatsapp/conversations` (filterable by status/assignee/q with cursor pagination), `GET /whatsapp/conversations/:id` (paginated message history), `GET /whatsapp/conversations/:id/context` (linked guest + active reservation summary)
- WhatsApp: push-notification routing — claimed conversations notify the assignee only; unassigned notify all inbox-permissioned staff; new event types `whatsapp_new_conversation` and `whatsapp_new_message`
- Web: Modern theme bundle skeleton registered in the theme registry (placeholders re-use Classic components until task-by-task Modern implementation lands)
- Dashboard: `Theme` card at the top of `/website?tab=branding` — 4-card grid; Classic selectable; Modern/Boutique/Resort visible but disabled until their implementation plans land. Click-to-select with optimistic UI and toast on save.
- Web: theme registry assertion test catches missing-export bugs at CI time (currently asserts Classic; Plans 2-4 expand coverage as each theme registers)
- Web: theme registry + `ThemeBundle` interface in `apps/web/src/themes/` (foundation for per-theme renderers)
- Web: `useThemeId()` hook reads the active theme from `useHotelInfo()` with a `'classic'` fallback for unknown themes
- Schema: `HotelWebsiteConfig.theme` column (default `'classic'`) for the website theme picker
- API: `HotelWebsiteService` accepts and returns the new `theme` field; 4 specs added covering valid persistence, default, untouched neighbours, and getConfig surfacing
- Types: `THEMES` const + `ThemeId` type + `isThemeId` guard in `@cnykra/types`
- Schema: 5 new fields on `HotelWebsiteConfig` for BYO custom domains (`byoDomain`, `byoDomainStatus`, `byoDomainProvisionedAt`, `byoDomainError`, `byoDomainLastCheckedAt`)
- Types: `LookupHostResponse` interface for the public hostname→slug lookup endpoint
- API: `HotelWebsiteService.claimByoDomain` / `refreshByoDomain` / `releaseByoDomain` — manage a hotel's custom domain via Cloudflare Pages, with hostname normalisation, validation, and conflict detection
- API: `POST /hotel-website/byo-domain` + `POST /hotel-website/byo-domain/refresh` + `DELETE /hotel-website/byo-domain` (OWNER) for managing BYO custom domains
- API: `GET /public/lookup-host?host=<hostname>` — public reverse-lookup endpoint that resolves a hostname (`acmehotel.com` or `<slug>.cnykra.com`) to a `{ slug, hotelId }` pair. Used by the booking site SPA to resolve BYO domains. 5-minute CDN cache; only resolves ACTIVE domains.
- Dashboard: "Custom Domain" card in `/website?tab=booking` — claim your own branded domain (e.g., acmehotel.com), get DNS instructions per apex/subdomain, refresh status, remove. Three new hooks: `useClaimByoDomain`, `useRefreshByoDomain`, `useReleaseByoDomain`.
- Dashboard: Landing tab — accordion editor for 9 sections with per-section enable toggles, image-URL thumbnails, and live iframe preview that reloads on save
- Dashboard: `useLanding`, `useUpdateLanding`, `useResetLanding` hooks
- Dashboard: new top-level `Website` route (`/website`) with 4 tabs (Landing / Booking / Testimonials / Branding); sidebar entry added above Hotel Settings
- Web: new public landing page at `<slug>.cnykra.com/` — 9 typed sections rendered in canonical order, fed by `GET /public/:slug/landing`, with `?preview=draft` mode banner
- API: `GET/PATCH /hotel-website/landing` + `POST /hotel-website/landing/reset` (OWNER) for editing the landing page doc
- API: `GET /public/:slug/landing` + `GET /public/:slug/landing/sections/:type` for the public renderer
- API: `LandingPageService` — raw / resolved / update / reset / single-section APIs with server-side auto-populate (hotel name, room types, testimonials, address flattening)
- Types: shared landing-page Zod schema + TypeScript discriminated union (`LandingSection`, `LandingPageDoc`, `ResolvedLandingDoc`, `PublicLandingResponse`, `defaultLandingDoc()`)
- Schema: `HotelLandingPage` model (one row per hotel) storing the 9-section landing-page doc as `sections JSONB`
- Hotel Website: per-hotel booking-site URL provisioning via Cloudflare Pages Custom Domains API — when OWNER enables direct booking, `<hotel-slug>.cnykra.com` is auto-added to the `cnykra-web` Pages project; SSL provisions automatically. Dashboard "Website" tab shows status (ACTIVE / PENDING / ERROR / REMOVED) with a clickable link, refresh button, and Remove action. New backend: `CloudflarePagesService`, endpoints `POST /hotel-website/domain/provision`, `POST /hotel-website/domain/refresh`, `DELETE /hotel-website/domain`. Schema adds `HotelWebsiteConfig.customDomain` / `customDomainStatus` / `customDomainProvisionedAt` / `customDomainError` + `DomainStatus` enum. Requires `CLOUDFLARE_API_TOKEN` + `CLOUDFLARE_ACCOUNT_ID` env vars (passed via deploy-live.yml from existing `CF_API_TOKEN`/`CF_ACCOUNT_ID` GitHub secrets).
- Public Booking: `GET /public/:slug` (hotel info), `GET /public/:slug/availability`, `GET /public/:slug/testimonials`, `GET /public/:slug/amenities` — anonymous read endpoints gated by `directBookingEnabled` and slug resolution
- Web: initialize `apps/web` Vite + React + TanStack Query workspace (`@cnykra/web`) for the public hotel booking site
- CI: `deploy-dev-web.yml` and `deploy-live-web.yml` — build `@cnykra/web` and deploy to Cloudflare Pages on push to `dev` and `main`; use `cloudflare/wrangler-action@v3` (matches existing dashboard deploys) and target Pages project `cnykra-web`
- Dashboard: Hotel Settings → "Website" tab (OWNER) — branding (logo, phone, check-in/out times, brand colour, tagline, social links, footer note), Direct Booking toggle with payment mode (full / fixed deposit / percentage deposit) and cancellation policy, plus Testimonials CRUD with active toggle
- Dashboard: `use-hotel-website.ts` hook bundle (`useWebsiteConfig`, `useUpdateWebsiteConfig`, `useTestimonials`, `useCreateTestimonial`, `useUpdateTestimonial`, `useDeleteTestimonial`)
- Web: 4-page guest booking flow — BookPage (hero, date search, room cards with rate plan expansion, testimonials, footer), CheckoutPage (OTP verify → details → order summary → Razorpay payment), ConfirmationPage, UnavailablePage. Brand color applied via CSS custom property; hotel slug resolved from `*.cnykra.com` hostname (with `?slug=` fallback in dev).
- API: register `HotelWebsiteModule` + `PublicBookingModule`; CORS now allows any `*.cnykra.com` origin (in addition to `FRONTEND_URL`)
- Public Booking: `POST /public/:slug/auth/request-otp` + `verify-otp` — guest OTP flow on the booking site (auto-creates stub guest on first request); `POST /public/:slug/bookings` creates a TENTATIVE reservation with auto-folio + Razorpay order; `POST /public/:slug/bookings/:id/verify-payment` verifies the HMAC, posts the folio payment, and confirms the reservation
- Hotel Website: `GET/PATCH /hotel-website/config` and `GET/POST/PATCH/DELETE /hotel-website/testimonials` — OWNER-only CRUD for direct-booking branding, payment policy, and testimonials
- Auth: new permissions `hotel_website.read_config` and `hotel_website.update_config` (OWNER default)
- Dashboard: Redeem Points button on folio — converts loyalty points to a discount charge on the open folio (OWNER/ACCOUNTANT/FRONT_DESK)
- Dashboard: Adjust Points button on guest loyalty card — OWNER/ACCOUNTANT can add or deduct points with a reason
- Dashboard: Loyalty settings tab in Hotel Settings — configure earn rate, redemption rate, tier thresholds (OWNER only)
- Dashboard: add use-loyalty.ts hooks for loyalty config, point adjustments, and folio redemption
- Loyalty: expose GET/PATCH /loyalty/config, POST /loyalty/guests/:id/adjust, POST /loyalty/reservations/:id/redeem
- Loyalty: add `LoyaltyService` — configurable earn rate, manual point adjustments, folio redemption (DISCOUNT charge), and tier recomputation
- Loyalty: add `LoyaltyConfig` model — per-hotel earn rate, redemption rate, tier thresholds, isEnabled
- Auth: `GET /auth/permissions` endpoint returns the current user's effective permission set (role defaults + per-hotel overrides).
- Dashboard: `usePermissions()` hook fetches effective permissions from backend, caches via TanStack Query; exposes `has(id)` for permission-based UI gating.
- Maintenance: preventive schedule detail page now shows work orders created from the schedule ('Recent fires' tab).
- Schema: `externalRef` nullable field on Reservation model for OTA booking reference numbers.
- Schema: `Guest.gender`, `CorporateInvoice.irnNumber`/`irnGeneratedAt`, `NightAuditConfig.generateFormC`, and new `FormCRecord` model for Indian compliance pack
- Search: `GET /search?q=` endpoint queries 8 entity types (reservations, folios, work orders, guests, vendors, rooms, assets, corporate accounts) in parallel with case-insensitive matching.
- Dashboard: Cmd-K / Ctrl-K command palette upgraded with `cmdk` library; searches across 8 entity types via unified search endpoint, retains static page/action navigation.
- Dashboard: `useSearch()` hook for debounced global search via TanStack Query.
- SSE: `GET /events/stream` endpoint provides per-hotel server-sent events; JWT passed as query param; 30s heartbeat keepalive.
- SSE: `SseGatewayService` manages per-hotel client subscriptions and broadcasts domain events.
- SSE: `SseEventProcessor` bridges BullMQ domain-events queue to SSE clients with event type mapping.
- SSE: domain event emits added to ReservationsService (create, check-in, cancel/no-show), HousekeepingService (room status change), FoliosService (charge, payment, settle).
- Compliance: GSTR-1 JSON export (`GET /reports/gstr1`) — Government-spec v1.6 JSON covering B2B and B2CS supplies, HSN/SAC summary, and doc issue count
- Compliance: NIC e-invoice JSON download per settled corporate invoice (`GET /corporate-accounts/:id/invoices/:invId/einvoice-json`) — schema v1.1, manual upload to IRP
- Compliance: FRRO Form C generation during night audit — auto-identifies foreign guest arrivals, persists FormCRecord, count shown in audit log
- Compliance: FRRO Form C CSV download (`GET /reports/form-c`) — date-range export with all FRRO portal columns
- Dashboard: Compliance section on Reports page — GSTR-1 JSON and Form C CSV download cards (OWNER/ACCOUNTANT; Form C also FRONT_DESK); Form C foreign-arrival count shown in night audit log detail dialog
- Dashboard: "Download E-Invoice JSON" button on settled corporate invoice dialog (`/accounting/:id`)
Security
- WhatsApp: assign endpoint now validates that the target user belongs to the same hotel — closes a cross-tenant assignment vulnerability discovered during code review
- WhatsApp: conversation detail's message query now filters by hotelId in addition to conversationId (defense-in-depth alongside the conversation guard); invalid `before` cursor returns 400 instead of an opaque Prisma error
Changed
- Web: deduped `SharedNavProps` / `SharedFooterProps` declarations — Classic theme's shared components now import the canonical interfaces from `apps/web/src/themes/types.ts`
- Web: theme registry fallback test now exercises the actual unregistered-theme branch (previously a no-op)
- Web: top-level pages (`LandingPage`, `BookPage`, `CheckoutPage`, `ConfirmationPage`, `UnavailablePage`) are now thin shells that delegate to the active theme's components via the registry
- Web: deleted `apps/web/src/landing/` directory (contents moved to `themes/classic/` in the prior commit)
- Web: split theme registry into `themes/registry.ts` so theme bundles can register without a circular import on `themes/index.ts` (unblocks vitest)
- Web: refactored existing landing + booking renderers into `apps/web/src/themes/classic/` (foundation for multi-theme support; net zero visual change)
- Dashboard: sidebar now enforces `permission` gating on maintenance and website nav items (fields were defined but never checked; previously those items were always visible).
- Notifications: tighten WhatsAppProvider types and test mocks (no behaviour change)
- Notifications: `WhatsAppProvider` now resolves credentials via `WhatsAppAccountResolver` (per-hotel `WhatsAppAccount` row first, env-var platform account as fallback). Callers updated to pass `hotelId`. Behaviour unchanged when no per-hotel rows exist.
- Prisma RLS: `WhatsAppAccount`, `Conversation`, `Message` added to the hotel-scoping middleware allowlist
- WhatsApp: notification listener cleaned up unused destructure (no behavior change)
- Dashboard: inline `userRole === 'OWNER'` checks in 7 route files replaced with permission-based gating via `usePermissions().has()`.
- Health: `GET /health` now checks Postgres and Redis connectivity via `@nestjs/terminus` (was a stub returning static ok).
- Dashboard: polling replaced with SSE-driven TanStack Query cache invalidation for front-desk board, dashboard stats, work orders, preventive schedules, and asset expiry. Falls back to polling when SSE is disconnected.
Fixed
- WhatsApp inbox: media URL in message bubbles now uses `VITE_API_URL` instead of a hardcoded `/api` prefix; previously media (images/audio/video/documents) would 404 in production because the dashboard origin does not proxy `/api` to the API host
- Dashboard: WhatsApp inbox media elements (`<img>`/`<audio>`/`<video>`) carry a TODO — browser tags cannot send Bearer tokens so media will show as broken until the endpoint is switched to cookie-session auth or blob fetch + object URL (deferred past Sprint 1)
- WhatsApp: lint errors across new module specs cleaned up (require → static import, drop unused destructures); CI lint job now passes on this branch
- Dashboard: removed the `Website` tab from Hotel Settings — moved to the new top-level `/website` route. Legacy `/settings?tab=website` redirects to `/website?tab=landing`.
- Web: extracted `SharedNav` and `SharedFooter` from BookPage into `apps/web/src/landing/` (refactor — no visible change; prepares for the Landing page)
- Loyalty: earn rate and tier thresholds now read from `LoyaltyConfig` (default unchanged)
- Schema (Sprint 9 — direct booking): `Hotel` gains `logoUrl`/`phone`/`checkInTime`/`checkOutTime`; `RoomType` gains `images`; `GuestPortalConfig` gains `directBookingEnabled`/`paymentMode`/`depositFixed`/`depositPercentage`/`cancellationPolicy`; new `HotelWebsiteConfig`, `HotelTestimonial` models and `BookingPaymentMode` enum
- Web: `SlugContext` is now async-capable — when the hostname isn't `*.cnykra.com`, it calls `/public/lookup-host` to resolve the slug. `*.cnykra.com` visitors keep the synchronous fast-path (no extra round-trip). Result cached in sessionStorage per hostname. `useSlug()` signature unchanged; new `useSlugState()` exposed for app-shell loading/error rendering.
- API: public `getHotelInfo` and `getLanding` endpoints now return the website `theme` field — required by the booking site's theme resolver (the dashboard endpoint already returned it; the public surface was missing it)
- API: `PublicLookupController.lookup` dropped the unused `@Res({ passthrough: true })` parameter that was failing `Deploy Live`'s lint step (`@typescript-eslint/no-unused-vars` rejects underscore-prefixed names). Cache and CORS headers are still set via the existing `@Header` decorators.
- Docs: `CLAUDE.md` refreshed to reflect Sprint 9.5 (Website module reorg) and Sprint 9.6 (BYO custom domains) — `apps/web` is no longer a stub; new `cnykra-web` Pages project documented; CF token scope expanded to cover Pages domain provisioning; new section §3.5 describes the two Pages projects.
- WhatsApp: inbound worker writes the correct display phone to message.toPhone; domain event payload now carries assigneeUserId for push routing; status updates and media-type fallback hardened
- WhatsApp: webhook endpoint now returns HTTP 200 (was 201) so Meta does not flag it as failing; queue bounds failed jobs with removeOnFail
- WhatsApp: account admin-connect endpoint no longer leaks encrypted access-token bytes in its response; disconnect returns 404 instead of 500 when no account exists
- Landing page: dashboard editor now shows a toast when a save or reset fails (was silently failing on error)
- Landing page: `?preview=draft` responses set `Cache-Control: no-store` so iframe reloads always hit fresh data
- Landing page: `PATCH /hotel-website/landing` with empty `sections` array is now a true no-op (no DB write)
- Landing page: deduplicated hotel fetch in `PublicBookingService.getLanding` — hotel and websiteConfig are now fetched in a single `runAsSystem` call after `getResolvedDoc`
- Build (HOTFIX): `packages/types` now compiles via `tsc` to `./dist/index.js` (its `main`). API runtime container previously crashed with `Cannot find module '@cnykra/types'` because the package shipped only TypeScript source. Dockerfile updated to build types in the builder stage and copy `packages/types/dist/` + `package.json` into the runtime image so the workspace symlink resolves to compiled JS.
- Dashboard: `/website` tab switch now uses TanStack Router `navigate` instead of `window.history.replaceState`, keeping the router's search-param state in sync
- Web: `Section` dispatcher now has an exhaustive `never` guard so TypeScript will catch any future missing case when `ResolvedLandingSection` grows
- Web: `LandingPage` now sources `--brand` CSS variable from the landing fetch (`data.hotel.primaryColor`) instead of the separate `useHotelInfo` fetch, eliminating a brief brand-colour flash on first load
- Tests: provide `EventBusService` mock in `housekeeping.service.spec.ts` and `folios.service.spec.ts`; spy `HotelContext.getOrThrow` in folios spec; add `asset`/`vendor` prisma mocks + name expectations in `work-orders-report.service.spec.ts` cost test. Fixes 46 pre-existing test failures that were blocking the Deploy Live workflow.
- Loyalty: atomic increment in `earnPoints` to prevent concurrent earn race condition
- Types: removed `.default([])` from `amenitiesSchema.items` and `gallerySchema.images` — previously a PATCH omitting these fields would silently overwrite stored data with an empty array; callers must now supply the field explicitly. Narrowed `ResolvedLandingSection` HERO intersection to exclude `null` from `subheadline` (server-side resolution guarantees a string when present).
- Payment: Razorpay adapter spec now compiles and passes locally (previously only worked in CI Docker builds).
- Reports: maintenance cost report now shows asset and vendor names instead of raw UUIDs.
Removed
- Removed empty `billing/` module stub (Sprint 1 scaffold, never implemented; billing logic lives in `folios/`)
v0.4.0 — 2026-05-12
Added
- Sprint 3G dashboard: Vitest + Testing Library wired into the dashboard workspace; `react-hook-form`, `zod`, `@hookform/resolvers`, and `cron-parser` dependencies added in preparation for the maintenance forms
- Maintenance API: new `GET /work-orders/counts` endpoint returning `{ openCount, overdueCount }` for the dashboard KPI tile (requires `maintenance.view`)
- Sprint 3G dashboard: route stubs for `/maintenance/work-orders`, `/assets`, `/vendors`, `/preventive` (8 placeholder pages; filled in later phases)
- Sprint 3G dashboard: maintenance query hooks (work-orders, assets, vendors, preventive-schedules) with smoke tests; types exported from `@cnykra/types`
- Sprint 3G dashboard: sidebar gains "Maintenance" collapsible section (Work Orders / Assets / Vendors / Preventive); StatusBadge gains `workOrder`, `asset`, `preventive`, `vendor` variants
- Sprint 3G dashboard: 7 mutation hooks for vendors + assets (create, update, deactivate/reactivate, retire)
- Sprint 3G dashboard: Vendors list page with filter chips, search, create dialog (GSTIN validation), mobile card layout
- Sprint 3G dashboard: Vendor detail page with Info / AMC Assets / WO History tabs; deactivate / reactivate actions
- Sprint 3G dashboard: Assets list page with AMC/warranty expiry chips, search, create dialog (location XOR guard); inline AMC-expiring warning indicator
- Sprint 3G dashboard: Asset detail page with Info / Children / History tabs; `<AssetTree>` recursive renderer; Retire action with confirmation
- Sprint 3G dashboard: 13 work-order mutation hooks (create, update, all 10 transitions + comment)
- Sprint 3G dashboard: `<StickyActionBar>` primitive with safe-area-inset-bottom support, role-aware buttons, status-aware transition menu via `getTransitionMenu` helper (~7 tests)
- Sprint 3G dashboard: 6 transition sheets/dialogs (assign, hold, complete, verify, reopen, cancel) + reusable `<WoCostForm>` subform
- Sprint 3G dashboard: Work Orders list page with filter chips (status + OVERDUE), full FilterBar, mobile card collapse, "Load more" pagination, create dialog with target picker (room/asset/common-area XOR)
- Sprint 3G dashboard: Work Order detail page with header card, Detail/Timeline/Cost tabs, comment thread, status-aware StickyActionBar wired to 10 transition controls (6 sheets + 4 one-tap mutations)
- Sprint 3G dashboard: 5 preventive-schedule mutation hooks (create, update, fire, deactivate, reactivate)
- Sprint 3G dashboard: Preventive Schedules list page with Active/Inactive filter, next-due-at relative time column; create dialog with INTERVAL_DAYS/CALENDAR_CRON trigger radio and live cron validation via `cron-parser`
- Sprint 3G dashboard: Preventive Schedule detail page with Trigger / Recent fires / Defaults tabs; Fire-now action with confirmation
- Sprint 3G dashboard: 3 maintenance report hooks + cards on `/reports` (Summary, Cost, MTTR) with INR formatting and hh:mm MTTR display
- Sprint 3G dashboard: `<MaintenanceKpiCard>` slotted into the home Dashboard with Open WO + Overdue + AMC-expiring counts and a deep-link CTA; gated by role
v0.3.0 — 2026-05-12
Added
- Maintenance module: `cron-parser` dependency for PM calendar-cron schedules
- Maintenance module: Prisma schema for `Vendor`, `Asset`, `WorkOrder`, `WorkOrderEvent`, `PreventiveSchedule`; new enums (`WorkOrderStatus`, `WorkOrderPriority`, `WorkOrderCategory`, `CommonAreaType`, `AssetStatus`, `PreventiveTriggerType`); `MAINTENANCE` added to `UserRole`; per-hotel `workOrderSequence` and `systemPmUserId` fields
- Maintenance module: RLS coverage for all five new models via `SCOPED_MODELS`
- Maintenance module: per-hotel `SYSTEM_PM` non-login user bootstrapped via seed; backfill migration for existing hotels
- Maintenance module: vendor DTOs (`CreateVendorDto`, `UpdateVendorDto`, `ListVendorsDto`) with Indian GSTIN validation
- Maintenance module: `VendorsService` with CRUD, GSTIN-validated DTOs, soft-deactivation/reactivation
- Maintenance module: `VendorsController` exposing 6 endpoints under `/vendors`
- Maintenance module: asset DTOs with location exactly-one-of guard at validation layer
- Maintenance module: `AssetsService` with exactly-one-of-location guard, parent-cycle detection, AMC/warranty expiry queries, retire-with-active-WO rejection
- Maintenance module: `AssetsController` exposing 8 endpoints under `/assets`
- Maintenance module: 6 new permission strings (`maintenance.view`/`create`/`assign`/`verify`/`delete`/`config`) added to permission catalog with default role grants for `OWNER`, `MAINTENANCE` (new role), `HOUSEKEEPING`, `FRONT_DESK`, `ACCOUNTANT`; `MAINTENANCE` added to `UserRole` union and to `EDITABLE_ROLES`
- Maintenance module: `MaintenanceModule` registered in `AppModule` with `VendorsController` and `AssetsController` (Phase 1 of 4 complete)
- Maintenance module: declarative work-order state machine with `ALLOWED_TRANSITIONS` map (7 states: OPEN/ASSIGNED/IN_PROGRESS/ON_HOLD/COMPLETED/VERIFIED/CANCELLED); shared `ACTIVE_BLOCKING_STATUSES` list for the housekeeping listener
- Maintenance module: work-order DTOs (create, update, list, transition family) with target-XOR and assignment-XOR validation
- Maintenance module: typed `MaintenanceEvent` discriminated union for EventBus integrations
- Maintenance module: `WorkOrdersService.create` with per-hotel sequential numbering (`WO-YYYY-NNNNN`), exactly-one-of target enforcement, assignment XOR, RETIRED-asset rejection, audit + events emission, blocking-event emission when `blocksBookings`+roomId+assignee
- Maintenance module: WO transitions (assign, unassign, start, hold, resume, complete, verify, reopen, cancel, comment) with declarative state machine enforcement, blocksBookings event side effects, verified-event with totalCost, and `hasOtherActiveBlockingWOs` helper for housekeeping listener
- Maintenance module: `WorkOrdersBoardService` with cursor pagination, filter combinations (status/priority/category/room/asset/assignee/date range/overdue/q), grouped-by-status board view, and detail-with-event-timeline lookup
- Maintenance module: `WorkOrdersController` exposing 15 endpoints under `/work-orders` (CRUD, board view, all 10 lifecycle transitions, comments); `WorkOrdersService.update` for cost/descriptive patches
- Housekeeping: listener for `WorkOrderBlockingActivated`/`Released` events — auto-transitions room between `OUT_OF_ORDER` and `DIRTY` with `RoomStatusLog` audit trail; respects `OUT_OF_SERVICE` and multiple-blocking-WO cases
- Maintenance module: preventive-schedule DTOs with trigger-type XOR (`INTERVAL_DAYS` vs `CALENDAR_CRON`) and target/assignee guards at validation layer
- Maintenance module: `PreventiveSchedulesService` with CRUD, trigger-type validation (cron-parser for CALENDAR_CRON), `nextDueAt` recomputation on create/update/reactivate, manual fire that calls `WorkOrdersService.create`, and row-locked `claimDue` (SELECT FOR UPDATE SKIP LOCKED) for the scheduler worker
- Maintenance module: BullMQ scheduler (`maintenance` queue + repeatable `pm.schedule.tick` every 15 min) with `PreventiveScheduleTickWorker` that claims due schedules with row-locking and fires them inside per-hotel SYSTEM context
- Maintenance module: `PreventiveSchedulesController` exposing 7 endpoints under `/preventive-schedules` (CRUD, manual fire, deactivate/reactivate); module fully wired with BullMQ queue
- Maintenance module: `WorkOrdersReportService` for maintenance-summary (counts by status/priority/category), maintenance-cost (sums by asset/category/vendor), and maintenance-mttr (verified-only, ms between createdAt and verifiedAt, optional category breakdown)
- Reports: three new endpoints — `GET /reports/maintenance-summary`, `/reports/maintenance-cost`, `/reports/maintenance-mttr` — each requires both `report.read` and `maintenance.view`
- Night audit: new per-step toggle `maintenanceOverdueEnabled` (default true) on `NightAuditConfig`; emits `WorkOrderOverdueEvent` for non-terminal overdue WOs once per business date (idempotency via `WorkOrderEvent.payload.overdueFlaggedOn`)
- Notifications: maintenance event listener subscribes to `WorkOrderOverdueEvent` and URGENT `WorkOrderCreatedEvent` (BullMQ); two new templates (`maintenance.work_order_overdue`, `maintenance.work_order_urgent_created`) across EMAIL/WhatsApp/Push channels with Handlebars body
Fixed
- Users: `getPermissionCatalog` test updated to include `MAINTENANCE` in `editableRoles`
v0.2.0 — 2026-05-12
Added
- Prisma schema for guests, rooms, room types, reservations, folios; expanded RLS `SCOPED_MODELS` to cover all PMS entities
- `GuestsService` with CRUD, loyalty auto-create, and event-driven processing (`GuestsEventProcessor`)
- `RoomsService` with rate computation, availability checks, and timezone-safe date handling
- `ReservationsService` with full lifecycle (create, modify, cancel, no-show), check-in, check-out, atomic `$transaction` create, and corporate routing
- `FoliosService` with charges, payments, settlement, and GST invoice generation; settlement returns `invoiceNumber`
- Controllers/modules: `GuestsController`, `RoomsController`, `ReservationsController`, `FoliosController`, all wired into `AppModule`
- Schema: `Deposit`, `CityLedgerEntry`, `CorporateInvoice`, GST-breakdown ledger fields; new `ACCOUNTANT` role on `UserRole`
- `AccountingService` — deposit lifecycle with `applyDeposits` at check-in
- `CorporateService` — city ledger, `FULL`/`PARTIAL`/`RATE` corporate checkout, invoice generation
- `GSTReportService` — GST summary grouped by slab + line-item detail
- Controllers: accounting, corporate, GST report; `AccountingModule` wired into `AppModule`
- `ACCOUNTANT` role guard tests; race-condition and ownership checks on corporate service
- Schema: `HotelCommissionPreference`, `OtaCommission`, `TravelAgent`, `AgentPayout`; commission models added to `SCOPED_MODELS`
- `PreferenceService` — hotel commission preferences with lazy creation
- `CommissionService` — OTA + travel-agent commission creation, reconciliation, dispute, rate override
- `AgentPayoutService` — payout settlement with transactional idempotency guard
- `CommissionReportService` — summary by channel/agent + line-item detail
- Wired into reservation checkout; `travelAgentId` added to reservation DTOs
- Schema: housekeeping enums, `HousekeepingConfig`, `RoomStatusLog`, new room fields
- `HousekeepingService` — config management, status transitions, integration hooks
- `HousekeepingBoardService` — room list with checkout-due labels
- Controllers (config, rooms); wired into reservation lifecycle on create, check-in, check-out
- Schema: `NightAuditConfig`, `NightAuditLog`, `Hotel.businessDate`
- `NightAuditService` — orchestrator with pre-flight, issue detection, full run flow
- `NightChargeService` — posts room charges with idempotency
- `NightReportService` — daily summary report generation
- `NightAuditSchedulerService` — BullMQ scheduled runs with retry and notifications; `setupSchedule` on module init
- Config + operations controllers; `NightAuditModule` wired with BullMQ queue
- Report query DTOs with date validation
- `CsvExportService` (column mapping + escaping) and `PdfExportService` (Handlebars + Puppeteer)
- Services: `OccupancyReportService`, `RevenueReportService`, `PaymentReportService`, `GuestReportService`, `HousekeepingReportService`
- `FlashReportService` — executive summary delegating to all report services
- Migrated `GSTReportService` and `CommissionReportService` into reports module
- Handlebars PDF templates for all 10 report types; `ReportsModule` wired into `AppModule`
- Schema: `Season`, `LosDiscount`, `ChannelConfig`, `StopSale`, `Restriction`, `Allotment`
- Services: `SeasonService`, `LosDiscountService`, `ChannelConfigService`, `StopSaleService`, `RestrictionService`, `AllotmentService` (soft/hard mode)
- `RateResolverService` — season → LOS → channel pipeline; integrated into `RoomsService`
- Validates stop-sale, restrictions, allotments on reservation create
- Schema: 3 enums, 5 models (`GroupBooking`, `GroupBlock`, group folio entities)
- Services: `GroupBookingService`, `GroupBlockService`, `GroupFolioService` (charge/payment/settle with GST), `RoomingListService` (bulk pickup with guest upsert)
- `GroupBookingsController` + module; group block availability exclusion + check-in/check-out hooks
- Group cutoff release and status transitions in night audit
- Group booking report (service, template, controller integration)
- Schema: `Chain`, `InvitationToken`, `PasswordResetToken`; added to `SCOPED_MODELS`
- `EmailService` abstraction with console implementation
- `UsersService` (CRUD with delegation enforcement), `InvitationService` (token generation + email + acceptance)
- Auth: `forgot-password`, `reset-password`, `change-password`; login extended with user/hotel details, `mustChangePassword` flow
- `ChainsService` — create chains, add/remove hotels, `CHAIN_ADMIN` promotion
- `JwtAuthGuard` extended with `X-Hotel-Id` chain validation
- `AuditService` + `AuditQueryService` with controller and tests; audit logging instrumented in reservations, folios, group bookings
- Role delegation config endpoints (`get/update roleCreationRules`)
- Schema: 7 enums, 8 models (yield config, demand signals, inventory snapshots, pricing strategies, forecasts)
- Services: `YieldConfigService`, `DemandSignalService`, `InventorySnapshotService`, `ForecastService`, `PricingStrategyService`, `YieldEngineService`
- `YieldSchedulerService` — BullMQ jobs for snapshots, provider sync, evaluation
- Demand multiplier integrated into rate resolver between season and LOS
- Fire-and-forget yield evaluation hooks on reservation create/cancel/no-show
- Yield evaluation hook added to night audit `run()` after `postCharges`
- `YieldManagementController` with 20 endpoints
- Schema: 6 models, 2 enums, `preCheckinCompletedAt`
- Guest auth decorators, JWT strategy, guard; `GuestAuthService` with OTP and magic link
- `GuestPortalConfigService` with default category seeding
- `RequestCategoryService` (CRUD), `GuestRequestService` (lifecycle + staff transitions)
- `GuestNotificationService` with magic-link generation
- `GuestReminderScheduler` for day-before check-in notifications
- `GuestPortalController` — reservations, check-in/out, folio, loyalty, profile
- Controllers: auth, config, guest requests, staff requests, categories
- Guest push subscribe/unsubscribe endpoints
- Schema: `OutletType` enum, `POS_CHARGE` and `POS_MANAGER` roles, 3 new models
- POS API-key auth (guard, decorators, `PosContext`); DTOs for outlet/terminal/charge/report
- Services: `PosOutletService` (CRUD + API key lifecycle), `PosChargeService` (room lookup, posting, idempotency, void), `PosReportService` (revenue by outlet, daily summary, charge detail)
- POS controllers (outlet CRUD, charge posting, reports); `PosModule` wired into `AppModule`
- Schema: `PaymentTransaction`, `PaymentLink`; `ONLINE` payment method
- `PaymentGateway` interface + `PaymentGatewayException`
- `RazorpayAdapter` implementing the gateway interface
- `PaymentService` — order creation, webhook handling, refunds (timing-safe HMAC, event filtering, amount guard)
- `PaymentController` + `PaymentWebhookController`; `PaymentModule` wired with `rawBody` for webhooks
- Guest portal embedded checkout flow
- Schema: notification preferences, templates, delivery logs, push subscriptions
- Channel providers: SendGrid (email), WhatsApp via Meta Cloud API, Web Push (VAPID)
- `TemplateService` — Handlebars rendering with code defaults and hotel overrides
- `NotificationService` orchestrator + `NotificationWorker` for async delivery
- Notification DTOs and controllers (settings, templates, logs, push subscriptions)
- `NotificationModule` replaces former `WhatsappModule`
- `GuestNotification`, `GuestAuth` OTP, and `Email` mocks replaced — all route through `NotificationService`
- Scaffolded Vite + React 18 + TypeScript dashboard (`apps/dashboard`)
- Tailwind brand tokens + shadcn/ui (new-york style, Radix)
- API auth response extended with user/hotel details; CORS enabled (comma-separated origin support)
- Auth store with ky HTTP client and JWT interceptors
- TanStack Router with login page, auth guard, file-based routes
- App shell: sidebar, topbar, theme toggle, placeholder pages for all module routes
- PWA config, splash screen, connectivity status, install prompt
- Cloudflare Pages deployment wired into `deploy-dev.yml` and `deploy-live.yml`
- Shared components: `PageHeader`, `FilterBar`, `EmptyState`, `StatusBadge`, `KPICard`, `DataTable` (sorting + client-side pagination)
- shadcn additions: `react-table`, `date-fns`, `table`, `select`, `badge`, `tabs`, `dialog`
- Pages: Dashboard home (KPIs, activity feed, quick actions), Rooms inventory (type cards, filters, data table), Front Desk (room tiles, status filters, floor tabs, detail sheet), Reservations list + detail with folio
- StatusBadge variants: loyalty, folio
- Guest/folio query hooks; folio mutation hooks
- Pages: Guests list (search, tier filter), Guest detail (personal info, ID, loyalty, reservation history), Folios list (status/balance filters), Folio detail (charges, payments, settle, invoice view)
- API: `GET /folios` endpoint, `roomType` included in guest detail
- Payment StatusBadge variant; financial query hooks
- Payment, accounting, POS mutation hooks
- Pages: Payments (transactions + payment links tabs), Accounting (corporate accounts + reports tabs), Corporate Account detail (ledger + invoices), POS (outlets + reports tabs), POS Outlet detail (terminals + charges)
- Confirmation dialogs for destructive actions
- shadcn additions: Switch, Textarea, RadioGroup, Accordion, Checkbox
- StatusBadge variants: notification, guestRequest
- Housekeeping page (room board + configuration); rate-management query hooks
- Pages: Rate Management (seasons, pricing, controls, preview), Notifications (settings, templates, delivery log), Guest Portal (settings, categories, requests management)
- StatusBadge variants and sidebar nav additions
- Reports query hook + export helper; night audit, group bookings, users query hooks
- ~19 new mutation hooks across pages
- Pages: Reports (11 report types, flash KPI view, CSV/PDF export), Night Audit (status/run, logs, configuration), Group Bookings list (filters, create dialog), Group Booking detail (info, blocks, reservations, folio tabs), Users & Staff (team members, create/invite, role rules)
- Schema: `Reservation.roomTypeId`, nullable `roomId`, `overbookingPercentage`
- `CreateReservationDto` accepts optional `roomId`; new `AssignRoomDto`
- `RoomsService.availabilityByType` — count-based availability
- `ReservationsService.create` rewritten for room-type-first booking with optional room
- New endpoints: `assign-room`, `unassign-room` with check-in gate
- Group-blocked counts subtracted from availability in `create()`
- Guest portal `select-room` updated; pre-check-in gated on room assignment
- Hotel-level housekeeping flow setting in Hotel Settings
- Auto-status migration when mode is switched
- Dashboard: renamed HK statuses, mode-switch UX
- Audit `mode` field + 9 per-step toggles on `NightAuditConfig`
- Run flow respects per-step toggles
- Dashboard configuration tab exposes toggles
- Phase 1: schema + service for custom permission rules per hotel
- Phase 2: migrated all 19 modules to `@RequirePermission` decorators (reservations, group-bookings, yield-management, commissions, rate-management, guest-portal, pos, notifications, folios, night-audit, users, rooms, accounting, payment, chains, reports, guests, audit-logs)
- Phase 3: legacy `RolesGuard` removed (`refactor(auth)`)
- Demo hotel seed script (`apps/api`)
- Catch-up Prisma migration for schema drift (47 tables, 24 enums)
- Test push notification endpoint for owners
- Push notifications wired end-to-end (VAPID, SW push handler, auto-evict oldest subscription at limit)
- Mobile-friendly PWA: bottom nav, touch animations, keyboard navigation, Ctrl-key shortcuts (replacing leader keys), command palette, theme toggle in preferences
- Brand SVG logos and SVG favicon; PWA icons, manifest shortcuts, Apple meta tags
- Login simplified to email + password
- Booking source grouping, smart group filtering, sidebar install button
- Rooms page card/list toggle with grouping and smart status tiles
Changed
- `WhatsappModule` replaced by `NotificationModule` as the unified delivery layer
- Sidebar redesigned with collapsible sections and preferences sub-menu; collapse removed in favour of enhanced search + external links
- Leader-key navigation replaced with Ctrl+key bindings and a shortcuts dialog popup
- Reports module consolidated — `GSTReportService` and `CommissionReportService` moved in
- Auth: `RolesGuard` removed in favour of permission decorators (Plan C Phase 3)
Fixed
- Prisma RLS middleware: AsyncLocalStorage context loss in `$use` middleware; `runAsSystem` now mutates store in-place to preserve context
- `RefreshToken` schema: removed `tokenHash` unique constraint (collision risk)
- `upsert` RLS gap: middleware now covers both `where` and `create` branches
- JWT lifetime locked to 15 minutes; non-root container user; env file permissions tightened
- bcrypt → bcryptjs for Alpine-container compatibility
- Razorpay config made optional for graceful startup without credentials
- CI/CD: native `ssh`/`scp` instead of `appleboy/ssh-action`; force-recreate container on deploy; non-blocking Cloudflare cache purge; GHCR auth in seed workflow; `--ignore-scripts` in Dockerfile with explicit `prisma generate`
- Traefik upgraded to v3.6; networks marked external
- Numerous dashboard fixes — local font files, dropdown triggers without `asChild`, accounting page manual-refetch hook, runtime errors on Reservations/Group Bookings/Night Audit/Users pages, strict `tsc -b` build cleanup
- Reservations: atomic `$transaction` wrap on create; state-machine, Decimal, and date-arithmetic quality fixes
- Folios: rounded GST total, `DateString`-to-`Date` coercion, tightened loyalty guard
- Accounting + corporate services: race conditions, ownership checks, error mapping, test coverage
Security
- Webhook handlers use timing-safe HMAC comparison; event filtering and amount guards on payment webhooks
- VAPID keys delivered to containers via separate env file; not exposed in compose
v0.1.0 — 2026-05-05
Added
- NestJS 10 application with TypeScript, strict null checks, and `forceConsistentCasingInFileNames`
- `HotelContext` — AsyncLocalStorage-based tenant context with `REQUEST` and `SYSTEM` modes; system mode enables background workers to bypass RLS
- `RequestContextMiddleware` — wraps every incoming request in an ALS context before guards run
- Prisma RLS middleware that auto-injects `hotelId` into all queries for scoped models (`User`, `RefreshToken`, `AuditLog`)
- Covers all read/write operations including `aggregate`, `count`, `groupBy`, and `upsert` (both `where` and `create` branches)
- `MissingTenantContextException` thrown on any scoped query without an active context
- System bypass for background workers via `HotelContext.runAsSystem()`
- `Hotel` — tenant root entity with `slug` (unique), `planTier`, `timezone`, `currency`, `gstin`
- `User` — scoped to hotel; `@@unique([hotelId, email])` enforces per-hotel email uniqueness
- `RefreshToken` — two-part token design: UUID `id` for O(1) lookup, bcrypt `tokenHash` for secret verification
- `AuditLog` — append-only, scoped to hotel; indexes on `(hotelId, createdAt)` and `userId`
- JWT access tokens (15-minute lifetime) via `@nestjs/jwt` and Passport
- Refresh tokens: 30-day rolling window, revocation via `revokedAt`; two-part format `{tokenId}:{rawSecret}` for O(1) DB lookup
- `JwtAuthGuard` — global guard; populates `HotelContext` from JWT payload
- `RolesGuard` — global guard; enforces `@Roles()` decorator; registered after `JwtAuthGuard`
- `@Public()` decorator bypasses auth on login, refresh, and health endpoints
- `@CurrentUser()` and `@CurrentHotel()` parameter decorators for controller use
- `POST /auth/login`, `POST /auth/refresh`, `POST /auth/logout`
- `EventBus` abstraction over BullMQ (`@nestjs/bullmq`) for typed inter-module events
- Domain modules communicate via events, never by importing each other's services
- `GET /health` — public endpoint returning `{ status: 'ok', timestamp }`, used by CI and load balancer probes
- Empty module shells for `reservations`, `housekeeping`, `channel`, `billing`, `whatsapp` — structure established for Sprint 2+
- `packages/types` — `@cnykra/types` package exporting `UserRole`, `PlanTier`, `AuthLoginResponse`, `AuthRefreshResponse`, `HealthResponse`
- Multi-stage Dockerfile (deps → builder → prod-deps → runtime); runs as non-root `node` user
- `.dockerignore` excluding `node_modules`, `dist`, `.env*`, coverage, and test files
- `docker-compose.dev.yml` — dev stack with resource limits (API: 2 vCPU/4 GB, DB: 1 vCPU/2 GB, Redis: 0.5 vCPU/512 MB); healthchecks on DB and Redis
- `docker-compose.live.yml` — live stack with backup container (pg_dump → S3/R2 every 6h); healthchecks on DB and Redis
- `docker-compose.traefik.yml` — Traefik v3.1 with HTTP→HTTPS redirect, Cloudflare origin certificate TLS
- GitHub Actions CI/CD: `deploy-dev.yml` (push to `dev`) and `deploy-live.yml` (push to `main`) — typecheck → lint → test → docker build/push → SSH deploy → Prisma migrate