# Implementation Plan
# Central License & Entitlement Management Platform

**Repository:** `apnar-business-licence-platform`
**Base Doc:** `only-for-dev/Product Requirements Document.md`
**Status:** Approved — execution order A → B → C
**Target:** Production-grade license server that scales to 1000+ products / many licenses.

---

## 0. Cross-cutting groundwork (before Phase A)

1. **Model factories** for `Product`, `Customer`, `License`, `LicenseDomain`, `LicenseEvent` — required for locking/hashing/webhook tests. None exist today.
2. **`LicenseKeyService`** — extract `randomKey()` + add key hashing (`hash()`). Single source of truth.
3. **`KeypairService`** — RSA-2048 generation is currently duplicated 3× (command, `Admin\ProductController`, `ProductResource`). Unify into one service.
4. **Config** — move tunables into `config/licenseserver.php`: default grace days, retention days, rate-limit thresholds, throttle windows.
5. Keep all existing tests green throughout (`composer test` — currently 24). Extend builds protected by `vendor/bin/pint`.

---

## Phase A — Production Safety (P0)

### A1. Atomic activation (concurrency)
- **File:** `app/Services/License/LicenseService.php` (`registerDomain`, `activate`).
- Open a DB transaction and `lockForUpdate()` the license row at the start of `activate()` so concurrent activation requests serialize.
- Re-check `activeDomains()->count() >= domainLimit()` inside the transaction after acquiring the lock.
- **Test:** two parallel activate calls, `max_domains=1` → exactly one `license_domains` row.

### A2. License key hashing
- **Migration:** `licenses.key_hash` (string, 64) + `licenses.key_last4` (string, 4) + unique index on `key_hash`.
- **Command:** `license:backfill-key-hash` — one-time backfill for existing keys.
- **`LicenseKeyService::hash()`** = `hash('sha256', $key)`.
- Replace all `->where('key', $key)` lookups (client + admin + payments) with `->where('key_hash', ...)`.
- Create flow stores `key`, `key_hash`, `key_last4`. Raw key returned once to the main platform (in the create response + notification), then treated as source-of-truth artifact.
- **Log hygiene (Rule 9):** never log raw keys. Add log context filtering; events/webhook payloads carry `key_hash` + `key_last4` only.
- **Tests:** create → lookup by hash; duplicate blocked by unique `key_hash`; blocked/revoked still correct; logs contain no raw key.

### A3. Public API rate limiting + brute-force defense
- Named `RateLimiter`s in `app/Providers/AppServiceProvider`:
  - `public-license`: activate 30 req/min per `ip + key_prefix`; validate/heartbeat 120 req/min per ip.
  - `key-failures`: sliding window per `key_hash` (e.g., 10 fails/15 min) → `429 too_many_attempts` + temp block.
- Apply to public group in `routes/api.php`.
- Counters in Redis when available; degrade to DB aggregation otherwise.
- **Tests:** 429 after burst; per-key blocking independent per IP; block lifts after window.

### A4. Idempotency
- **Migration:** `idempotency_keys` — `key` (unique), `method`, `path`, `request` (json), `response` (json), `created_at`. (§33 exact constraint.)
- **Middleware** `EnsureIdempotency`: `Idempotency-Key` header → replay stored response; otherwise run and store (unique insert / lock to avoid double execution).
- Wire into `POST /api/v1/admin/licenses` and `POST /api/v1/admin/payments`.
- **Tests:** same key twice → same license id, one `created` event; concurrent duplicate store → one row.

### A5. Inbound signed webhook receiver
- **Migration:** `webhook_events` — `external_event_id` (unique), `type`, `payload` (json), `status` (`pending/processing/processed/failed/ignored`), `attempts`, `last_error`, `processed_at`.
- **Endpoint:** `POST /api/v1/webhooks/platform`:
  - Verify `X-Webhook-Signature = hash_hmac('sha256', rawBody, secret)` + timestamp window (±5 min) + unique `external_event_id` (Rule 7).
  - Dispatch per-type handlers: `license.created`, `payment.succeeded` → `recordPayment`, `subscription.cancelled` → policy, `refunded` → policy.
- **Command:** `license:inbound-webhooks:process {--limit=50}` — retry failed with exponential backoff.
- **Admin:** extend `webhooks()` dashboard endpoint + Filament replay action.
- **Tests:** bad signature → 401 no side-effect; duplicate event id → idempotent; malformed → `failed` row.

