HotelHub HMSDocs

03 β€” Architecture #

Modular monolith Laravel. Service layer + event-driven + repository pattern. Same codebase untuk standalone & SaaS via APP_MODE flag.


1. Architectural Principles #

  1. Modular monolith β€” semua modul (FO, CM, BE, POS, Acc, AI, pSEO) dalam 1 Laravel app, dipisah via app/Modules/{Module}/ namespace.
  2. Service layer β€” logic bisnis di Services\*, controller hanya orchestrate.
  3. Event-driven β€” operasi penting fire event; listener tangani side-effect (queue, GL post, notification).
  4. Repository pattern β€” abstract DB access di Repositories\* untuk testability.
  5. Format-based adapter β€” integrasi pihak ketiga via adapter berdasarkan format API, bukan per vendor (sesuai global rule "no hardcoded providers").
  6. Standalone-first, SaaS-compatible β€” codebase identik, mode dipilih runtime via APP_MODE.
  7. Append-only audit β€” semua perubahan kritis tercatat di audit_logs.
  8. Idempotency by default β€” semua webhook & external mutation pakai idempotency_key.

2. Folder Structure #

app/
β”œβ”€β”€ Console/
β”‚   └── Commands/                 # artisan commands
β”‚       β”œβ”€β”€ NightAuditCommand.php
β”‚       β”œβ”€β”€ OtaSyncCommand.php
β”‚       β”œβ”€β”€ PseoSitemapCommand.php
β”‚       └── LicenseHeartbeatCommand.php
β”œβ”€β”€ Events/                        # domain events
β”‚   β”œβ”€β”€ ReservationCreated.php
β”‚   β”œβ”€β”€ ReservationCheckedIn.php
β”‚   β”œβ”€β”€ FolioCharged.php
β”‚   β”œβ”€β”€ PaymentReceived.php
β”‚   β”œβ”€β”€ OtaBookingIngested.php
β”‚   └── ...
β”œβ”€β”€ Listeners/                     # event handlers (queue-able)
β”‚   β”œβ”€β”€ PostFolioToGl.php
β”‚   β”œβ”€β”€ SendConfirmationEmail.php
β”‚   β”œβ”€β”€ SyncRoomToOta.php
β”‚   β”œβ”€β”€ NotifyHousekeeping.php
β”‚   └── ...
β”œβ”€β”€ Http/
β”‚   β”œβ”€β”€ Controllers/
β”‚   β”‚   β”œβ”€β”€ Admin/                # admin panel routes
β”‚   β”‚   β”œβ”€β”€ Owner/                # owner-only routes
β”‚   β”‚   β”œβ”€β”€ Staff/                # staff (FO/HK/POS/Acc) routes
β”‚   β”‚   β”œβ”€β”€ Guest/                # public guest portal
β”‚   β”‚   β”œβ”€β”€ Api/                  # REST API
β”‚   β”‚   └── Webhook/              # OTA webhooks, payment callbacks
β”‚   β”œβ”€β”€ Middleware/
β”‚   β”‚   β”œβ”€β”€ RequirePair.php       # license pairing gate
β”‚   β”‚   β”œβ”€β”€ EnsureProperty.php    # multi-property scope
β”‚   β”‚   β”œβ”€β”€ TenantInitializer.php # SaaS only
β”‚   β”‚   └── AuditLogger.php
β”‚   └── Requests/                 # FormRequest validators
β”œβ”€β”€ Models/
β”‚   β”œβ”€β”€ Property.php
β”‚   β”œβ”€β”€ Room.php
β”‚   β”œβ”€β”€ RoomType.php
β”‚   β”œβ”€β”€ RatePlan.php
β”‚   β”œβ”€β”€ Reservation.php
β”‚   β”œβ”€β”€ Guest.php
β”‚   β”œβ”€β”€ Folio.php
β”‚   β”œβ”€β”€ Charge.php
β”‚   β”œβ”€β”€ Payment.php
β”‚   β”œβ”€β”€ HousekeepingTask.php
β”‚   β”œβ”€β”€ PosOrder.php
β”‚   β”œβ”€β”€ GlAccount.php
β”‚   β”œβ”€β”€ JournalEntry.php
β”‚   β”œβ”€β”€ Provider.php              # BYOK integration provider
β”‚   β”œβ”€β”€ License.php
β”‚   └── ...
β”œβ”€β”€ Modules/                       # business logic per modul
β”‚   β”œβ”€β”€ FrontOffice/
β”‚   β”‚   β”œβ”€β”€ Services/
β”‚   β”‚   β”‚   β”œβ”€β”€ ReservationService.php
β”‚   β”‚   β”‚   β”œβ”€β”€ CheckInService.php
β”‚   β”‚   β”‚   β”œβ”€β”€ NightAuditService.php
β”‚   β”‚   β”‚   └── RoomAssignmentService.php
β”‚   β”‚   β”œβ”€β”€ Repositories/
β”‚   β”‚   β”œβ”€β”€ DTOs/
β”‚   β”‚   └── Policies/
β”‚   β”œβ”€β”€ ChannelManager/
β”‚   β”‚   β”œβ”€β”€ Services/
β”‚   β”‚   β”œβ”€β”€ Adapters/             # format-based adapters
β”‚   β”‚   β”‚   β”œβ”€β”€ BookingComAdapter.php
β”‚   β”‚   β”‚   β”œβ”€β”€ AgodaAdapter.php
β”‚   β”‚   β”‚   β”œβ”€β”€ TravelokaAdapter.php
β”‚   β”‚   β”‚   └── ChannelAdapterInterface.php
β”‚   β”‚   β”œβ”€β”€ Jobs/
β”‚   β”‚   β”‚   β”œβ”€β”€ PushAvailabilityJob.php
β”‚   β”‚   β”‚   β”œβ”€β”€ PullBookingsJob.php
β”‚   β”‚   β”‚   └── ResolveConflictJob.php
β”‚   β”‚   └── DTOs/
β”‚   β”œβ”€β”€ BookingEngine/
β”‚   β”œβ”€β”€ Pos/
β”‚   β”œβ”€β”€ Housekeeping/
β”‚   β”œβ”€β”€ Accounting/
β”‚   β”‚   β”œβ”€β”€ Services/
β”‚   β”‚   β”‚   β”œβ”€β”€ GlPostingService.php
β”‚   β”‚   β”‚   β”œβ”€β”€ ReportService.php
β”‚   β”‚   β”‚   └── EFakturService.php
β”‚   β”‚   └── Posting/
β”‚   β”‚       └── PostingRules.php
β”‚   β”œβ”€β”€ Ai/
β”‚   β”‚   β”œβ”€β”€ Services/
β”‚   β”‚   β”‚   β”œβ”€β”€ AiClient.php
β”‚   β”‚   β”‚   └── ConciergeService.php
β”‚   β”‚   β”œβ”€β”€ Adapters/             # FORMAT-based, not vendor
β”‚   β”‚   β”‚   β”œβ”€β”€ OpenAICompatibleAdapter.php
β”‚   β”‚   β”‚   β”œβ”€β”€ AnthropicFormatAdapter.php
β”‚   β”‚   β”‚   β”œβ”€β”€ GeminiFormatAdapter.php
β”‚   β”‚   β”‚   └── AiAdapterInterface.php
β”‚   β”‚   └── DTOs/
β”‚   β”œβ”€β”€ Payment/
β”‚   β”‚   β”œβ”€β”€ Adapters/             # FORMAT-based payment
β”‚   β”‚   β”‚   β”œβ”€β”€ RedirectFlowAdapter.php
β”‚   β”‚   β”‚   β”œβ”€β”€ EmbedFlowAdapter.php
β”‚   β”‚   β”‚   β”œβ”€β”€ QrisFlowAdapter.php
β”‚   β”‚   β”‚   └── PaymentAdapterInterface.php
β”‚   β”‚   └── Services/
β”‚   β”œβ”€β”€ Pseo/
β”‚   β”‚   β”œβ”€β”€ Services/
β”‚   β”‚   β”‚   β”œβ”€β”€ PseoRouteResolver.php
β”‚   β”‚   β”‚   β”œβ”€β”€ ContentGenerator.php
β”‚   β”‚   β”‚   └── SitemapGenerator.php
β”‚   β”‚   └── Templates/
β”‚   β”œβ”€β”€ Compliance/                # Indonesia-specific
β”‚   β”‚   β”œβ”€β”€ Services/
β”‚   β”‚   β”‚   β”œβ”€β”€ Pb1Service.php
β”‚   β”‚   β”‚   β”œβ”€β”€ EFakturService.php
β”‚   β”‚   β”‚   β”œβ”€β”€ LaporWnaService.php
β”‚   β”‚   β”‚   └── KtpOcrService.php
β”‚   β”œβ”€β”€ License/
β”‚   β”‚   β”œβ”€β”€ Services/
β”‚   β”‚   β”‚   β”œβ”€β”€ LicenseClient.php       # adopt v3 from whitelabel
β”‚   β”‚   β”‚   └── PairingService.php
β”‚   β”‚   └── Middleware/
β”‚   β”œβ”€β”€ Tenancy/
β”‚   β”‚   β”œβ”€β”€ Bootstrappers/
β”‚   β”‚   └── Services/
β”‚   └── ...
β”œβ”€β”€ Providers/
β”‚   β”œβ”€β”€ AppServiceProvider.php
β”‚   β”œβ”€β”€ EventServiceProvider.php
β”‚   β”œβ”€β”€ RouteServiceProvider.php
β”‚   β”œβ”€β”€ ModuleServiceProvider.php  # auto-register modules
β”‚   β”œβ”€β”€ TenancyServiceProvider.php # SaaS only
β”‚   └── AdapterServiceProvider.php # bind format adapters
└── Support/
    β”œβ”€β”€ Money.php                  # value object Rp dengan presisi
    β”œβ”€β”€ DateRange.php
    └── ...

