Skip to content

System Requirement Definition (SRD)

Project: PTX Channel Manager (ptx-cm)
Version: 2.7.0
Date: 2026-03-12
Type: Internal Tool (not SaaS)


1. System Overview

1.1 Purpose

PTX-CM is an internal OTA extranet automation tool that prevents overbookings by auto-syncing room availability across multiple OTA partner dashboards (Booking.com, Agoda, Traveloka, Expedia) from a centralized interface. It targets a hospitality operator managing 100+ properties across Southeast Asia (Vietnam, Indonesia, Malaysia).

1.2 Problem

Staff manually update 4 OTA extranets per property (~400+ sessions). When a booking arrives on one OTA, availability isn't reduced on others fast enough, causing multiple daily overbookings. Each overbooking damages guest trust, incurs relocation costs, and risks OTA penalties.

1.3 Solution

A unified dashboard that:

  1. Connects OTA accounts and auto-discovers properties from each OTA extranet
  2. Polls OTA extranets for new bookings every 2-3 minutes
  3. Automatically pushes updated availability to all connected OTAs
  4. Alerts staff on sync failures or potential overbookings
  5. Provides a single view of all bookings and availability, filterable by country

1.4 Technical Approach

Reverse-engineer OTA extranet API calls via HTTP request interception (Playwright). Replay captured requests to read bookings and write availability updates. No official OTA API partnerships required.

1.5 Constraints

  • Solo developer
  • Zero infrastructure budget (self-hosted on existing server)
  • OTA extranet automation carries ToS risk
  • Mixed 2FA methods across OTA accounts
  • One OTA account may manage multiple properties
  • 3 countries with different currencies and timezones

2. Actors (User Roles)

IDRolePreset KeyDescriptionAccess Level
U-01Super Adminsuper_adminSystem owner. Full access to all modules and all roles. Cannot be deleted.Full access (country=null)
U-02AdminadminSystem administrator. Full access except cannot manage other SA users.Full access (country=null)
U-03ManagermanagerOperations manager overseeing properties. Full CRUD on operational modules, view-only on settings/users.Full or country-scoped
U-04OTA OperatorotaStaff managing OTA accounts, connections, sync, room mappings.Country-scoped
U-05Customer ServicecsHandles bookings and alerts. View/edit bookings, resolve alerts.Country-scoped
U-06FinancefinViews booking revenue, rates, availability. Read-only.Country-scoped
U-07PurchasingpoPlaceholder for future ERP purchasing module. Minimal access.Country-scoped

Note: Roles are stored in a roles table (E-14) with bitwise permissions per module. Admins can create custom roles beyond these 7 presets. See FR-20–FR-23.


3. Functional Requirements (FR-xx)

Phase 1 — P1: Anti-Overbooking Core

IDFeaturePriorityStatusDescriptionActor
FR-01OTA Account ManagementP1⚠️ PARTIALCRUD + encrypted credentials ✅. Test connection & refresh session are stubs (return hardcoded messages). No Playwright integration.U-01
FR-02Property Discovery & ImportP1❌ NOT IMPLOTA adapters are all stubs — fetchProperties() returns empty. No property discovery, no cross-OTA matching. Manual property CRUD works.U-01
FR-03Booking PullP1⚠️ PARTIALBullMQ orchestration ✅, booking upsert ✅, dedup ✅. But OTA adapters fetchBookings() returns empty. Also always fetches from new Date(0) (no incremental pull). Room mapping uses first available instead of matching by OTA room ID.System
FR-04Availability Auto-SyncP1⚠️ PARTIALAvailability calculation logic ✅, overbooking detection ✅, BullMQ processors ✅. But OTA adapters pushAvailability() is a stub — never actually pushes to any OTA.System
FR-05Property & Room ManagementP1✅ COMPLETECRUD for properties, room types, OTA connections, room mappings all working.U-01, U-02
FR-06Sync Status DashboardP1✅ COMPLETEKPI cards, sync status panel, recent bookings — all fetch real DB data.U-01, U-02
FR-07Overbooking AlertP1⚠️ PARTIALAlert creation on overbooking ✅, alert list/resolve ✅, dashboard banner ✅. LINE Notify not implemented (stub returns "not available in P1"). Email notification service exists but depends on SMTP config.U-01, U-02
FR-08Country-Scoped AccessP1✅ COMPLETECountry scope guard, decorator, helper all working. Staff locked to country, manager sees all.U-01, U-02

Phase 2 — P2: Operational Efficiency

IDFeaturePriorityStatusDescriptionActor
FR-09OTA Rate Formula EngineP2✅ IMPLEMENTEDv2.7.0: Replacing simple markup/discount adjustments with formula-template-based rate calculator. Each OTA type has configurable formula (commission%, promo%, surcharges). Multiple rate lines per OTA (member, campaign, Early Bird, Lastmin variants). Net price from Airbnb or manual → formula → BAR rate → derived lines. Auto-calculate on screen, manual push to OTAs. New entities: E-23 (OtaFormulaTemplate), E-24 (OtaRateConfig), E-25 (OtaRateLine). Replaces RatePlan/RatePlanAdjustment system. Rate Rules module (CRUD) retained for date-range overrides. Backend: ota-rate-configs module + formula-engine service (mathjs). UI: OTA cards with parameter inputs, live BAR calculation, rate line management (S-11).U-02, U-03
FR-10Availability CalendarP2❌ NOT IMPL14-day date grid (room types × dates) with color-coded cells showing available/total. Click cell → popover to block/unblock rooms → immediate OTA sync job. Property sidebar (same pattern as Rate Manager). Route: /availability. Backend: GET /availability + PUT /availability/block. Existing: Prisma model, AvailabilityCalcService, AvailabilitySyncProcessor. Missing: controller, frontend page+components.U-03, U-04
FR-11Booking TimelineP2❌ NOT IMPLNo route, no component, no page. Not built at all.U-01, U-02
FR-12Cancellation SyncP2❌ NOT IMPLNo cancelBooking method in OTA adapter interface. Booking status can be changed to "cancelled" via workflow engine, but no OTA-side detection or sync.System

