# BACKEND_SAAS_PLAN.md — Super-Admin SaaS API

Implementation contract for the platform/super-admin surface (packages, subscriptions, invoices,
coupons, revenue/insights, audit, settings, email templates). Pairs with `BACKEND_README.md` (the
frontend-side contract). Where the two disagree, **this file + the live code win** (see §0).

Scope decisions locked with the product owner:

- **No payment processing.** Pure DB/structure surface. Gateway keys are only *stored* (encrypted) in
  settings — nothing is ever charged. Invoices are marked paid manually; subscriptions managed manually.
- **Trends come from a monthly snapshot table** (`platform_snapshots`), not live approximation.
- **Support chat is out of v1.** No `support_*` tables/controllers/routes.
- Out of scope (per README): Admins & Roles page, RBAC permissions page.

---

## 0. Reconciliation: README vs. actual code

| Topic | README says | Actual code | Decision |
|---|---|---|---|
| Envelope | `{ status: true, ... }` | `ApiResponse` → `{ status:<int>, message, data, errors:[] }` | Keep actual. Frontend only unwraps `data`. Always use `ApiResponse::*`. |
| Auth header | `X-Authorization` | `FixAuthorizationHeader` normalizes it → `Authorization` | Nothing to do; `auth:sanctum` works. |
| Role gate | `role:super_admin,admin` | Spatie `role:` alias + `RolesEnum` values, `\|`-joined | `role:role.super_admin\|role.admin`. |
| Routes | flat `/v1/...` | `apiPrefix=api/v1`, one file per module in `routes/v1/` | One `routes/v1/{module}.php`, wired into `routes/api.php`. |
| List shape | `{ summary, items }` | `ApiResponse` wraps paginators as `{ meta, items }` | Ledgers return `data = ['summary'=>…, 'items'=>…]` (un-paginated). |

Clients are **Users** with role `role.client` — section K extends the User/ClientResource layer.

---

## 1. Package ↔ RolesEnum bridge

`PackageTier` (frontend values) maps 1:1 onto existing package roles in `RolesEnum`:

| `PackageTier` | `RolesEnum` | value |
|---|---|---|
| `TRACK_ANALYZE` | `RolesEnum::TRACk_ANALYZE` | `package.track_analyze` |
| `WEALTH_MANAGER` | `RolesEnum::WEALTH_MANAGER` | `package.wealth_manager` |
| `WHITE_LABEL` | `RolesEnum::PACKAGE_AGENCY` | `package.agency` |

`PackageTier::role(): RolesEnum` and `PackageTier::fromRole()` implement the bridge. `SubscriptionService`
is the **only** place client roles change: activating/changing a plan strips any existing `package.*`
role and assigns the new one (keeping `role.client`); cancelling removes it. Roles are on the `sanctum`
guard (seeded by `RolesSeeder`).

---

## 2. Cross-cutting foundations

- `app/Enums/Saas/` — `PackageTier`, `SubscriptionStatus`, `BillingInterval`, `InvoiceStatus`,
  `CouponType`, `CouponDuration`, `AuditAction` (+ category map).
- `App\Services\Saas\RevenueMetricsService` — single MRR/ARR/ARPU/churn/NRR/GRR home (normalize
  `yearly → amount/12`). Consumed by snapshots, insights, revenue.
- `App\Services\Saas\AuditLogger::record(AuditAction, ?target, detail)` — writes one immutable
  `audit_entries` row (actor + IP from request).
- Every platform route group: `['auth:sanctum', 'role:role.super_admin|role.admin']`. **Not** tenant-scoped.
- Ledger services return `['summary'=>[...], 'items'=>Resource::collection(...)]`.

---

## 3. Features (build order)