config/
β”œβ”€β”€ app.php
β”œβ”€β”€ hotel.php                      # global hotel config
β”œβ”€β”€ modes.php                      # standalone / saas mode flags
β”œβ”€β”€ tenancy.php                    # SaaS only
β”œβ”€β”€ license.php                    # client kit v3 config
β”œβ”€β”€ ai.php                         # default adapter config
β”œβ”€β”€ ota.php                        # OTA defaults
β”œβ”€β”€ pseo.php                       # pSEO routes & templates
└── ...

database/
β”œβ”€β”€ migrations/
β”‚   β”œβ”€β”€ shared/                    # jalan di standalone & SaaS tenant DB
β”‚   β”œβ”€β”€ landlord/                  # SaaS central DB only
β”‚   └── tenant/                    # SaaS tenant DB only (= shared sebenarnya)
β”œβ”€β”€ seeders/
β”‚   β”œβ”€β”€ ProductionSeeder.php
β”‚   β”œβ”€β”€ DemoSeeder.php
β”‚   └── ...
└── factories/

resources/
β”œβ”€β”€ views/
β”‚   β”œβ”€β”€ layouts/
β”‚   β”œβ”€β”€ admin/
β”‚   β”œβ”€β”€ owner/
β”‚   β”œβ”€β”€ staff/
β”‚   β”œβ”€β”€ guest/                     # public booking engine
β”‚   β”œβ”€β”€ pseo/
β”‚   └── license/
β”‚       β”œβ”€β”€ pair-wizard.blade.php
β”‚       └── pair-success.blade.php
β”œβ”€β”€ js/
└── css/