Phase 2.5 — P2: User Profile & Preferences

IDFeaturePriorityStatusDescriptionActor
FR-16User Profile ManagementP2✅ COMPLETEProfile page with name/email/locale edit, live format preview.U-01, U-02
FR-17Password ChangeP2✅ COMPLETECurrent password verification, 8-char min, mustChangePassword flow.U-01, U-02
FR-18Auth HydrationP2✅ COMPLETEGET /users/me on mount, 401 → redirect to login.System
FR-19Client-Side ThemeP2✅ COMPLETEDark/light toggle, localStorage persistence.U-01, U-02

Phase 2.7 — P2: Role & Permission Management

IDFeaturePriorityStatusDescriptionActor
FR-20Role CRUDP2✅ COMPLETECRUD with permission map, system role protection, 7 presets seeded.U-01, U-02
FR-21Bitwise Permission SystemP2✅ COMPLETEJSONB bitmask per module, bitwise AND checks.System
FR-22Permission-Based Access ControlP2✅ COMPLETEPermissionsGuard + @RequirePermission decorator. Frontend sidebar/button hiding.System
FR-23Role AssignmentP2✅ COMPLETERole selector in Users tab, JWT includes permissions.U-01, U-02

Phase 2.9 — P2: Booking Status Workflow Engine

IDFeaturePriorityStatusDescriptionActor
FR-25Booking Status TransitionsP2✅ COMPLETETransition table with FK integrity, UNIQUE(from,to), allowedRoles, hooks.U-01, U-02
FR-26Booking Status ChangeP2✅ COMPLETEPATCH with transition validation, role check, country scope, hook execution.U-01–U-05
FR-27Transition HooksP2✅ COMPLETEaudit_log, update_availability, send_notification hooks execute on transitions.System
FR-28Per-Status UI ConfigP2✅ COMPLETEJSONB uiConfig on BookingStatusDef with per-role sections/buttons/editableFields.U-01, U-02
FR-29Workflow Config PageP2✅ COMPLETESettings workflow tab with status list, transitions CRUD, Mermaid preview.U-01, U-02
FR-30Workflow VisualizationP2✅ COMPLETEMermaid stateDiagram-v2 with role annotations, client-side rendering.U-01, U-02

Phase 2.8 — P2: Client-Side Data Optimization

IDFeaturePriorityStatusDescriptionActor
FR-24Client-Side Data CachingP2✅ COMPLETESWR for all GETs, mutate on change, polling with tab-visibility pause, debounced filters.System

Phase 2.10 — P2: Supplier Room Allocation

IDFeaturePriorityStatusDescriptionActor
FR-31Supplier Room AllocationP2✅ COMPLETEM:N junction table (E-17) linking Supplier↔RoomType with roomCount. Soft warning when SUM(allocations) != totalRooms. Backend service with country scope validation.U-01, U-02, U-04
FR-32Supplier Allocation CRUDP2✅ COMPLETECreate/update/delete allocations per room type via modal (S-23). Soft delete (isActive). Two controllers: room-type context (POST /room-types/:id/supplier-allocations) and standalone (PUT/DELETE /supplier-allocations/:id).U-01, U-02, U-04
FR-33Supplier Allocation DisplayP2✅ COMPLETERoom type table shows supplier badges with room counts + AlertTriangle on mismatch. Supplier detail page shows allocations grouped by property with clickable rows.U-01–U-07
FR-34Booking Supplier AttributionP2✅ COMPLETEPATCH /bookings/:id/supplier endpoint for assign/unassign. Supplier picker in Booking Detail Financial card with search dropdown. Audit logged. Permission: BOOKINGS.EDIT.U-01–U-05

Phase 2.11 — P2: Activity Log Infrastructure

IDFeaturePriorityStatusDescriptionActor
FR-35Activity Log PersistenceP2✅ COMPLETEReplace flat-file (logs/activity.log) with PostgreSQL activity_logs table (E-18). HTTP requests logged via middleware (fire-and-forget Prisma create). Client events batched via createMany. GET /activity-logs queries DB with orderBy createdAt desc, take N. Same API response shape — no frontend changes. ACTIVITY_LOG_ENABLED env kill-switch preserved. Visible in S-25 Activity Log tab.System

Phase 2.12 — P2: Customer Management

IDFeaturePriorityStatusDescriptionActor
FR-36Customer Profile ManagementP2✅ COMPLETENew Customer entity (E-19) with name, email, phone, nationality, notes. CRUD endpoints (GET/POST/PATCH/DELETE /customers). CS-enriched data layer independent of OTA booking data. Search by name/email/phone. Sidebar nav item with Contact icon. i18n (en/vi).U-01–U-05
FR-37Return Customer LinkingP2✅ COMPLETEManual linking of bookings to customer profiles via POST /customers/:id/link and /unlink. Nullable customerId FK on Booking. Suggestion engine (GET /customers/suggestions) shows unlinked bookings matching name/email. Booking list shows return customer icon (UserCheck) + clickable link to customer detail. Booking detail shows customer chip or "Link to Customer" button.U-01–U-05
FR-38Customer MergeP2✅ COMPLETEMerge duplicate customer profiles via POST /customers/:id/merge. Moves all bookings from source to target customer. Concatenates notes with ---\n[Merged from: sourceName] separator. Atomic $transaction. Audit logged. Requires DELETE permission.U-01–U-03

