CNYKRA

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
Start free trial