03 β Architecture #
Modular monolith Laravel. Service layer + event-driven + repository pattern. Same codebase untuk standalone & SaaS via
APP_MODEflag.
1. Architectural Principles #
- Modular monolith β semua modul (FO, CM, BE, POS, Acc, AI, pSEO) dalam 1 Laravel app, dipisah via
app/Modules/{Module}/namespace. - Service layer β logic bisnis di
Services\*, controller hanya orchestrate. - Event-driven β operasi penting fire event; listener tangani side-effect (queue, GL post, notification).
- Repository pattern β abstract DB access di
Repositories\*untuk testability. - Format-based adapter β integrasi pihak ketiga via adapter berdasarkan format API, bukan per vendor (sesuai global rule "no hardcoded providers").
- Standalone-first, SaaS-compatible β codebase identik, mode dipilih runtime via
APP_MODE. - Append-only audit β semua perubahan kritis tercatat di
audit_logs. - 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:
- owner β semua, tidak bisa di-revoke
- manager β semua kecuali billing & owner-level config
- front_office β reservation, guest, folio, payment
- housekeeping β room status, task
- pos_cashier β POS only + folio post
- accounting β GL, AR/AP, report, e-Faktur
- revenue_manager β rate plan, channel manager
- maintenance β work order
- hr β employee, payroll (Phase 2)
- read_only_auditor β view all, no edit
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:
- All
Reservation,Folio,Charge,Payment,JournalEntryCUD - All login/logout, 2FA, password reset
- All RBAC role/permission changes
- All admin config changes (BYOK provider add/edit/delete β tanpa expose key)
- All license activate/heartbeat/revoke
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 #
- PSR-12 + Laravel Pint default
- PHPStan level 6 minimum (level 8 target untuk new code)
- Strict types declared:
declare(strict_types=1); - Eloquent OK untuk queries simple; DB query builder untuk reporting (faster, less hydration)
- DTOs untuk data transfer cross-layer (
spatie/laravel-dataringan) - No
mixedkecuali strict-typed downstream - No facades di service layer β inject via constructor
14. Testing Strategy #
- Unit tests β service methods, adapter contract conformance
- Feature tests β HTTP request β response flow
- Integration tests β OTA mock servers (Booking.com sandbox), payment sandbox
- Browser tests (Pest+Dusk) β wizard pairing, booking engine flow
- Architecture tests (Pest arch) β enforce adapter naming, no facades in services
- Mutation tests (Infection) β high-value modules: pricing, accounting, tax
Coverage target: 80% line coverage modul accounting + tax + pricing (highest risk). 50% baseline lain.
Detail: 21-QA_CHECKLIST.md.
15. Technical Debt Policy #
- TODO β harus include issue link + due date
- "Temporary" workaround β tracked sebagai task di
22-PROGRESS.md - Deprecation: minimum 1 minor version warning sebelum removal
- No silent fallback yang masking bug β semua fallback log + telemetry