Phase 2.13 — P2: OTA Status Tracking

IDFeaturePriorityStatusDescriptionActor
FR-39OTA Status StorageP2✅ COMPLETEotaStatus (varchar 100, nullable) and otaStatusUpdatedAt (timestamptz, nullable) added to Booking entity (E-08). Populated during upsertFromOta() sync. Existing bookings unaffected. View-only; no interaction with internal status workflow (FR-25–FR-30).System
FR-40OTA Status NormalizationP2✅ COMPLETEOtaStatusDef entity (E-20) maps raw OTA status strings to display labels and colors per OTA type. CRUD via GET/POST/PATCH /ota-status + soft-delete/restore. Unique constraint on [rawStatus, otaType]. Graceful fallback: unknown statuses display raw string.U-01, U-02
FR-41OTA Status DisplayP2✅ COMPLETEBookings list (S-08) shows "OTA Status" column with colored badge (OtaStatusDef label/color when mapped, raw string fallback, "—" when null). Booking detail (S-09) shows OTA status + otaStatusUpdatedAt in Booking Info card. Independent otaStatus filter dropdown in bookings list.U-01–U-06
FR-42OTA Status AdminP2✅ COMPLETE"OTA Statuses" tab in Master Data (S-24). CRUD for OtaStatusDef mappings: raw status, OTA type, normalized label, color, sort order. OTA type filter. Soft delete/restore. Same admin pattern as Booking Statuses tab.U-01, U-02

Phase 2.14 — P2: List Page Layout Standardization

IDFeaturePriorityStatusDescriptionActor
FR-43Unified List Page LayoutP2✅ COMPLETEStandardize all list pages (S-03, S-08, S-21, S-26) to bookings-style layout: fixed h-14 header bar with inline search + action buttons, filter bar with removable FilterChip tags + dropdown selectors + Clear All, sticky uppercase table headers, compact cell padding (px-3 py-1.5), full pagination footer with rows selector + page numbers. Extract shared FilterChip and ResizableHeader components. Add Active/Inactive status filter to Customers page. Checkboxes remain bookings-only.U-01–U-07

Phase 2.15 — P2: BPM Process Management