### A6. RBAC
- Add role/permission layer (`spatie/laravel-permission` or Filament Shield). Roles: `super_admin`, `support`, `readonly`.
- Gate Filament panel + policy on destructive ops: `revoke` = super_admin only (§7 high severity); block/unblock/extend moderate.
- Authorize controller actions too (REST + UI parity).
- **Tests:** role enforcement (support cannot revoke via API).

### A7. Domain normalizer
- **`App\Support\DomainNormalizer`:** strip scheme/path/query, strip `www.`, strip default ports, trim trailing dot, lowercase, IDN→punycode.
- Use in `activate`, `validate`, `registerDomain`, and admin add-domain. Store canonical form only.
- **Tests:** §9 examples (`https://www.example.com/`, `example.com`, `WWW.EXAMPLE.COM`) → `example.com`.

---

## Phase B — PRD MVP completeness (P1)

### B1. Entitlements (flagship)
- **Migrations (§33):**
  - `entitlements` — product FK, `code` (unique per product `(product_id, code)`), `type` (boolean/number/string), description.
  - `plan_entitlements` — plan FK, entitlement FK, `value`, unique `(plan_id, entitlement_id)`.
  - `license_entitlements` — license FK, entitlement FK, `value`, unique `(license_id, entitlement_id)` → **per-license snapshot** (§11).
- **`EntitlementService`:** snapshot plan → license on create / plan change; per-license overrides allowed.
- **`License` helpers:** `entitlement(code)`, `allEntitlements()` (§37 SaaS).
- **Signed doc:** add `entitlements` map to payload (§46 sample).
- **Admin:** `EntitlementResource` (product-scoped), plan relation-manager, license snapshot relation-manager.
- **Endpoints:** admin entitlement CRUD; `GET /api/v1/license/entitlements` (signed).
- **Tests:** snapshot on create; later plan change does not mutate old license; override honored; signature carries entitlements.

### B2. Plans entity
- **Migration:** `plans` — product FK, `code`, `name`, `interval` (`monthly/quarterly/annual/lifetime/trial`), `max_activations`, `max_domains`, `is_active`, unique `(product_id, code)`.
- Add nullable `licenses.plan_id` FK; keep `licenses.plan` string for display/back-compat.
- **Admin:** `PlanResource`; **Endpoints:** plans CRUD.
- **Tests:** plan CRUD; license created against plan gets entitlement snapshot.

### B3. Product versions
- **Migration:** `product_versions` — product FK, `version`, `status` (`active/deprecated/blocked`), `released_at`, unique `(product_id, version)`.
- **Admin + endpoints:** CRUD. Deprecated/blocked surfaced in signed doc for client warnings.
- **Optional enforcement:** per-product `enforce_min_version` → validation rejects older versions with `version_unsupported`.

### B4. License state machine completion
- **Migrations:** widen `licenses.status` to `pending | active | grace | expired | suspended | revoked | cancelled`; add `grace_ends_at`, `suspended_at`, `revoked_at`, `cancelled_at`; denormalized indexed `effective_status` + `effective_expires_at` (solves S2).
- **`LicenseStatus` enum + `LicenseStateMachine` service** enforcing §43 rules: revoked is terminal (Rule 3), expired cannot create production activations (Rule 2), grace policy configurable (Rule 1).
- **Admin/API:** add `suspend`, `cancel` transitions + reinstate; legacy `blocked` → suspended semantics.
- **Tests:** each transition pre/post state; revoked cannot reactivate; cancelled returns blocked wire status.

### B5. Validation logging + retention
- **Migration:** `validation_logs` — license FK nullable, `key_hash` (indexed), `status`, `environment`, `ip`, `domain`, `app_version`, `instance_id`, `response_code`, `created_at` (indexed for pruning). (§34 90–180 days.)
- Change `validate()` to write `validation_logs` (sampleable), NOT `license_events` rows for every call. `license_events` reserved for meaningful lifecycle events.
- **Command:** `license:prune-logs {--days=180}` scheduled daily.
- **Admin/API:** validation log reader + "recent failures" (from §26).

### B6. Global audit log
- **Migration:** `audit_logs` — `actor_type/id`, `action`, `subject_type/id`, `reason`, `ip`, `user_agent`, `metadata` (json), `created_at`. No update/delete endpoints (not silently editable, §27).
- **`AuditService`** recording every admin state change (Filament actions, admin API, CLI commands).
- **Tests:** each transition writes an audit row.

### B7. Client deactivate / heartbeat / environments
- **Endpoints:**
  - `POST /api/v1/license/deactivate` — set domain inactive, free activation slot (§18); idempotent for unregistered domains.
  - `POST /api/v1/license/heartbeat` — update `last_seen_at`/`app_version`/`instance_id`; short response.
  - `POST /api/v1/license/entitlements` (from B1).
