# SaaS Architecture

## 1. Tenancy Model

**Shared database, shared schema, row-level isolation** (`agency_id` on every tenant table). Rationale: fastest to build correctly, cheapest to operate, easiest to run cross-tenant admin reporting for Super Admin — the main risk (data leakage) is mitigated structurally via the Repository-layer `TenantScope` (see `01-system-architecture.md` §4), not by developer discipline alone.

Candidates are **not** tenant-scoped — a candidate profile is platform-global and can apply to any agency's jobs; visibility/unlock rules control what any given agency can see of it.

## 2. Subscription Management

- Plans: **Free, Basic, Professional, Enterprise** (see schema `subscription_plans`), each carrying limits for: candidate views, contact reveals, resume downloads, recruiter seats, job postings.
- `agency_subscriptions` tracks the agency's current plan + billing cycle + status (`trialing/active/past_due/cancelled/expired`).
- `subscription_usage` resets every billing period and is incremented atomically (DB transaction + row lock) every time a limited action occurs — checked **before** the action executes (fail closed: if usage would exceed limit, action is blocked with an upgrade prompt).
- Plan changes: upgrades apply immediately (prorate via Stripe/PayPal proration APIs where supported, flat immediate-switch for JazzCash/Easypaisa/Bank Transfer); downgrades apply at period end.
- A daily cron (`SubscriptionExpiryCheck`) flips `active → expired` for lapsed subscriptions, demotes the agency to Free-tier limits, and fires a `subscription_expiry` notification 7/3/1 days before expiry.

## 3. Credit System

- Actions that cost credits: contact reveal, resume download, candidate unlock, messaging — costs are **admin-configurable** per action in `credit_costs` (Super Admin settings), not hardcoded.
- `credit_balances` holds current balance per agency; every change is journaled in `credit_transactions` (purchase/usage/refund/expiry/bonus) with a running `balance_after` for auditability — balance is **never** trusted from a cached column alone in critical paths; `CreditService::debit()` recomputes/locks the row (`SELECT ... FOR UPDATE`) inside a transaction to prevent race-condition overspend from concurrent requests.
- Credits can carry an `expires_at` (e.g. promotional bonus credits) — a daily cron expires them and logs a `expiry` transaction.
- Credit purchase is a wallet debit or a direct gateway payment (agency's choice at checkout).

## 4. Wallet System

- One wallet per agency (`wallets`, 1:1), holding a real-money balance.
- `wallet_transactions` is the ledger (deposit/withdrawal/credit_purchase/refund/adjustment) — same locking discipline as credits.
- Wallet top-ups go through the Payment System (any supported gateway); wallet balance can then be spent on credit purchases or subscription payments without re-entering payment details each time.

## 5. Billing & Invoicing

- Every chargeable event (subscription renewal, credit purchase, wallet top-up) generates an `invoices` row with line items (`invoice_items`), tax calculation (configurable `tax_percentage` in Settings → Payment), and a system-generated `invoice_number` (format `INV-{YEAR}-{ZEROPADDED}`).
- Invoice PDF generated via the PDF exporter library and stored under `storage/agencies/{id}/invoices/`; emailed automatically on generation.
- `payments` records the gateway attempt against an invoice; multiple payment attempts can map to one invoice (e.g. failed → retried → success).
- Refunds are a distinct approval workflow (`refund_requests`) — Agency Owner requests, Super Admin (or delegated Support) approves/rejects, and approved refunds trigger the gateway's refund API + a wallet/credit reversal transaction.

## 6. Usage Tracking

Two distinct, deliberately separate logs:

| Log | Purpose | Table |
|---|---|---|
| `subscription_usage` | Plan-limit enforcement (resets per cycle) | Numeric counters |
| `activity_logs` | Forensic/behavioral trail (never resets) | Append-only event stream |

Keeping these separate means plan-limit logic stays simple arithmetic while the activity log can grow and be archived/partitioned independently without touching billing logic.

## 7. Payment Gateway Abstraction

All gateways implement a common `PaymentGatewayInterface` (`charge()`, `refund()`, `verifyWebhook()`, `getStatus()`), so the Service layer never branches on gateway type:

```
Libraries/PaymentGateways/
├── PaymentGatewayInterface.php
├── StripeGateway.php
├── PayPalGateway.php
├── JazzCashGateway.php       (HMAC-signed redirect flow)
├── EasypaisaGateway.php      (HMAC-signed redirect flow)
└── BankTransferGateway.php   (manual verification, Support-staff-approved)
```

Webhooks land on `/api/v1/webhooks/{gateway}`, are signature-verified, then dispatch a `PaymentService::handleWebhook()` call that updates `payments.status` and triggers downstream effects (activate subscription, credit wallet, mark invoice paid) idempotently (webhook events deduped by `gateway_transaction_id`).

## 8. Reporting Engine

- **Admin reports** (platform-wide): revenue, subscriptions, agencies, recruiters, candidates, jobs, applications, credits, wallet, payments — built as pre-aggregated materialized summary tables refreshed nightly (cron `ReportGeneration`) plus on-demand real-time queries for smaller date ranges.
- **Agency reports** (tenant-scoped): recruiter performance, candidate views, contact reveals, hires, interviews, conversion rates — same engine, `TenantScope`-filtered.
- Export pipeline: `ReportExportService` → PDF (via PDF exporter lib) / Excel (PhpSpreadsheet) / CSV — all three share one internal tabular data structure so adding a new export format doesn't touch report-generation logic.

## 9. Notification Engine

- Channels: Email (SMTP, queued), SMS (gateway-agnostic adapter, e.g. Twilio-compatible), In-App (`notifications` table + unread badge), WhatsApp (optional, Business API adapter — feature-flagged off by default since it requires separate account approval).
- Every trigger (registration, job application, interview invite, payment success, subscription expiry, etc.) maps to a `notification_templates` row per channel, so copy can be edited by Super Admin without a code deploy.
- `NotificationService::send($user, $triggerKey, $data)` resolves the user's channel preferences, renders the template, and dispatches — queued via Redis when available, synchronous fallback otherwise.

## 10. Audit Logging (Platform Trust Layer)

Distinct from activity logs: `audit_logs` captures **before/after diffs** on sensitive writes (subscription changes, permission changes, payment status changes, agency verification decisions) for compliance/dispute resolution — written by an `AuditService` wrapper any Service method can call, never bypassable from a controller directly.
