Skip to content

System Requirement Definition (SRD)

ProjectPTX Channel Manager (ptx-cm)
Version2.1.0
Date2026-02-18
TypeInternal 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-09Bulk Rate UpdateP2❌ NOT IMPLRate and RateRule models exist in Prisma but no service, no controller, no API endpoints, no frontend UI. push_rates job type defined but no processor.U-02
FR-10Availability CalendarP2❌ NOT IMPLNo route, no component, no page. Not built at all.U-01, U-02
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❌ NOT IMPLM:N junction table linking Supplier↔RoomType with roomCount. Replace property-level supplier link. Soft warning when SUM(allocations) != totalRooms.U-01, U-02, U-04
FR-32Supplier Allocation CRUDP2❌ NOT IMPLCreate/update/delete allocations per room type. Soft delete (isActive). Both entry points: room-type context and supplier context.U-01, U-02, U-04
FR-33Supplier Allocation DisplayP2❌ NOT IMPLRoom type table shows supplier badges with room counts. Warning badge on mismatch. Supplier list shows total rooms allocated. Supplier detail shows allocations grouped by property.U-01–U-07
FR-34Booking Supplier AttributionP2❌ NOT IMPLOptional nullable supplierId on Booking for future manual attribution. Column ready, no UI yet.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 exportFR-03
S-09Booking DetailP1Guest/stay/property info, status transitions, revert, audit historyFR-03, FR-26, FR-28
S-10Availability CalendarP2Not built. No route, no component.FR-10
S-11Rate ManagerP2Not built. No route, no component, no backend.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. Shows total vs allocated with warning.FR-31, FR-32

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, future attribution), ota_type, ota_booking_id(unique), guest_name, guest_email, check_in, check_out, num_rooms, num_guests, status(confirmed/cancelled/no_show), total_amount, currency, 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 and sync failure alertsid, property_id, alert_type(overbooking/sync_failure/session_expired), 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)

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 trailAll mutations logged in E-12

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

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 (guest management, check-in/out, housekeeping, invoicing)
  • 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