IDFeaturePriorityStatusDescriptionActor
FR-44Process Type RegistryP2❌ NOT IMPLProcessType entity (E-21) defining available process types (booking, ota_sync, alert_resolution, customer_mgmt, ota_onboarding, property_setup). CRUD via admin API. Each type has key, label, entity_class, description. Seeded with 6 defaults matching BPM_SPEC.md P-01 to P-06.U-01, U-02
FR-45Generalized Status EngineP2❌ NOT IMPLExtend BookingStatusDef and BookingStatusTransition with process_type_id FK discriminator. Same transition validation, role checks, hooks — now entity-type-aware. Backward compatible: booking workflow via process_type_id='booking' default. Existing /booking-status/* endpoints remain as aliases. New generic /process-status/* endpoints added.System
FR-46Process Instance TrackingP2❌ NOT IMPLProcessInstance entity (E-22) tracking active process executions. Records: process_type, entity_id, current_status_key, started_at, started_by, completed_at. Status history via existing AuditLog (E-12). Dashboard widget showing active instances per process type.System, U-01–U-05
FR-47Process VisualizationP2❌ NOT IMPLExtend existing Mermaid workflow preview (S-20) to render any process type's state diagram. Process type selector dropdown in workflow config. BPMN-style Mermaid flowcharts for non-state-machine processes (P-02, P-05). Read-only process flow viewer accessible from sidebar. See BPM_SPEC.md for process definitions.U-01, U-02

Phase 2.16 — P1: Workflow PTX Integration (Approach A Retrofit)

Reference: docs/workflow_ptx.md — 12-flow SOP, 4-axis status system, SLAs, risk rules.
Approach: Retrofit existing 3-track BPM engine to match workflow_ptx.md 4-axis model. Add booking_source + booking_accounting process types, enrich Booking model, implement cross-axis validation rules.
Brainstorm: plans/reports/brainstorm-lean-260324-1159-workflow-integration.md

IDFeaturePriorityStatusDescriptionActor
FR-484-Axis Status AlignmentP1✅ COMPLETERetrofit 3-track → 4-axis status system per workflow_ptx.md §5. Track 1 booking_cs (CS Status): 14 statuses, 28 transitions. Track 2 booking_payment (Payment Status): 7 statuses, 10 transitions. Track 3 booking_source (Source Status): 4 statuses, 7 transitions. Track 4 booking_accounting (Accounting Status): 12 statuses, 18 transitions. ProcessInstance auto-created per booking. Seed file: seed-booking-workflow.ts. Note: Booking.status currently FK to CS statuses; will migrate to lifecycle statuses in FR-56.U-01, U-02
FR-49Booking Model EnrichmentP1❌ NOT IMPLAdd fields to Booking entity (E-08) for workflow tracking per workflow_ptx.md §4: guest_phone (varchar 50, nullable), guest_nationality (varchar 2, nullable), checkin_time (timestamptz, nullable — actual check-in time), checkout_time (timestamptz, nullable — actual checkout time), cost_amount (bigint, nullable — cost price from host), margin (bigint, nullable — auto-calc or manual: totalAmount - costAmount), special_requests (text, nullable), cs_owner_id (FK→User, nullable — CS staff assigned), source_owner_id (FK→User, nullable — Source staff assigned), booking_health (varchar 10: ok/warning/risk, nullable — auto-calculated from 4-axis state), risk_flags (JSONB array, nullable — auto-generated flag strings). Prisma migration. All new fields nullable for backward compat.System
FR-50Cross-Axis Validation RulesP1❌ NOT IMPLImplement 5 business rules from workflow_ptx.md §5.6 as transition hooks: R1: Check-in requires SOURCE=src_confirmed (block booking_ordercheckin_already unless Source track is confirmed). R2: Check-in alert if ACCOUNTING=acc_deposit_pending and check-in < 6h (ALERT, not hard block). R3: Host payment requires CHECKOUT (block booking_accountingacc_payment_paid unless CS track = checked_out). R4: Close order requires all money done (block booking_orderreview_order unless Payment + Accounting terminal). R5: Auto-risk flag generation on any ProcessInstance transition. Add new hook type cross_axis_check to BookingStatusTransition hooks JSONB.System
FR-51Booking Health Auto-CalcP2❌ NOT IMPLOn every ProcessInstance transition for a booking, auto-recalculate booking_health field based on 4-axis state combinations. 🟢 OK: all tracks progressing normally. 🟡 Warning: deposit pending, contact not made > 1h, source not confirmed within 24h. 🔴 Risk: missing deposit < 6h before check-in, cannot contact guest, source no_room, checkout but not paid host. Store risk flags in risk_flags JSONB. Hook type: recalc_health on all 4 process type transitions.System
FR-52SLA Definition & MonitorP2❌ NOT IMPLNew SlaDefinition entity (E-26) with: process_type_key, from_status_key, max_duration_minutes, escalation_level (1/2/3), alert_channel (system/group_chat), is_active. SLA Monitor BullMQ scheduled job (every 5 min) scans active ProcessInstances against SLA thresholds. Generates alerts when SLA breached. Per workflow_ptx.md §6: CS last-minute (10 min), 5-day advance (24h), peak season (D-90 to D-1). Escalation: L1 → L2 → L3 (critical). New alert types: sla_breach, missing_deposit, missing_contact, unconfirmed_checkin, payment_mismatch.System, U-01
FR-53Auto-Assign CS & SourceP3❌ NOT IMPLOn booking creation (sync or manual), auto-assign cs_owner_id and source_owner_id based on configurable rules: by property, by country, by staff workload balance. Rule config stored in Settings or separate assignment_rules table. Fallback: no auto-assign if no rules match (manual assignment required).System
FR-54Risk DashboardP2❌ NOT IMPLDashboard enhancements per workflow_ptx.md §8.4: (1) Real-time booking status chart showing 4-axis breakdown (pie/donut per track). (2) High-risk bookings list filtered to booking_health=risk. (3) Margin report (auto-calc from cost_amount/total_amount). (4) OTA Payout pending list (Payment track = not terminal). (5) Host payment pending list (Accounting track = payment_pending). New route: /dashboard/operations or additional panels on S-02.U-01–U-05
FR-55Auto-Flag SystemP2❌ NOT IMPLAuto-flag bookings matching risk conditions from workflow_ptx.md §5.6 Rule 5. Flag types: missing_deposit (Source confirmed but Accounting pending), missing_contact (CS < contacted and check-in < 6h), not_purchased (check-in < 24h, Source not confirmed), payment_mismatch (Payment=fully_paid but Accounting≠paid), checkout_not_paid_host (CS=checked_out but Accounting=pending). Flags shown in Booking list as colored icon badges. Filter by flag type.System

Phase 2.17 — P1: Unified Booking Lifecycle

Reference: plans/reports/brainstorm-lean-260324-1607-unified-booking-workflow.md
Approach: Create booking_lifecycle ProcessType that owns Booking.status. Department tracks (CS, Source, Payment, Accounting) run in parallel. Lifecycle auto-transitions via data-driven trigger rules stored in DB.

IDFeaturePriorityStatusDescriptionActor
FR-56Booking Lifecycle TrackP1❌ NOT IMPLNew booking_lifecycle ProcessType with 10 statuses representing end-to-end booking lifecycle: lc_new, lc_processing, lc_apt_confirmed, lc_ready_checkin, lc_checked_in, lc_checked_out, lc_closing, lc_closed (terminal), lc_cancelled (terminal), lc_no_show. Booking.status FK migrated from CS statuses to lifecycle statuses. Auto-create booking_lifecycle ProcessInstance on booking creation. Seed file updated with lifecycle statuses + transitions. Migration script maps existing CS-based booking statuses to lifecycle equivalents.U-01, U-02
FR-57Data-Driven Lifecycle Trigger EngineP1❌ NOT IMPLNew LifecycleTriggerRule entity (E-27) stores trigger rules in DB. When any department track (CS/Source/Payment/Accounting) transitions, engine queries matching trigger rules from DB, evaluates JSONB conditions (AND logic: check other tracks' statuses), and auto-transitions the booking lifecycle. ~17 rules seeded. Priority-based evaluation (higher wins). Supports currentLifecycle: null for any-stage rules (cancellation). *terminal* wildcard matches any terminal status. Admin can add/edit/disable rules without code deploy.System

Phase 3 — P3: Intelligence

IDFeaturePriorityStatusDescriptionActor
FR-13Rate Parity CheckerP3❌ NOT IMPLNot built.U-01
FR-14Revenue AnalyticsP3❌ NOT IMPLNot built.U-01
FR-15Rate Rules EngineP3❌ NOT IMPLNot built. DB models exist but no logic.U-01

4. Screen List (S-xx)

IDScreenPhaseStatusDescriptionRelated FR
S-01LoginP1Email/password authentication for staff
S-02DashboardP1Sync status overview with country tabs, alerts, recent bookings, KPI cardsFR-06, FR-07, FR-08
S-03Properties ListP1List all properties with sync health indicators, filterable by countryFR-05
S-04Property DetailP1Property info, room types, connected OTAs (via OTA accounts)FR-05
S-05OTA Accounts ListP1⚠️List works. But links to /ota-accounts/[id]404 (detail page not built). Test/refresh are stubs.FR-01
S-06Connect OTA AccountP1Multi-step wizard (select OTA → credentials → submit). No real Playwright test.FR-01
S-07Import PropertiesP1Not built. OTA adapter fetchProperties() is a stub. No import UI.FR-02
S-08Bookings ListP1Filters, search, pagination, resizable columns, CSV export. OTA Status column + independent filter (FR-41).FR-03, FR-41
S-09Booking DetailP1Guest/stay/property info, status transitions, revert, audit history. OTA status in Booking Info card (FR-41).FR-03, FR-26, FR-28, FR-41
S-10Availability CalendarP2Route /availability in sidebar Operations group. Property sidebar + 14-day grid. Color cells + block/unblock popover.FR-10
S-11Rate ManagerP2Rate manager form, preview table, bulk update with OTA adjustments. Route: /rates.FR-09
S-12Booking TimelineP2Not built. No route, no component.FR-11
S-13Rate Parity ReportP3Not built.FR-13
S-14Analytics DashboardP3Not built.FR-14
S-15Rate Rules ConfigP3Not built.FR-15
S-16SettingsP1All 7 tabs working: Users, Roles, Booking Statuses, Workflow, Preferences, Notifications, SystemFR-08
S-17Sync Job LogP1⚠️List/filter/auto-refresh works. Missing "Force Sync" button (backend endpoint exists).FR-04
S-18User ProfileP2Profile edit, password change, theme toggleFR-16, FR-17, FR-19
S-19Role ManagementP2Permission matrix editor for preset and custom roles. In Settings Roles tab.FR-20, FR-21, FR-23
S-20Workflow ConfigP2Transitions CRUD, hooks config, Mermaid preview. In Settings Workflow tab.FR-25, FR-28, FR-29, FR-30
S-21Suppliers ListP2Paginated table: code, name, country, phone, email, total rooms allocated. Import CSV. Export CSV.FR-31, FR-33
S-22Supplier DetailP2Basic info + bank details cards. Room allocations table grouped by property (replaces linked properties).FR-31, FR-33
S-23Supplier Allocation ManagerP2Modal on room type table row. Add/edit/remove supplier allocations per room type. Supplier search dropdown, inline edit, delete confirmation, mismatch warning.FR-31, FR-32
S-24Master DataP2Tabbed admin data management: Users (CRUD + role assign), Roles (list → S-19), Countries (color/sort edit), Booking Statuses (add/edit/soft-delete), OTA Statuses (FR-42). Route: /master-data.FR-20, FR-23, FR-42
S-25LogsP2Super-admin only. Two tabs: Activity Log (HTTP request stream, 5s auto-refresh, pause/resume, last 100 from activity_logs DB) and Audit Log (entity change history with diff viewer, filter by entityType/action, paginated 50/page from audit_logs DB). Route: /logs.FR-35, FR-27
S-26Customers ListP2Searchable table: name, email, phone, nationality, booking count, last stay. Create/edit via modal. Route: /customers.FR-36
S-27Customer DetailP2Contact info cards (editable), notes, booking history table, link/unlink bookings, merge with another customer. Route: /customers/[id].FR-36, FR-37, FR-38
S-28Process ManagementP2Process type list + per-type workflow config. Extends S-20 pattern with process type selector. Route: /settings/processes.FR-44, FR-45, FR-47
S-29Process InstancesP2Active/completed process instance viewer with filters by type, status, date. Route: /process-instances.FR-46
S-30Operations DashboardP24-axis booking status breakdown (pie/donut charts per track), high-risk bookings list, margin report, OTA payout pending, host payment pending. Route: /dashboard/operations or panel in S-02.FR-54
S-314-Axis Booking DetailP1Enhanced S-09 with 5 tracks: Lifecycle status (main badge) + 4 department status tracks sidebar (CS, Source, Accounting, Payment) — each showing current status badge + available transitions. Booking health indicator (🟢/🟡/🔴). Risk flags list. CS/Source owner assignment. Cost/margin display.FR-48, FR-49, FR-50, FR-51, FR-56
S-32Lifecycle Trigger Rules AdminP1Admin UI for managing LifecycleTriggerRule records (E-27). Table showing all rules with department track, status, current/target lifecycle, conditions, priority, active toggle. CRUD modal for add/edit. Seed data viewer. Accessible from Settings > Workflow tab or Workflow Designer.FR-57

5. Entity List (E-xx)

IDEntityDescriptionKey Fields
E-01UserInternal staff accountid, email, password_hash, name, role_id(FK→Role), country(nullable), locale(vi/id/ms/en, default:en), created_at
E-02PropertyHotel/lodging managed by operatorid, name, country(VN/ID/MY), timezone, currency, address, is_active. supplierId removed (migrated to E-17)
E-03RoomTypeRoom category within a propertyid, property_id, name, base_rate, total_rooms, max_occupancy, is_active
E-04OtaAccountTop-level OTA login account (one per OTA credential set)id, ota(booking/agoda/traveloka/expedia), label, credentials_encrypted, two_factor_method(none/totp/manual), two_factor_secret, session_data(jsonb), status(active/expired/error/requires_2fa), last_session_refresh, user_id, country_code, created_at
E-05OtaConnectionLightweight link between property and OTA accountid, property_id, ota_account_id, ota_property_id, is_active, created_at
E-06OtaRoomMappingMaps internal room type to OTA IDsid, room_type_id, ota_connection_id, ota_room_id, ota_rate_plan_id
E-07AvailabilityDaily availability per room typeid, room_type_id, date, total_rooms, booked_rooms, blocked_rooms
E-08BookingReservation from any OTAid, property_id, room_type_id, supplier_id(nullable FK→Supplier), customer_id(nullable FK→Customer), ota_type, ota_booking_id(unique), guest_name, guest_email, guest_phone(nullable varchar 50), guest_nationality(nullable varchar 2), check_in, check_out, checkin_time(nullable timestamptz), checkout_time(nullable timestamptz), num_rooms, num_guests, status(FK→BookingStatusDef), ota_status(nullable varchar 100), ota_status_updated_at(nullable timestamptz), total_amount, cost_amount(nullable bigint), margin(nullable bigint), currency, special_requests(nullable text), cs_owner_id(nullable FK→User), source_owner_id(nullable FK→User), booking_health(nullable varchar 10: ok/warning/risk), risk_flags(nullable JSONB), raw_data(jsonb), created_at
E-09SyncJobTrack sync operationsid, ota_connection_id, job_type(pull_bookings/push_availability/push_rates/verify), status(pending/running/completed/failed), error, payload(jsonb), started_at, completed_at
E-10RateDaily rate per room type per OTAid, room_type_id, ota_connection_id, date, rate_amount(integer), currency
E-11AlertOverbooking, sync, and workflow alertsid, property_id, alert_type(overbooking/sync_failure/session_expired/sla_breach/missing_deposit/missing_contact/unconfirmed_checkin/payment_mismatch), severity(critical/warning/info), message, is_resolved, resolved_by, created_at, resolved_at
E-12AuditLogTrack all system changesid, entity_type, entity_id, action(create/update/delete), old_value(jsonb), new_value(jsonb), performed_by, created_at
E-13RateRuleAutomated rate adjustment rulesid, property_id, room_type_id, ota_connection_id, rule_type(markup/discount/seasonal), value, start_date, end_date, is_active
E-14RoleNamed role with bitwise permissions per moduleid, name(unique key), label(display), description, permissions(JSONB: {module: bitmask}), is_system(bool), is_active, created_at
E-15BookingStatusTransitionState machine transition rule between two statusesid(UUID), from_key(FK→E-08b), to_key(FK→E-08b), allowed_roles(JSONB[]), hooks(JSONB[]), sort_order, is_active, created_at. UNIQUE(from_key, to_key), CHECK(from_key≠to_key)
E-16BookingStatusDef.uiConfigPer-role UI visibility config (JSONB column on E-08b){ "roleName": { sections: string[], buttons: string[], editableFields: string[] } }. Wildcard "*" as default. Controls booking detail page rendering per status×role
E-17SupplierRoomAllocationM:N junction linking Supplier to RoomType with room countid, supplier_id(FK→Supplier), room_type_id(FK→RoomType), room_count(int), notes(nullable), is_active(default true), created_at, updated_at. UNIQUE(supplier_id, room_type_id)
E-18ActivityLogHTTP request activity log (replaces flat-file)id(UUID), user_email(nullable varchar 255), user_id(nullable FK→User onDelete:SetNull), method(varchar 10: GET/POST/PUT/DELETE/PATCH/CLIENT), path(varchar 500), status(int: HTTP code or 0 for client events), screen(varchar 100: resolved UI screen name), created_at(timestamptz). Index: createdAt DESC, userEmail
E-19CustomerCS-managed guest profile (independent of OTA data)id(UUID), name(varchar 255), email(nullable varchar 255), phone(nullable varchar 50), nationality(nullable varchar 2), notes(nullable text), is_active(bool default true), created_at, updated_at. Booking.customer_id nullable FK→Customer. Indexes: name, email, isActive
E-20OtaStatusDefOTA status normalization mapping (raw→display)id(UUID), raw_status(varchar 100), ota_type(enum: booking_com/agoda/traveloka/expedia), normalized_label(varchar 100), color(varchar 7, hex), sort_order(int default 0), is_deleted(bool default false), created_at, updated_at. UNIQUE(raw_status, ota_type). Indexes: otaType, isDeleted
E-21ProcessTypeBPM process type registryid(UUID), key(varchar 50 unique), label(varchar 100), description(text nullable), entity_class(varchar 100: Booking/OtaAccount/Property/Alert/Customer), is_active(bool default true), created_at. Seeded: booking, ota_sync, alert_resolution, customer_mgmt, ota_onboarding, property_setup
E-22ProcessInstanceActive process execution trackerid(UUID), process_type_id(FK→E-21), entity_id(varchar 255: polymorphic ref), current_status_key(varchar 50), started_by(nullable FK→User), started_at(timestamptz), completed_at(nullable timestamptz), is_active(bool default true). Index: processTypeId+entityId, isActive
E-23OtaFormulaTemplateSystem-level formula template per OTA typeid(UUID), ota_type(varchar 30), name(varchar 100), description(nullable varchar 500), formula(text: safe math expression e.g. net / ((1-promote/100) * (1-commission/100))), parameters(JSONB: [{name, label, type, default, min, max}]), is_default(bool), is_active(bool default true), created_at, updated_at. Index: otaType. 5 defaults seeded (trip_com, agoda, expedia, booking_com, traveloka)
E-24OtaRateConfigPer-property×OTA rate configuration linking template to parameter valuesid(UUID), property_id(FK→Property), ota_connection_id(FK→OtaConnection, UNIQUE), formula_template_id(FK→E-23), parameter_values(JSONB: {commission:15, promote:5, surcharge:0}), is_active(bool default true), created_at, updated_at. Index: propertyId. Replaces RatePlan+RatePlanAdjustment
E-25OtaRateLineRate line variant within OTA config (member, campaign, etc.)id(UUID), rate_config_id(FK→E-24), name(varchar 100: "Basic"/"Member"/"Early Bird"), line_type(varchar 30: base/member/campaign), discount_percent(decimal 5,2: % off BAR, default 0), sort_order(int default 0), is_active(bool default true), created_at, updated_at. Index: rateConfigId
E-26SlaDefinitionSLA threshold definition per process statusid(UUID), process_type_key(FK→ProcessType.key), from_status_key(varchar 50), max_duration_minutes(int), escalation_level(int 1-3), alert_channel(varchar 30: system/group_chat), description(text nullable), is_active(bool default true), created_at, updated_at. Index: processTypeKey. FR-52
E-27LifecycleTriggerRuleData-driven lifecycle auto-transition rulesid(UUID), name(varchar 100), department_track(varchar 50: booking_cs/booking_source/booking_payment/booking_accounting), department_status(varchar 50: status key or terminal wildcard), current_lifecycle(nullable varchar 50: lifecycle must be in this status, null=any), target_lifecycle(varchar 50: target lifecycle status), conditions(JSONB: [{track, statusIn?, statusNotIn?}] — AND logic), priority(int default 0, higher=first), is_active(bool default true), created_at, updated_at. Index: [department_track, department_status], [is_active]. FR-57

6. Non-Functional Requirements

6.1 Performance

MetricTarget
Booking detection latency< 3 minutes (poll interval)
Availability push latency< 60 seconds after detection
End-to-end sync< 5 minutes
Dashboard load time< 2 seconds
Concurrent OTA sessions400+ (100 properties x 4 OTAs, shared via OTA accounts)

6.2 Security

RequirementImplementation
OTA credential storageAES-256-GCM encryption at rest
Internal authbcrypt password hashing, JWT sessions
Session managementHttpOnly cookies, CSRF protection
Data protectionPDPA/PDPD/PP71 compliant (VN/ID/MY)
Access controlBitwise permission-based (JSONB per module: V/C/E/D bitmask) + country scoping. 7 preset roles + custom roles
Audit trailEntity mutations logged in E-12 (audit_logs). HTTP request activity logged in E-18 (activity_logs)

6.3 Reliability

RequirementImplementation
Job retryBullMQ exponential backoff, max 3 retries
Session recoveryAuto-detect expired sessions, alert for re-login
Data consistencyPostgreSQL transactions for availability updates
MonitoringSync failure alerts within 1 minute
BackupDaily PostgreSQL backup

6.4 Scalability

DimensionTarget
Properties100+
Countries3 (VN, ID, MY)
OTA channels4
OTA accounts~12-20 (few accounts per OTA per country)
Total connections~400+
Bookings/day~200-800
Sync jobs/hour~4,800+ (staggered scheduling)

6.5 Tech Stack

ComponentTechnology
BackendNestJS (TypeScript)
FrontendNext.js 14+ (App Router, TypeScript)
DatabasePostgreSQL 16
Cache/QueueRedis 7 + BullMQ
ORMPrisma
Browser AutomationPlaywright
MonorepoTurborepo
AuthPassport.js + JWT (httpOnly cookie)
Notificationsnodemailer (LINE Notify stub only)

7. Key Decisions (D-xx)

IDDecisionContextChosenRationale
D-01OTA integration methodNo official API accessHTTP request replay + Playwright fallbackFastest path; replay intercepted extranet requests
D-02Backend languageRust vs TypeScriptTypeScript (NestJS)Solo dev; Playwright is JS-native; shared types with frontend
D-03Sync strategyPush-only vs push+verifyPush with polling verificationPush alone unreliable; periodic verification catches silent failures
D-04Anti-overbookingOptimistic vs pessimisticPessimistic locking + buffer roomsOverbooking cost is high; prefer false negatives over overbookings
D-05Currency handlingDecimal vs integerInteger (cents/smallest unit)VND and IDR have no decimals; consistent integer math avoids float errors
D-06TimezoneLocal vs UTC storageUTC storage, local displayIndonesia has 3 timezones; UTC avoids conversion bugs
D-07PMS scopeBuild vs buyDefer (out of scope)PMS is separate product; focus on channel management first
D-08OTA credential modelPer-property vs per-accountPer-account (OtaAccount entity)One OTA login manages N properties; avoids duplicate credentials and sessions
D-09Onboarding flowProperty-first vs OTA-firstOTA-firstMatches real workflow: OTA accounts exist before properties are added to system
D-10Country scopingGlobal access vs country-scopedCountry-scoped staff + global manager100+ properties across 3 countries; staff only needs their country
D-11Supported countriesTH/VN/ID vs VN/ID/MYVN/ID/MY onlyThailand removed per business decision; Malaysia added
D-12OTA-country mappingDB table vs config constantHardcoded config constantYAGNI — 4 OTAs x 3 countries = 12 combos. Static config sufficient
D-13Permission modelEnum roles vs bitwise permissionsBitwise JSONB permissionsSystem expanding to ERP; enum can't express per-module CRUD. JSONB {module: bitmask} is extensible, uses bitwise AND for checks, and supports unlimited modules without schema changes
D-14Permission storageSingle BigInt vs JSONB mapJSONB map per moduleBigInt limited to 16 modules (64 bits / 4 bits). JSONB supports unlimited modules, is human-readable in queries, and PostgreSQL indexes JSONB efficiently
D-15Role presetsHardcoded vs DB-storedDB-stored with is_system flagPresets seeded on deploy, protected from deletion. Custom roles addable by admin. Avoids code changes for new roles
D-16Client-side caching libraryReact Query vs SWR vs manualSWR4KB bundle, hooks-only API, built by Vercel for Next.js. Stale-while-revalidate pattern fits this app's simple GET/mutate workflows. React Query is overkill for current needs
D-17Workflow data modelAll-JSONB vs fully-normalized vs hybridHybrid (transition table + JSONB uiConfig)Transitions need FK integrity and queryability (relational). UI config is always read as blob (JSONB). Best balance of normalization and pragmatism
D-18Hooks locationHooks on statuses vs transitionsHooks on transitionsSame status pair can have different hooks depending on direction. E.g., confirmed→cancelled triggers availability restore, but cancelled→confirmed does not. Transition-level hooks are more precise
D-19Supplier model levelProperty-level vs room-type-levelRoom-type-level (M:N junction)One supplier provides rooms across multiple properties. Same room type can be split across suppliers by count. Accounting-only — zero OTA sync impact
D-20Supplier allocation validationHard block vs soft warningSoft warningSUM(roomCount) != totalRooms shows warning in API response and UI badge. Not blocked — allows flexible allocation during data entry
D-21Supplier allocation deleteHard delete vs soft deleteSoft delete (isActive)Consistent with other entities. Inactive allocations excluded from SUM checks and display
D-22Activity log storageFlat-file vs Kafka vs PostgresPostgres (activity_logs table)Flat-file not queryable, lost on redeploy. Kafka is over-engineering for this scale (single monolith, B2B tool). Postgres already present — zero new infrastructure, ACID, indexed, queryable. Same API response shape preserved so frontend unchanged
D-23Customer identificationAuto-match vs manual linkingManual linking by CSAuto-matching on name/email produces false positives (same name, different guest; OTAs use different emails). CS manually creates profiles and links bookings — zero false positives. Suggestion engine shows similar unlinked bookings to speed up workflow
D-24Customer scopeCountry-scoped vs globalGlobal (no country scoping)Guests travel across countries. CS needs cross-country view of guest history. Customer entity is not country-scoped
D-25Merge notes strategyKeep target vs concatenateConcatenate with separatorAppend source notes below target with ---\n[Merged from: sourceName] separator. No data loss on merge
D-26OTA status trackingAuto-map to internal vs view-onlyView-only with optional normalizationOTA status stored as raw string, completely independent of internal workflow. Optional OtaStatusDef table normalizes raw strings to display labels/colors per OTA. Graceful fallback: unknown statuses display raw. Zero coupling with internal status engine
D-27BPM engine approachCamunda vs bpmn-engine vs extend currentExtend current engineSolo dev, zero budget, internal tool. Current state machine covers 90% of needs. Add entity_type discriminator for multi-process support. Full BPMN runtime is YAGNI. ISO 19510 referenced for documentation, not runtime compliance. See BPM_SPEC.md
D-28Process documentation formatBPMN XML vs Mermaid diagramsMermaid flowchartsAlready used for workflow preview (S-20). Renders inline in markdown. No external tooling needed. Covers 80% of BPMN notation visually
D-29Rate calculation approachVisual formula builder vs expression input vs template+paramsTemplate + configurable parametersReal OTA formulas follow 2-3 patterns. Templates are system-managed (dev-reviewed), users only fill numeric parameter values. Covers 95%+ of cases. Visual builder is 2-4 weeks YAGNI; expression input is error-prone. mathjs used for safe evaluation (no eval())
D-30Rate line derivationIndependent rates vs BAR-derivedBAR-derived with discount %Calculate BAR once via formula, derive member/campaign lines as % discount from BAR. Simpler model, fewer parameters, matches real OTA pricing structure

8. OTA-Country Availability Matrix

OTAVNIDMY
Booking.comYesYesYes
AgodaYesYesYes
TravelokaYesYesYes
ExpediaYesYesYes

All 4 OTAs available in all 3 countries. Matrix exists as guardrail for future additions.


9. Out of Scope

  • PMS features (check-in/out, housekeeping, invoicing) — note: basic guest profiles (E-19) added for CS use
  • Direct booking engine
  • Mobile native app (responsive web sufficient)
  • Multi-language UI (English-only, internal tool)
  • Payment processing (handled by OTAs)
  • Guest communication (handled by OTAs)
  • SaaS/multi-tenant architecture
  • Official OTA API partnerships (future migration path)
  • Thailand market (removed)

10. Assumptions

#AssumptionRisk if Wrong
1OTA extranet API calls can be captured and replayedEntire approach fails
2OTA sessions persist for hours/daysFrequent re-auth breaks automation
33-5 min sync latency acceptableOverbookings still occur within window
4OTAs won't detect/block automation at low volumeAccount ban risk
5Staff will adopt the dashboardTool goes unused
6Room type mappings between OTAs are stableMapping breaks cause wrong updates
7One OTA account manages multiple propertiesSchema design based on this

11. Risks

RiskSeverityLikelihoodMitigation
OTA detects automation, bans accountCRITICALMediumRate limiting, human-like request patterns, start with 2-3 test properties
OTA extranet API changesHIGHHighIsolated adapters per OTA, health monitoring, fast-fix pipeline
2FA blocks automationHIGHHighTOTP: automate via secret. SMS/Email: manual login + long session persistence
Solo dev maintenance burdenHIGHHighStart with 2 OTAs in Phase 1, stabilize before expanding
Overbooking within sync windowMEDIUMMediumBuffer rooms, pessimistic locking, instant alerts
Cross-OTA property matching errorMEDIUMLowAlways show confirmation UI, allow undo, manual override
400+ connections overwhelm serverMEDIUMMediumCountry-scoped filtering reduces active load; staggered scheduling

PTX Channel Manager — Internal Documentation