routes/
β”œβ”€β”€ web.php
β”œβ”€β”€ admin.php
β”œβ”€β”€ owner.php
β”œβ”€β”€ staff.php
β”œβ”€β”€ guest.php
β”œβ”€β”€ api.php
β”œβ”€β”€ webhook.php
β”œβ”€β”€ pseo.php
β”œβ”€β”€ pair-routes.php                # license v3 from kit
└── tenant.php                     # SaaS only

storage/
└── app/
    β”œβ”€β”€ llm-presets/               # JSON presets (autofill UI)
    β”œβ”€β”€ ota-presets/
    β”œβ”€β”€ payment-presets/
    └── lock-presets/

3. Layer Diagram #

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  HTTP Layer (Controllers)                                   β”‚
β”‚  β€’ Validate request via FormRequest                         β”‚
β”‚  β€’ Authorize via Policy                                     β”‚
β”‚  β€’ Delegate to Service                                      β”‚
β”‚  β€’ Format response (Resource / Blade view)                  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
               β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Service Layer (app/Modules/*/Services/*)                   β”‚
β”‚  β€’ Business logic                                           β”‚
β”‚  β€’ Transaction management                                   β”‚
β”‚  β€’ Event dispatching                                        β”‚
β”‚  β€’ Coordinate Repositories + Adapters                       β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
               β”‚                     β”‚
       β”Œβ”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”
       β”‚ Repositories  β”‚    β”‚ Adapters         β”‚
       β”‚ (DB access)   β”‚    β”‚ (External APIs)  β”‚
       β”‚ Eloquent +    β”‚    β”‚ Format-based,    β”‚
       β”‚ scopes        β”‚    β”‚ BYOK config      β”‚
       β””β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
               β”‚                     β”‚
       β”Œβ”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”
       β”‚ MySQL/Postgresβ”‚    β”‚ External: OTA,   β”‚
       β”‚ via Eloquent  β”‚    β”‚ Payment, AI, WA, β”‚
       β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β”‚ SMS, Lock        β”‚
                            β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Event Bus (Laravel Events + Queue)                         β”‚
β”‚  β€’ Async listeners β†’ side effects                           β”‚
β”‚  β€’ GL posting, OTA sync, email, audit log                   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

4. Event Flow Examples #

A. Guest checks in via FO #

[FO staff] click "Check-in" on reservation
    ↓
ReservationController@checkIn
    ↓
CheckInService::execute(ReservationId)
    β”œβ”€ Update reservation.status = 'in_house'
    β”œβ”€ Update room.status = 'occupied'
    β”œβ”€ Open folio if not exists
    β”œβ”€ Auto-post room charge (1st night)
    β”œβ”€ Generate registration card PDF
    └─ Fire event: ReservationCheckedIn
        ↓
        Listeners (async via queue):
          β”œβ”€ SendCheckInEmailListener     β†’ mail queue
          β”œβ”€ NotifyHousekeepingListener   β†’ realtime ws + db
          β”œβ”€ AuditLogListener              β†’ audit_logs append
          β”œβ”€ SyncRoomToOtaListener         β†’ ota-sync queue (block other OTAs)
          β”œβ”€ PostFolioToGlListener         β†’ GL journal entry
          β”œβ”€ TriggerLaporWnaListener       β†’ if guest WNA, queue lapor
          └─ AwardLoyaltyPointsListener    β†’ if member

B. OTA booking ingest (Booking.com webhook) #

POST /webhook/ota/booking-com (HMAC verified)
    ↓
WebhookController@handleBookingCom
    ↓
ProcessOtaBookingJob::dispatch($payload) β†’ ota-ingest queue
    ↓
[worker] OtaBookingIngestService::ingest($payload)
    β”œβ”€ Map OTA room type β†’ internal RoomType
    β”œβ”€ Find or create Guest (email match or create new)
    β”œβ”€ Create Reservation
    β”œβ”€ Open folio + post charges
    β”œβ”€ Mark inventory taken (sync back to other OTAs)
    └─ Fire: OtaBookingIngested
        ↓
        Listeners:
          β”œβ”€ SendConfirmationEmail         (if email present)
          β”œβ”€ SyncOtherOtasListener         β†’ push reduced ARI to all other OTAs
          β”œβ”€ AuditLog
          └─ NotifyFrontOffice             β†’ realtime

C. AI Concierge guest message #

[Guest WhatsApp] "Boleh extend checkout sampai 2 PM?"
    ↓
WhatsAppWebhookController@receive
    ↓
ConciergeService::handleMessage($from, $text)
    β”œβ”€ Lookup guest by phone
    β”œβ”€ Build context: reservation, room, current charges
    β”œβ”€ AiClient::ask(systemPrompt, userText, context)
    β”‚     ↓
    β”‚   AdapterFactory::resolve(activeProvider) β†’ e.g. OpenAICompatibleAdapter
    β”‚     ↓
    β”‚   POST {base_url}/chat/completions (BYOK API key)
    β”‚     ↓
    β”‚   Return AI text + structured action (if any)
    β”œβ”€ If action = "request_late_checkout":
    β”‚     LateCheckoutService::request(reservation, until=14:00)
    β”‚     ↓ Fire ReservationModified event
    └─ Send AI reply via WhatsApp

5. Adapter Pattern (BYOK rule) #

Interface #

// app/Modules/Ai/Adapters/AiAdapterInterface.php
interface AiAdapterInterface
{
    public function chat(array $messages, array $options = []): AiChatResponse;
    public function listModels(): array;
    public function tokenCount(string $text): int;
}

Format-based implementations #

// OpenAI-compatible (covers DeepSeek, Groq, Together, Fireworks,
// Mistral, DeepInfra, OpenRouter, Cerebras, OpenAI itself,
// Ollama, LM Studio, vLLM)
class OpenAICompatibleAdapter implements AiAdapterInterface { ... }

// Anthropic format
class AnthropicFormatAdapter implements AiAdapterInterface { ... }

// Gemini format
class GeminiFormatAdapter implements AiAdapterInterface { ... }

Resolution #

// app/Modules/Ai/Services/AiClient.php
public function ask(string $prompt): AiChatResponse
{
    $provider = Provider::active('ai')->first();
    $adapter = AdapterFactory::for($provider->api_format)
        ->withConfig([
            'base_url'       => $provider->base_url,
            'api_key'        => decrypt($provider->api_key_encrypted),
            'extra_headers'  => $provider->extra_headers ?? [],
            'model'          => $provider->default_model,
        ]);

    return $adapter->chat([
        ['role' => 'system', 'content' => $this->systemPrompt()],
        ['role' => 'user',   'content' => $prompt],
    ]);
}

Hard rule: TIDAK ADA class bernama OpenAIAdapter, MidtransAdapter, BookingComAdapter dengan logic vendor-specific. Class adapter purely format-based.

Untuk OTA (yang spec API-nya per-vendor sangat unik) β€” pengecualian terbatas: BookingComAdapter, AgodaAdapter, TravelokaAdapter boleh ada karena API mereka bukan "format keluarga". Tapi tetap implement ChannelAdapterInterface shared. (Ini decision pragmatik β€” payment & AI bisa format-based, OTA tidak.)


6. Mode Selection #

// config/modes.php
return [
    'mode' => env('APP_MODE', 'standalone'), // standalone | saas
    'tenancy_enabled' => env('APP_MODE') === 'saas',
    'license_required' => env('APP_MODE') === 'standalone',
];

// app/Providers/AppServiceProvider.php
public function register()
{
    if (config('modes.tenancy_enabled')) {
        $this->app->register(TenancyServiceProvider::class);
    }
    if (config('modes.license_required')) {
        $this->app->register(LicenseServiceProvider::class);
    }
}

// bootstrap/app.php
->withMiddleware(function (Middleware $middleware) {
    $middleware->web(append: [
        // Standalone only
        \App\Modules\License\Middleware\RequirePair::class,
        // SaaS only
        \App\Modules\Tenancy\Middleware\TenantInitializer::class,
    ]);
})

Middleware self-skip jika mode tidak match.


7. Tenancy Strategy (SaaS) #

stancl/tenancy v4. DB-per-tenant, subdomain identification.

Request β†’ app.{tenant}.hotelxyz.com
    ↓
TenantInitializer middleware
    β”œβ”€ Resolve tenant by subdomain
    β”œβ”€ Switch DB connection: tenant_<uuid>
    β”œβ”€ Switch cache prefix: tenant:<uuid>:
    β”œβ”€ Switch storage path: tenants/<uuid>/
    └─ Continue request

Central DB tetap accessible via central() helper untuk billing, plan, etc.

Detail: 18-SAAS_UPGRADE_PATH.md.


8. RBAC #

spatie/laravel-permission. Roles:

Permission granular: reservation.create, reservation.cancel, folio.transfer, gl.post, ota.sync, dll.

Detail matrix: 15-ADMIN_SECURITY.md.


9. Audit Log #

spatie/laravel-activitylog + custom append-only constraint.

Tracked actions:

Append-only enforcement: trigger DB-level + Eloquent observer prevent UPDATE/DELETE pada audit_logs.


10. Idempotency #

Semua POST yang mutate state (booking create, payment, OTA push, etc.) accept Idempotency-Key header. Service cek di Redis (TTL 24h) β€” kalau sudah ada response cached, return tanpa eksekusi ulang.

$key = $request->header('Idempotency-Key');
if ($cached = Cache::get("idem:{$key}")) {
    return $cached;
}
$response = $this->service->execute(...);
Cache::put("idem:{$key}", $response, 86400);
return $response;

11. Event Catalog (key events) #

Event Fired oleh Listener (sample)
ReservationCreated ReservationService SendConfirmationEmail, SyncRoomToOta, PostFolioToGl
ReservationCheckedIn CheckInService NotifyHousekeeping, TriggerLaporWna, AuditLog
ReservationCheckedOut CheckOutService FinalizeFolio, PostGlClose, AwardLoyaltyPoints
ReservationCancelled ReservationService RefundIfApplicable, ReleaseInventory, NotifyOta
FolioCharged ChargeService RecalculateFolio, PostGl
PaymentReceived PaymentService UpdateFolioBalance, AuditLog, IssueReceipt
OtaBookingIngested OtaBookingService SendConfirmationEmail, SyncOtherOtas, NotifyFo
RoomStatusChanged HousekeepingService NotifyFo, BroadcastWs
PosOrderClosed PosService PostToFolioOrPayment, KitchenComplete
NightAuditCompleted NightAuditService GenerateDailyReport, EmailManagement
LicensePaired LicenseClient EmitInstallation, AuditLog
LicenseRevoked LicenseClient LogOutSessions, NotifyOwner

12. Naming Conventions #

Layer Pattern
Controller XxxController (singular)
Service XxxService
Repository XxxRepository
Adapter FormatAdapter (e.g. OpenAICompatibleAdapter)
Job VerbXxxJob (e.g. PushAvailabilityJob)
Event NounVerbedEvent past-tense (e.g. ReservationCheckedIn)
Listener VerbNounListener (e.g. SendConfirmationEmailListener)
Migration 2026_04_28_create_reservations_table
Route name dot.case (e.g. reservations.create)
Permission dot.case (e.g. reservation.create)
Test class XxxTest mirrors structure

13. Coding Standards #


14. Testing Strategy #

Coverage target: 80% line coverage modul accounting + tax + pricing (highest risk). 50% baseline lain.

Detail: 21-QA_CHECKLIST.md.


15. Technical Debt Policy #