# Architecture

A **modular Laravel monolith**. Livewire components and controllers only orchestrate; business rules live in
services, record-level authorization in policies, tenant isolation in model scopes, and shared filtering in
query classes. Every rule is enforced on the server, regardless of what the UI shows.

```
Browser (Livewire SPA navigation, Alpine.js)
   │  HTTPS, CSRF token, session cookie
   ▼
Middleware: SecurityHeaders (CSP nonce) → web group (session, CSRF) → EnsureAccountIsActive
            (account/org status, idle & absolute timeouts) → EnsurePasswordIsChanged → EnsureTwoFactorEnrolled
            → auth / can:<permission> (re-applied by Livewire on every component request)
   ▼
Livewire component / Controller   — validates input format, calls authorize(), delegates
   ▼
Service layer (App\Services)      — business rules, transactions, snapshots, audit, notifications
   ▼
Policies (App\Policies)           — permission + tenant + seller-row checks for each record/action
Queries  (App\Queries)            — tenant-scoped, filtered builders shared by lists/exports/reports/dashboard
   ▼
Eloquent models                   — global OrganizationScope, ULID route keys, enum casts, no mass-assignment of system fields
   ▼
MySQL / MariaDB                   — FKs (RESTRICT on history), UNIQUE keys, CHECK constraints, audit triggers
```

## 1. Module map

| Module | Livewire / Controllers | Services | Policies |
|---|---|---|---|
| Authentication | `Auth\*Controller` (classic forms) | `Security\LoginService`, `LoginThrottle`, `TwoFactorAuthenticator`, `SessionManager` | — |
| Dashboard | `Dashboard`, `DashboardCharts` (lazy) | `Dashboard\DashboardService` | `dashboard.view` gate |
| Customers | `Customers\CustomerIndex/Form/Show`, `CustomerExportController` | `Customers\CustomerService` | `CustomerPolicy` |
| Vendors | `Vendors\VendorIndex/Form/Show` | `Catalog\VendorService` | `VendorPolicy` |
| Service Master | `Services\ServiceIndex` (modal form) | `Catalog\ServiceCatalogService` | `ServicePolicy` |
| Packages | `Packages\PackageIndex/Form/Show` | `Catalog\PackageService`, `Packages\ServiceDetailSchema` | `PackagePolicy` |
| Sellers | `Sellers\SellerIndex/Form/Show` | `Sellers\SellerService` | `SellerPolicy` |
| Work orders | `WorkOrders\WorkOrderIndex/Form/Show`, print/export/download controllers | `WorkOrders\WorkOrderService`, `WorkOrderStatusService`, `AttachmentService`, `DeadlineCalculator`, `StatusHistoryRecorder`, `WorkOrderNotifier` | `WorkOrderPolicy`, `WorkOrderAttachmentPolicy` |
| Reports | `Reports\ReportIndex/ReportViewer`, `ReportExportController` | `App\Reports\*` (11 definitions) | `report.view` / `report.export` gates |
| Administration | `Admin\OrganizationIndex/Form`, `UserIndex/Form`, `RoleIndex/Form`, `AuditLogIndex`, `SettingsPage` | `Organizations\OrganizationService`, `Users\UserService`, `PasswordService`, `RoleService`, `Settings\LogoService`, `App\Settings\SettingsService` | `OrganizationPolicy`, `UserPolicy`, `RolePolicy`, `AuditLogPolicy` |
| Notifications | `Notifications\Bell`, `NotificationIndex`, `NotificationController` | `WorkOrderNotifier`, command `work-orders:deadline-alerts` | own notifications only |

## 2. Tenancy model

```
Platform (Super Admin, no organization)
└── Main Reseller (organization, type main_reseller)        owns the catalog: vendors, packages
    ├── Sub-Reseller A (type sub_reseller, parent = main)    own sellers, customers, work orders, users
    └── Sub-Reseller B                                        vendor access: all or selected vendors
```

* **Accessible organizations** (`User::accessibleOrganizationIds()`): platform admin → all; main reseller user →
  own + its sub-resellers; sub-reseller user → own only.
* **Fail-safe global scope** (`Models\Scopes\OrganizationScope`) on customers, sellers, work orders and
  attachments: every Eloquent query of an authenticated user is restricted to accessible organizations, so a
  forgotten filter cannot leak data. Console jobs (no user) run unscoped by design.
* **Explicit scopes** `visibleTo($user)` add the seller-row restriction for seller logins and are used by lists,
  exports, reports and route binding.
* **Route model binding** resolves records through `visibleTo()` (trait `ScopesRouteBindingToUser`) → a foreign
  record is a **404**, its existence is never confirmed. Policies then authorize the action (**403**).
* **Record ownership is never taken from the browser.** The organization of a work order is always the
  customer's organization; new records get the organization from the user's tenant context
  (`Organizations\TenantResolver`); IDs sent by the browser are only lookups inside the user's scope.