- **Environment support (§10):** add `environment` column on `license_domains`; per-plan caps (production=x, staging=x, development=unlimited) applied in the limit math.
- **`check_interval_hours` enforcement:** sweeper flags stale registrations when `last_seen_at > now - interval`; soft flag only, no auto-revoke.

### B8. api_clients / api_keys
- **Migrations:** `api_clients`, `api_keys` — `key_hash` (unique), `client_id` FK, `scopes`, `expires_at`, `revoked_at`, `ip_restriction` (§33 `api_keys.key_hash` unique).
- **Admin + endpoints:** manage clients/keys, rotation, IP allow-list for internal platform.
- **Auth middleware** for internal service-to-service auth (SDK foundation).

---

## Phase C — Scale & polish (P2)

### C1. Validation cache + sweepers
- Redis cache of signed docs keyed `license:{key_hash}:v{valid_until}` (short TTL). State change bumps `license:{id}:rev` — revocation busts cache instantly (§31).
- **Command:** `license:sweep-status` (every 15 min) flips indexed status between active/grace/expired using `valid_until`/`grace_ends_at` — no more O(n) recompute per request/dashboard.
- **Dashboard:** aggregate counters (MariaDB/Redis) instead of live rescan per load.

### C2. Observability (§28)
- JSON structured logs; request-id middleware; response-time + status metrics; failed-webhook alerts (email + dashboard pending count).

### C3. Offline tokens + SDK foundation (§20/§39)
- Signed payload gains `issued_at` + `offline_valid_until` window; PHP/Laravel SDK wrapping activate/validate/heartbeat/deactivate + local cached validation honoring the window.

### C4. Abuse detection (§26 Security, §51)
- `suspicious_domains` watchlist (temp-mail domains, punycode lookalikes); repeated-failure counters from `validation_logs`; IP banning via throttle integration.

### C5. Reporting (§49) + retention/backup ops
- Filament report pages (product/plan-wise counts, activation trends, revocations, trial conversions) on aggregate tables.
- Retention sweeper tuning; documented daily backup + restore verification (§35); DR RPO≤15m / RTO≤1h (§36).

### C6. Multi-tenant readiness (§48)
- Nullable `organization_id`/`tenant_id` scopes on `licenses` now, so future org licensing needs no redesign.

---

## Schema inventory (final, Delta vs today)

| Table | Status today | Addition |
|---|---|---|
| products | exists | + `apply` fields (skip) / keep |
| customers | exists | — |
| licenses | exists | + `key_hash`, `key_last4`, `plan_id`, new statuses, `grace_ends_at`, `suspended_at`, `revoked_at`, `cancelled_at`, `effective_status`, `effective_expires_at`, `organization_id` |
| license_domains | exists | + `environment` |
| license_events | exists | unchanged |
| webhook_outbox | exists | unchanged |
| **plans** | missing | new |
| **entitlements** | missing | new |
| **plan_entitlements** | missing | new |
| **license_entitlements** | missing | new |
| **product_versions** | missing | new |
| **webhook_events** | missing | new (inbound) |
| **idempotency_keys** | missing | new |
| **validation_logs** | missing | new |
| **audit_logs** | missing | new |
| **api_clients / api_keys** | missing | new |

## API inventory (final)

**Public (client):** activate, validate, ping, deactivate, heartbeat, entitlements — all rate-limited.
**Platform inbound:** `POST /api/v1/webhooks/platform` (HMAC signed) + versioned.
**Admin (REST, Sanctum + RBAC):** dashboard, events, webhooks (in/out), products (+ regenerate-key), plans, entitlements, versions, customers, licenses CRUD + block/unblock/suspend/cancel/revoke/extend/domains, payments, audit, validation-logs, api-clients/api-keys.

## Test plan
- New suites: `ConcurrencyTest` (A1/A4), `RateLimitTest` (A3), `WebhookInboundTest` (A5), `EntitlementSnapshotTest` (B1), `StateMachineTest` (B4), `ValidationLogRetentionTest` (B5), `AuditLogTest` (B6), `KeyHashTest` (A2), `DomainNormalizerTest` (A7).
- Gate: existing 24 tests stay green; `vendor/bin/pint --test`.
- Run `composer test` after each phase.

## Deployment for 1000-product scale
- MySQL/MariaDB; `CACHE_STORE=redis`, `QUEUE_CONNECTION=redis`; 2+ queue workers; scheduler with `withoutOverlapping`.
- Backup daily + restart-tested; DR RPO≤15m / RTO≤1h.