← All projects

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
Hospitality Booking Platform
Restaurant & F&B dashboard with in-room food orders, best-selling menu, partnership bill, and resto staff roles
Restaurant & F&B: partner restaurants manage menus, staff, and in-room orders, while partnership bills are issued automatically.
Config panel with role levels, per-role menu access control, display exchange rates, and payment gateway settings
Config panel: leveled roles, menu access per role, locked exchange rates, and payment methods. Endpoint permissions stay enforced on the server.
Flutter Report App with a PIN unlock screen and an owner revenue and occupancy report
Report App (Flutter): owners check revenue and occupancy behind m-banking style PIN and biometric unlock.
Next.jsNestJSPostgreSQLPrismaRabbitMQSocket.IOFlutterFastAPISuiMidtransPayPalDocker

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
  • WhatsApp

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

→