Each: migration → model (+Filter) → service → FormRequest(s) → `Admin\` controller → Resource →
`routes/v1/{x}.php` → seeder → Export.

1. **Packages** `/v1/packages` — CRUD; `subscribers_count` (active subs); archive-vs-delete guard;
   `summary.mrr = Σ(price_monthly × subscribers_count)`.
   - `GET /v1/packages/tires` — returns the available package tiers (the `PackageTier` enum cases)
     for frontend tier dropdowns/selects. Response: `data.tiers` = `["TRACK_ANALYZE", "WEALTH_MANAGER",
     "WHITE_LABEL"]`. Read-only; gated like the rest of the packages routes. Declared **before**
     `/{id}` so the literal segment wins over the wildcard.
2. **Subscriptions** `/v1/subscriptions` — list+manage; `amount`/`mrr` derived from package+interval;
   role-sync; `plan_change` audit. **K:** `User::subscription()`, `ClientResource` embed, accept
   `package_id`/`billing_interval` on client-create.
3. **Invoices** `/v1/invoices` — list; `INV-YYYY-NNNN` generator; PATCH transition state-machine
   (`unpaid|overdue→paid`, `paid→refunded`); overdue computed on read; `invoice_paid`/`refund` audit.
4. **Coupons** `/v1/coupons` — CRUD + toggle; derived `status`; `times_redeemed` read-only.
5. **Snapshots** `platform_snapshots` + `CapturePlatformSnapshotJob` (monthly) + `PlatformSnapshotSeeder`
   (backfill 12 months).
6. **Insights/Revenue** `/v1/admin/insights`, `/v1/admin/revenue` — read aggregations; series from
   snapshots, headline numbers live; cache 5–15 min.
7. **Audit** `/v1/admin/audit` — read-only over `audit_entries`; `?category=`/`?search=`.
8. **Settings + Email templates** `/v1/admin/settings`, `/v1/admin/email-templates` — singleton settings
   (4 JSON groups, encrypted+masked secrets); editable transactional templates.

---

## 4. Exports

`?export=csv|xlsx` on the list endpoints (Packages, Subscriptions, Invoices, Coupons, Audit) returns a
Blob via `Maatwebsite\Excel` (mirrors `ClientExport`). `app/Exports/Saas/*Export.php`.

---

## 5. Tables

`packages`, `subscriptions`, `invoices`, `coupons`, `audit_entries`, `platform_settings` (singleton),
`email_templates`, `platform_snapshots`. Additive: `User::subscription()` (FK on `subscriptions.client_id`).

---

## 6. Permissions & admin delegation

The super admin grants granular access to created admins. Built on `spatie/laravel-permission`
(permissions assigned **directly** to each admin user — the frontend matrix maps 1:1 to checkboxes).

- **Catalogue** — `App\Enums\PermissionsEnum` (14 keys + `group()`/`label()`), seeded on the `sanctum`
  guard by `PermissionsSeeder`. The `super_admin` role is seeded with **all** of them, so it always
  passes every `permission:` gate.
- **Assignment** — `POST/PUT /api/v1/admins` accept `permissions[]` (or a `preset` =
  `finance|support|read_only`); `AdminService` runs `syncPermissions`. `AdminResource` returns
  `roles` + `permissions` so the matrix pre-checks boxes.
- **Catalogue endpoint** — `GET /api/v1/admin/permissions` → `{ groups[], presets{} }` (gated
  `permission:admins.manage`).
- **Gating** — each route group carries `permission:<key>` (or `role_or_permission:` for client
  routes that agencies also use). `clients.impersonate` is enforced in `ResolveTenantMiddleware`.

| Permission | Guards |
|---|---|
| `dashboard.view` | `GET /admin/insights` |
| `revenue.view` | `GET /admin/revenue` |
| `packages.manage` | all `/packages*` (view+edit) |
| `subscriptions.manage` | all `/subscriptions*` |
| `invoices.manage` | all `/invoices*` |
| `coupons.manage` | all `/coupons*` |
| `clients.view` / `clients.manage` | `/clients` read / write (agency & super-admin bypass by role) |
| `clients.impersonate` | `X-Tenant-Id` "view as client" (ResolveTenantMiddleware) |
| `push.send` | `/push-notifications*` |
| `support.handle` | reserved — Support is out of v1 |
| `admins.manage` | all `/admins*` + `/admin/permissions` |
| `settings.manage` | `/admin/settings*`, `/admin/email-templates*` |
| `activity.view` | `/activity-logs*`, `GET /admin/audit` |

Preset bundles: **Finance** (dashboard/revenue/packages/subscriptions/invoices/coupons/clients.view),
**Support** (dashboard/clients.view/impersonate/push/support), **Read-only**
(dashboard/revenue/clients.view/activity). Ledger reads and writes share one `.manage` per resource.

---

## 7. Out of scope for v1

Live payments/charging/webhooks/dunning, auto invoice generator, PDF, websocket presence, Support chat,
and the dedicated **Roles & Permissions management page** (granular permission *enforcement* ships here;
only the role-CRUD admin UI is deferred).