* **Catalog** (vendors/packages) belongs to the main reseller (`organization_id` = root). Sub-resellers see the
  parent's catalog, optionally restricted to selected vendors (`organization_vendor`).
* Public identifiers are random **ULIDs** (routes, form values); business numbers (`CUS-000001`,
  `WO-2026-000001`) are display identifiers; numeric primary keys stay internal.

## 3. Key domain rules (where they live)

| Rule | Enforced in |
|---|---|
| Play type = number of services (1 Single, 2 Double, 3 Triple, 4 Quad, 5+ Multi) | `Enums\PlayType::fromServiceCount`, `PackageService`, `WorkOrderService` + DB CHECK on `packages` & `work_orders` |
| Package belongs to the selected vendor; services come from the package | `WorkOrderService::resolvePackage` / `snapshotServices` |
| Work order snapshots (vendor/package names, description, services+details, play type, prices, service address) | `WorkOrderService::packageSnapshot/addressSnapshot` |
| Status transitions, required data, permissions, recovery | `Enums\WorkOrderStatus`, `WorkOrderStatusService`, `WorkOrderPolicy::transition` + DB CHECKs |
| Deadline states (UTC storage, business-timezone "today", DST-safe) | `Services\WorkOrders\DeadlineCalculator`, `Support\BusinessClock` |
| Unique business numbers under concurrency | `Numbering\SequenceGenerator` (atomic `INSERT … ON DUPLICATE KEY UPDATE LAST_INSERT_ID()`) + UNIQUE keys |
| Referenced records are deactivated, not deleted | `VendorService`, `PackageService`, `SellerService`, `CustomerService` (+ RESTRICT foreign keys) |
| Audit trail | `Audit\AuditLogger` (called by every service) + append-only model + DB triggers |

## 4. Front end

* **Livewire 3 full-page components** with `wire:navigate` → SPA-like navigation that keeps the application
  shell (`@persist('sidebar')`), shows a progress bar, updates titles and supports back/forward. Protected routes
  are still authorized on the server for every request.
* Livewire + Alpine are **bundled by Vite** (`resources/js/app.js`) instead of auto-injected, so all scripts
  carry the CSP nonce. Modules: `layout` (sidebar/drawer/active link), `toasts`, `charts` (Chart.js imported
  lazily), `errors` (friendly 401/403/404/419/429/500 handling), `forms` (focus first invalid field).
* Reusable Blade components in `resources/views/components/ui` (button, input, select, field, card, modal,
  badge, status/deadline/play-type badges, stat card, page header, empty state, sortable header…). Icons are
  inline Heroicons (`App\Support\Icons`).
* Lists share `Livewire\Concerns\WithTable`: server-side pagination, **whitelisted** sorting, debounced search,
  URL-persisted filters.

## 5. Cross-cutting

* **Settings** (`App\Settings`): typed registry with defaults & validation, cached, audited; drives timezone,
  date format, number prefixes, deadline warning days, pagination, password policy, login throttling, session
  timeouts, admin 2FA requirement.
* **Notifications**: database channel only (no e-mail/SMS by design), delivered synchronously.
* **Logging**: business history in `audit_logs`; technical security events in `storage/logs/security-*.log`
  (no passwords, tokens or session IDs; e-mail addresses are hashed).
* **Caching/queues/sessions**: database drivers only — no Redis anywhere.

## 6. Design decisions

| Decision | Rationale |
|---|---|
| **Laravel 12** instead of 11 | All Laravel 11 releases carry unpatched advisories (incl. high-severity CVE-2026-48019); 12 has the same structure, supports PHP 8.2 and is maintained. |
| Custom lean RBAC (no package) | One role per user, privilege levels and role scopes are needed for "cannot manage higher-privilege users"; ~200 lines, fully tested, no dependency. |
| Permission enum + gates + policies | No magic strings; permissions, never role names, are checked. |
| Global scope **and** explicit scopes | Defense in depth: isolation even when a developer forgets a filter, plus explicit row-level rules for seller logins. |
| JSON `details` per package service, validated by `ServiceDetailSchema` | Service-specific fields without dozens of nullable columns; only known keys are stored. |
| Price history table + snapshots on work orders | Pricing is versioned; existing orders never change when prices change. |
| Reports as classes with one query each | Screen, CSV and print always show the same numbers; totals reuse the same scopes as lists and dashboard. |
| Classic Blade forms for authentication | Smallest attack surface (no JavaScript on guest pages), standard Laravel auth primitives. |
| TOTP implemented in-house, QR via `bacon/bacon-qr-code` | RFC 6238 is small and fully unit-tested against reference vectors; QR generated locally (secret never leaves the server). |

## 7. Future-ready (not implemented, by design)

Mobile / Quad Play (activate the Mobile service), vendor API integration, address availability lookup,
electronic order submission, e-mail/SMS notifications, commissions, customer portal, e-signature and the
Bangladesh billing variant can be added as new modules/services without changing the existing core.
