Multi-property hotel booking & management
Hospitality Booking Platform
An end-to-end multi-property hotel platform: a guest booking site, an operations dashboard for staff, a mobile reporting app for owners, and an AI chatbot, with dynamic pricing, multi-gateway payments, and policy-based refunds.
- Role
- Full Stack Developer
- Year
- Dec 2025
- Type
- Personal project




83
Database models
~60
Backend modules
8
Payment methods
3 + AI
Client apps + chatbot
4
UI languages
5
Display currencies
Problem
Hotel operations break down in the gaps: prices edited by hand and inconsistent across channels, two guests booking the same room at the same moment, rooms held by unpaid bookings, refunds calculated manually and disputed, foreign guests confused by Rupiah prices, and owners who need a laptop just to check performance.
Solution
A single platform that covers the whole stay, from guests finding a room on SEO-optimized pages, seeing prices in 5 currencies, and paying by virtual account, QRIS, PayPal, hotel credit, or a Sui-based token, through check-in, check-out, rescheduling, and refunds. On the operator side it handles per-branch dynamic pricing with approvals, room plans, maintenance, front-desk work, financial reports, restaurant partnerships with automatic monthly billing, and a newsroom.
Problems Solved
Room prices changed manually, inconsistent between channels
→Per-branch price proposals with rules (weekend, specific date, date range) approved by the owner, all computed per night by one pricing engine
Two guests can book the same room at the same time
→Atomic availability lock in the database: one request wins, the other gets a 409
Rooms held by bookings that never get paid
→Unpaid bookings auto-expire after 15 minutes through a RabbitMQ delay queue and the room is released
Refunds calculated by hand and easily disputed
→Refund policy based on days before check-in, with a preview before cancelling, paid out as cash or hotel credit
Refunded money leaves the hotel ecosystem
→Refund to hotel credit usable for the next booking, or withdrawable as an on-chain token
Foreign guests confused by Rupiah prices
→Display currency (IDR, USD, SGD, JPY, CNY) with daily rates and a rate snapshot saved at booking
Rooms hard to find on Google
→Server-rendered room listing pages per city, branch, room type, and promo, with JSON-LD, canonicals, and sitemaps
Owners need a laptop to see performance
→Flutter reporting app with PIN and biometric unlock, m-banking style
In-house restaurants managed separately
→Partnership module (subscription or revenue share) with automatic monthly billing and in-room food ordering
Reception answers the same questions over and over
→AI chatbot on the web (and WhatsApp) that answers questions, checks rooms, and creates bookings
Key Features
Foundation
- 3 account types (hotel admin, restaurant admin, Google guest) with JWT + DB sessions
- Leveled roles, branch-scoped admins, multi-level sidebar managed from a config panel
- Multi-branch with slugs & aliases for URLs
- 4 languages: Indonesian, English, Japanese, Chinese
Rooms & Pricing
- Multilingual master data: room types, bed types, facilities, galleries
- Room plan: floor map with status and nightly price
- Price proposals with FIXED_PRICE / PERCENTAGE rules, priority, and conflict detection
- Price adjustments per type or per room, with approval and a comparison calendar
Booking Lifecycle
- Multi-room booking, double-booking prevention, auto-expiry, real-time status
- Payments via Midtrans, DPAY, PayPal, hotel credit, and Sui token
- Two-step cancel & refund (preview token → confirm) with approval
- Rescheduling with price-difference payment and automatic rollback
- Room maintenance that blocks rooms and cancels conflicting pending bookings
Monitoring & Reports
- Finance dashboard: daily metrics, revenue, occupancy, Excel export
- Reports across branches, per room type, and per physical room
- User logs and active admin sessions
Restaurant & F&B
- Restaurants, owners, per-site branches, and staff
- Multilingual categories & products with approval
- Hotel ↔ restaurant partnerships with cron-based monthly billing
- In-room food ordering with Midtrans Snap
Content & SEO
- Server-rendered room listing and room detail pages
- Newsroom: drafts, scheduling, takedown, revision history, 301 for old slugs, view & like analytics
- Sitemap index per module, robots, canonicals, noindex for thin pages
Extra Channels
- AI chatbot (web widget + WhatsApp webhook) that books through the same flow as the web
- Flutter Report App for hotel owners & managers and restaurant owners
System Design & Architecture
A Next.js frontend (guest site, hotel and restaurant dashboards, newsroom, config panel) and a Flutter app talk to a NestJS REST API with a Socket.IO channel for live payment status. RabbitMQ handles delayed jobs, scheduled jobs handle billing, exchange rates, and publishing, and a Python agent powers the chatbot. Every service is containerized with Docker and deployed through a Jenkins pipeline to a VPS.
Clients
- Next.js 16 web (SSR guest site, dashboards, newsroom)
- Flutter Report App
Backend
- NestJS 11 REST /api/v1 + Socket.IO
- Python FastAPI AI agent
- RabbitMQ delay queue + DLX
- Scheduled jobs
Data
- PostgreSQL via Prisma 7
- MinIO object storage
External
- Midtrans
- DPAY
- PayPal
- Sui blockchain
- Exchange-rate API
- Google OAuth
Tech Stack
- Frontend
- Next.js 16 (App Router, SSR, server actions), React 19, TypeScript, Tailwind CSS 4, shadcn/Radix UI, TanStack Query & Table, Zustand, NextAuth, i18next, Socket.IO client, Recharts, Framer Motion, Sui dApp Kit
- Backend
- NestJS 11, Prisma 7 + PostgreSQL, Passport JWT, class-validator, Socket.IO, @nestjs/schedule, RabbitMQ, ExcelJS
- Storage
- MinIO (S3-compatible)
- Payments
- Midtrans Core API & Snap, DPAY (HMAC-SHA256), PayPal Orders v2 with webhook verification
- Web3
- Sui: token minting via TreasuryCap, transfer listener (WebSocket + polling fallback)
- AI
- Python FastAPI, OpenAI-compatible client to OpenRouter (Gemini Flash)
- Mobile
- Flutter (Android & iOS), Provider, fl_chart, secure storage, biometrics
- Testing & DevOps
- Jest, Playwright, Cypress, race-condition scripts, Docker, Jenkins
Technical Deep Dive
01Preventing double-bookingAvailability is stored per room per night, and every night is locked with one atomic statement, so correctness holds even with multiple backend instances.+
- Each night is claimed with INSERT … ON CONFLICT DO UPDATE … WHERE isBooked = false
- The second concurrent request waits on the row lock, then fails the condition and gets 409 Conflict
- Paying with hotel credit locks the balance row (SELECT … FOR UPDATE) so one balance can't fund two bookings
- Verified with scripts that fire parallel booking requests
02Auto-expiring bookings with RabbitMQUnpaid bookings release their rooms without a cron job scanning the bookings table.+
Booking created (PENDING)
└─► publish to delay queue (TTL 15 min, no consumer)
└─► TTL ends → dead-letter exchange → booking.expiry.process
└─► consumer: still PENDING? → EXPIRED, release room, emit socket- The consumer is idempotent: already-paid bookings are ignored
- A payment arriving after expiry doesn't revive the booking; it's flagged for manual refund instead
03Multi-gateway payment orchestrationOne signature-verified webhook, one place where payment status transitions live.+
- The webhook routes by order-id prefix (BK- for bookings, FO- for food orders); unknown prefixes still get 200 so gateways don't retry forever
- Midtrans, DPAY, and PayPal all funnel into a single applyGatewayStatus function
- PayPal doesn't accept IDR: bills are converted to USD server-side, and webhook payloads are only a trigger. Status and amount are re-fetched from PayPal and matched against the bill
- Status changes are pushed over Socket.IO, with 5-second polling as a fallback while pending
04Per-night pricing engineListings, availability, room plans, bookings, and reschedules all get their price from one PricingService.+
For each night: 1. Find the APPROVED proposal for the branch covering that date 2. Start from the room type's base price 3. Apply matching rules (WEEKEND / SPECIFIC_DATE / DATE_RANGE) by priority 4. Apply APPROVED adjustments (latest wins; per-room beats all-rooms) Total = Σ nightly prices → stored on the booking item
- Maker-checker: managers create proposals and adjustments, owners approve them; overlapping periods are rejected
- Prices are locked into the booking, so later changes never affect existing bookings
- Room types without a proposal are hidden from the public, so guests never see a room they can't book
- Refactored from 'check-in price × nights' to per-night sums, because one stay can span weekends or promo dates
05Rescheduling with settlement & rollbackThe hardest case: a guest moves to more expensive dates and pays the difference by virtual account.+
- The new booking starts PENDING while the old one stays active until the difference is paid, so a failed payment never costs the guest their room
- Paid → old booking cancelled and released; expired → new room released, old booking untouched
- Settlement runs from every path that can change status, with row locks and conditional updates so concurrent processes don't overwrite each other
- A negative difference goes straight to the guest's hotel credit
06Policy-based refundsRefund amounts are computed by the server and confirmed by the guest, never trusted from the client.+
- A policy is a day range before check-in + refund percentage + allowed payout type; ranges are validated not to overlap
- Preview issues a 15-minute token holding the refund amount, and confirm redeems that token
- Credit refunds land instantly; cash refunds go through approval
07Tokenized hotel credit on SuiHotel credit can be withdrawn as an on-chain token and spent back on bookings.+
- After admin approval the backend mints the token to the guest's wallet via TreasuryCap
- The IDR rate is total IDR backing ÷ total tokens minted, with a backing log
- Token payments are verified by a transfer listener or a client-submitted transaction digest, idempotent per digest
08Multi-currency without billing driftBilling is always in IDR; other currencies are display-only.+
- Converted prices are marked ≈ with a tooltip showing the rate and its timestamp
- A daily job fetches rates, keeps the last good rate on failure, and supports a manual lock
- Each booking stores a server-computed snapshot: currency, amount, rate, and rate time
- Language and currency live in cookies, so SSR pages render correctly with no hydration mismatch
09SEO room listing pagesOne catch-all route serves city hubs, branches, room types, promo facets, and room details.+
- Paths resolve in a canonical order (location → type → facet); aliases and other orders 301 to the canonical path
- Unknown segments return a real 404, not a soft 404
- Branch and room-type slugs share one namespace so a segment never has two meanings
- Listings with fewer than 2 rooms get noindex, follow and are left out of the sitemap
10Layered access controlSecure by default: every endpoint is admin-only unless explicitly opened.+
Request ─► ① Auth (JWT + DB session) ─► ② Endpoint guard ─► ③ Service check (role level, branch scope) ─► data Dashboard ─► ④ Menu access control (only decides what's shown)
- Roles and branches are re-read from the database, so changes apply without re-login
- A clear split between visible menus (UX) and endpoint permissions (security)
11Report App: m-banking style securityOwners check performance from their phone without weakening security.+
- Password login once per device, then a mandatory 6-digit PIN (weak PINs rejected)
- Unlock by server-checked PIN (device revoked after 5 failures) or fingerprint
- Auto-lock on idle or background, refresh tokens rotated on every use
12An AI chatbot that can transactGuests can check rooms and book straight from chat.+
- A FastAPI agent calls an LLM through OpenRouter and uses the backend's public endpoints
- Chat bookings go through the same booking flow as the web, so no business rule is duplicated
- Conversation context and history are stored per number; the WhatsApp webhook is in place
Challenges & Lessons
Race conditions on bookings and balances
Database-level locks, tested with parallel requests
Data consistency has to be guaranteed by the database, not application code
Payments arriving after a booking expired
Don't revive the booking; flag it for manual refund
In distributed systems event order isn't guaranteed, so design for the late case
Reschedules with pending payment
Keep the old booking until the difference is paid; roll back from every path
Never let a third-party failure make the user lose something
Wrong prices for stays spanning a weekend
Refactor to per-night pricing in a single PricingService
One source of truth for anything involving money
Gateways that don't support IDR
Convert on the server and store the amount and currency actually charged
Separate display price from billed amount from day one
Hidden menus ≠ secure endpoints
Audit permissions and switch to admin-only by default
Authorization belongs on the server; the UI only controls what's shown
SEO for dynamic data
SSR, backend path resolver, canonical and noindex rules
SEO is an architecture decision, not just meta tags
Next project
ESP Design Platform