# 07 — Milestones M1–M6

Estimated total: **~22–26 weeks** for a small cross-functional team (2–3 backend, 1 frontend, 1 QA, part-time DevOps). Provider app-review lead times run **in parallel** and are the main schedule risk. Start M2 app submissions during M1. Estimates are planning figures, not commitments.

| Milestone | Duration (est.) | Depends on | Demo at end |
|-----------|-----------------|------------|-------------|
| M1 Foundation | 3–4 wks | — | Sign up an agency → isolated DB provisioned → owner logs in with 2FA → invites a teammate → roles enforced → audit trail visible |
| M2 Clients & social accounts | 3–4 wks | M1 | Create clients, assign staff, connect accounts via OAuth (real sandbox where available, fakes locally), health/reconnect UI, capability matrix |
| M3 Composer & delivery | 5–6 wks | M2 | Media library → compose master post → per-target overrides → validation → single-stage approval → schedule → publish to 10 targets with partial failure → retry |
| M4 Collaboration | 3–4 wks | M3 | Multi-stage approvals, client portal, calendar drag-drop, campaigns, tasks, notifications |
| M5 Reporting & AI | 3–4 wks | M3 (M4 for portal reports) | Metrics sync with freshness, dashboards, branded PDF/XLSX scheduled reports, opt-in AI assistant with budgets |
| M6 Commercial & advanced | 4–5 wks | M1–M5 | Razorpay billing + GST invoices + limits, inbox (permitted surfaces), white-label, super-admin ops dashboards, hardening |

---

## M1 — Foundation ✅ implemented 29 Oct 2026

### Scope
Secure authentication, master schema, tenant registration/provisioning, domain resolution, dynamic tenant DB connections, tenant migrations, tenant-safe roles, middleware, validation, audit logging, seeders, tests, and dev docs. Tenant context is reset across request and worker lifecycles.

### Work breakdown

1. **Project bootstrap**
   - `composer create-project laravel/laravel:^13 .` and commit the lockfile.
   - Packages: `livewire/livewire:^4.4`, `laravel/fortify`, `laravel/horizon`, `laravel/pennant`, `laravel/pulse`, `laravel/sail` (dev), `pestphp/pest` + `pestphp/pest-plugin-laravel` (dev), `larastan/larastan` (dev), `laravel/pint` (dev), `league/flysystem-aws-s3-v3`, `sentry/sentry-laravel`, `pragmarx/google2fa` (via Fortify).
   - Tailwind CSS 4 + Alpine.js via Vite.
   - Sail services: `mysql:8.4`, `redis:7`, `minio`, `mailpit`, `clamav` (clamav is used from M3).
   - CI (GitHub Actions): Pint, Larastan level 8, Pest (with MySQL + Redis services), `composer audit`, `npm audit --omit=dev`.
2. **Connections & config**
   - `config/database.php`: `master` (default), `tenant` (template, no credentials), `provisioner` (used only by the provisioning worker).
   - `config/tenancy.php`: base domain, central domains, DB name pattern `social_tenant_%04d`, tenant DB host pool, secret store driver.
3. **Master migrations** (see doc 02, M1 row), with models `User`, `Tenant`, `TenantDomain`, `TenantDatabaseConnection`, `TenantUser`, `TenantProvisioningJob`, `Plan`, `PlanFeature`, `TenantFeatureOverride`, `Subscription`, `UsageSummary`, `SystemSetting`, `AdminAuditLog`, `SupportAccessGrant`, `BackupRun`.
4. **Tenancy layer** (`app/Tenancy`): `TenantManager`, `TenantConnectionConfigurator`, `WorkspaceSession` (originally `TenantResolver`, host-based), `SecretStore` (+ `EncryptedDbSecretStore` using a dedicated `TENANT_SECRETS_KEY`; `AwsSecretsManagerStore` stub with interface tests), middleware (`ResolveTenant`, `EnsureTenantActive`, `EnsureTenantMember`, `RequireTwoFactor`), `InitializeTenancy` job middleware, `Queue::before/after/failing` listeners, `TenantManager::run()`.
5. **Provisioning**: `RegisterAgency` action (master TX) → `ProvisionTenantJob` on the `provisioning` queue → idempotent steps `CreateDatabase`, `CreateDatabaseUser`, `StoreSecret`, `RunTenantMigrations`, `SeedTenant`, `VerifyTenant`, `ActivateTenant`. Admin retry action. `tenants:provision {tenant}` and `tenants:retry-provisioning {tenant}` commands.
6. **Tenant migrations & seeds**: `tenant_settings`, `team_roles`, `team_permissions`, `team_members`, `tenant_audit_logs`, `notifications`. `TenantDatabaseSeeder` seeds system roles from `config/rbac.php`.
7. **Commands**: `tenants:migrate {--tenant=} {--pretend} {--step}`, `tenants:list`, `tenants:verify`, `tenants:rotate-credentials {tenant}`, `tenants:suspend|reactivate {tenant}`.
8. **Auth (Fortify)**: registration (agency signup creates user + tenant), login, logout, email verification, password reset, TOTP 2FA + recovery codes, password confirmation. Rate limiters. Livewire/Blade views: login, register, 2FA challenge, profile security, agency switcher (a user in multiple agencies).
9. **RBAC**: `config/rbac.php` (roles → permissions), `Gate::before` for owner, `PermissionRegistrar` that reads system roles plus tenant custom roles, `TenantUserPolicy`, team invitation flow (signed, expiring, hashed token; accept creates or links a master user).
10. **Audit logging**: `AuditLogger` with a `Redactor` (keys: `password`, `token`, `secret`, `authorization`, `access_token`, `refresh_token`, `code`). Events: login success/fail, 2FA enable/disable, tenant created/provisioned/suspended, member invited/role changed/removed, settings changed, support grant used.
11. **UI (minimal but real)**: central landing + signup; provisioning status page (Livewire polling); agency dashboard shell with navigation placeholders for M2+; team management; agency settings (name, timezone, locale); super-admin: tenants list/detail, provisioning jobs with retry, suspend/reactivate, audit log viewer.
12. **Seeders (dev only)**: plans + features (spec §13), a super admin (credentials printed once by the `dev:seed` command, never committed), two demo agencies (`acme`, `globex`) each with provisioned tenant DBs and all roles.
13. **Docs**: `README.md` (setup, commands), `docs/dev/tenancy.md`, `docs/dev/testing.md`, `docs/runbooks/provisioning.md`.

### M1 acceptance criteria (all automated)

- [x] Registering an agency creates master rows with status `provisioning_pending`. The tenant is not reachable until provisioning completes.
- [x] Provisioning creates a dedicated DB and a dedicated MySQL user that **cannot** access the master DB or another tenant DB (verified by attempting a cross-schema query with tenant credentials → access denied).
- [x] Killing provisioning after each step leaves the tenant inactive. Retry completes successfully (one dataset test per step).
- [x] The current workspace comes from the session and is re-checked against membership on every request; a Globex member can never open Acme (choosing it is 403, a forged session choice is dropped). A forged tenant ID in the payload is ignored. (Originally subdomains; replaced by one address + workspace picker, see docs/dev/tenancy.md.)
- [x] A worker processing a job for tenant A, then a job for tenant B, never leaks connection, cache prefix, storage prefix, or auth context. After each job the `tenant` connection is purged (asserted).
- [x] An exception inside a tenant job still ends tenancy (`finally`).
- [x] 2FA is mandatory for owner/admin/super admin. Login and reset are rate-limited.
- [x] Role matrix tests pass for M1 permissions (team, agency settings, audit).
- [x] Critical actions write audit logs with redaction (tested: no token/password strings in logs).
- [x] `tenants:migrate` migrates all tenants, records `schema_version`, and supports `--pretend`.
- [x] The web DB user has no DDL privileges (documented and checked by `tenants:verify`).
- [x] Pint and Larastan **level 8** clean; 124 Pest tests green locally (MariaDB). The CI workflow targets MySQL 8.4 but has not run yet (no remote configured). Coverage measurement needs a coverage driver (pcov/xdebug), which is not installed locally.

### M1 commands (expected)

```bash
cp .env.example .env && ./vendor/bin/sail up -d
./vendor/bin/sail artisan key:generate
./vendor/bin/sail artisan migrate --database=master --path=database/migrations/master
./vendor/bin/sail artisan db:seed --class=Database\\Seeders\\Master\\PlanSeeder
./vendor/bin/sail artisan dev:seed            # demo agencies + provisioned tenant DBs (local only)
./vendor/bin/sail artisan horizon             # workers incl. provisioning queue
./vendor/bin/sail artisan tenants:migrate
./vendor/bin/sail test
```

---

## M2 — Clients & social accounts ✅ implemented 30 Oct 2026

**Deliverables**
- Clients CRUD (profile, contacts, brand, voice, competitors, notes, statuses), client user assignment, client-level roles, and client switcher. "Client visible to user" query scopes are applied everywhere.
- `platform_integrations` admin UI (super admin). Capabilities are seeded from versioned JSON and editable status per environment.
- OAuth framework: central callback, `oauth_states` (hashed state, PKCE verifier encrypted, 10 min TTL, single use), tenant + client restored from state, account discovery screen (pick pages/orgs/channels to link), consent record.
- Adapters (auth + discovery + health + refresh + revoke): Facebook, Instagram, LinkedIn, YouTube, X, TikTok, Pinterest, Threads, GBP. Fakes for local/testing. Real adapters are fully implemented against documented endpoints but gated by integration status.
- Token health: `tokens:refresh-due` (hourly; refreshes tokens expiring in under 72 h), `accounts:health-check` (daily), reconnect alerts (in-app + email), disconnect/revoke, and the Meta deauthorise/data-deletion webhooks.
- Effective capability UI per account (what this account can and cannot do, and why).

**Acceptance**
- [x] OAuth state replay and mismatch are rejected. PKCE is used where supported. Tokens are encrypted at rest (DB dump shows ciphertext).
- [x] Token refresh success, refresh failure → `needs_reconnect` + notification, revoked scope → capability downgraded. Covered by `Http::fake` tests for each provider.
- [x] A user assigned to client A cannot list, view, or connect accounts of client B (404).
- [x] The same external account connected under two clients in one tenant is rejected by the unique constraint with a helpful message.
- [x] Providers without credentials show `blocked` with a reason. No fake success outside local/testing (boot guard test).

---

**Delivered differently or deferred**
- Credentials are grouped per provider *app* (`meta` serves Facebook + Instagram, `google` serves YouTube + Business Profile).
- The reconnect alert is e-mail only. The in-app notification centre comes with M4 notifications.
- Cross-agency duplicate detection (the same Page connected by two agencies) is not flagged yet. It needs a central index decision (docs/plan/11).
- Adapters are contract-tested against provider-shaped responses, not live sandboxes. Each needs a live check once its developer app exists.

---

## M3 — Composer, media & delivery ✅ implemented 31 Oct 2026

**Deliverables**
- Media library: S3/MinIO, tenant prefix, signed direct uploads (multipart for large video), MIME sniffing, ClamAV scan job, metadata extraction (image dims, ffprobe for video), thumbnails/variants (FFmpeg on the `media` queue), folders, tags, search, dedupe by checksum, storage quota via `UsageMeter`.
- Composer (Livewire): master post, rich text → per-platform plain text conversion, link, media, grouped account picker (capability-filtered), per-target overrides, live per-platform preview, validation engine driven by `provider_capabilities.limits` (text length, media count/type/size, dimensions, aspect ratio, video duration), drafts, duplicate, version history (immutable versions + diff view).
- Single-stage approval gate (full workflows come in M4): submit → approve/request changes. Material-change invalidation via `content_hash`.
- Scheduling: now / scheduled (tz-aware), outbox, relay, central dispatch index, dispatcher, sweeper, `PublishTargetJob`, reconciliation job, retry/backoff, manual retry, cancel.
- Publishing adapters (publish + validate + reconcile) for providers whose status allows it. Fakes support scripted outcomes (success, transient, rate limit, auth, validation, timeout-after-send) for tests.
- Post detail page: per-target status, attempts timeline, provider links, errors with remediation hints.
- `tenant_usage_events` + hourly roll-up into master `usage_summaries`.

**Acceptance**
- [x] 10-target bulk publish with 2 scripted failures → exactly 8 published, 2 failed/retryable, `partially_failed`, no duplicates after replaying all jobs twice.
- [x] Timeout-after-send → `unknown_outcome` → reconciliation, never a blind duplicate.
- [x] Editing an approved caption, media, or targets → approval superseded and the schedule cancelled. Unapproved posts cannot be scheduled.
- [x] DST tests: scheduling at 02:30 on a spring-forward day in `America/New_York` is rejected. Fall-back ambiguity is handled. `Asia/Kolkata` baseline.
- [x] Killing Horizon mid-run and restarting → no lost or duplicated publications.
- [x] Infected upload (EICAR test file) is quarantined and cannot be attached.
- [x] Media URLs are signed, expire, and are tenant-checked (cross-tenant signed URL attempt fails).

**Delivered differently or deferred**
- Rich text is entered as plain text (per-platform conversion is a no-op for now). Templates and categories have tables but no UI yet (M4).
- Large video uses Livewire uploads (set `LIVEWIRE_TEMPORARY_FILE_UPLOAD_DISK=s3` for direct-to-S3 pre-signed uploads in production). Separate transcoded renditions beyond thumbnails are not generated yet.
- Some actions that provider APIs support aren't implemented by our adapters yet (LinkedIn video, X video, Pinterest video, TikTok photo, Facebook Reels). They show as *configurable*, never as available (docs/dev/publishing.md).
- The "Horizon killed mid-run" criterion is covered by sweeper tests simulating lost jobs and in-flight attempts. It has not yet been tested against a real Horizon/Redis process.

---

## M4 — Collaboration ✅ implemented 1 Nov 2026

**Deliverables**
- Configurable per-client approval workflows: optional internal stage, required client stage, multi-stage, min approvers, deadlines (reminder + escalation jobs), delegation, final approval audit with timestamp/IP/content hash.
- Review comments (threaded, per target, resolvable), revisions view, notifications (database + mail + optional Slack webhook) with per-user preferences.
- Client portal (same app, `client` role, restricted layout): dashboard, approval inbox, draft review with per-platform preview, approve/reject with comment, calendar (read), assets (shared), reports (from M5), feedback.
- Unified and per-client calendar (day/week/month), drag-and-drop reschedule with revalidation, queue slots, blackout windows, recurring series (with policy warnings), pause/resume/cancel.
- Campaigns (grouping, date ranges, objective), team tasks with checklists and assignments, activity stream.

**Acceptance**
- [x] A client approver sees only their own client's posts and targets. Internal-only comments are hidden from the portal.
- [x] A user without `approvals.decide_*` cannot approve via a crafted Livewire call.
- [x] A deadline breach triggers escalation. Delegation is recorded in history.
- [x] Calendar drag across DST boundaries recomputes UTC correctly.

**Delivered differently or deferred**
- Client stages create one step per client on the post. Client sign-off must come from client-side approvers (see docs/dev/collaboration.md). This clarifies how the Approver role is used: approvers act client-side when assigned to a client with the "Client User" client role.
- Queue slots are per client, not per account, so a multi-account post can share one slot.
- Recurring content is "series of draft copies", each reviewed separately, rather than auto-repeating publications (provider duplicate-content rules).
- Reports in the portal arrived with M5. Rich @mentions in comments and notification digests are not built yet.

---

## M5 — Reporting & AI ✅ implemented 29 Oct 2026

**Deliverables**
- Metrics sync jobs per provider (`sync` queue, staggered), `metric_sync_runs` diagnostics, freshness markers ("updated 3 h ago from Instagram Graph API v2x"), explicit "not available from this platform" states, backfill and deletion rules.
- Dashboards: agency overview, per-client, per-account, post performance, campaign comparison, custom date ranges in the client timezone. Metric definitions tooltip with provenance.
- Reports: templates, agency branding, PDF (Browsershot or DomPDF, decided in M5 spike) and XLSX (`openspout`), scheduled delivery with logs, signed download links.
- AI assistant (opt-in per tenant, `ai.use` permission): caption drafts, platform variants, hashtags, translation, brand-voice rewrite, content-plan ideas. Prompt templates are versioned. Only the selected client's brand voice is sent (no secrets, no other clients' data). Consent recorded. Token usage → credits → plan budget enforcement. Output always lands in the composer for human review. **Never auto-published.**

**Acceptance**
- [x] Unavailable metrics display "Not available" and never 0 or a fabricated value.
- [x] Every metric shows its source and `fetched_at`. Failed syncs are visible with the error.
- [x] An AI request beyond budget is blocked with a clear message. AI output cannot reach a target without a human save + approval.
- [x] Prompt payload test: no tokens, emails of other clients, or secrets are included.

**Delivered differently or deferred**
- PDF engine: DomPDF (no headless Chromium on workers). See docs/dev/reporting.md.
- Real connectors read stable counter fields only: Facebook, Instagram and YouTube followers plus post reactions/likes, comments, shares and views. Insights-endpoint metrics (impressions, reach) and the other platforms show as *configurable* or *Not available* until each is verified against the live API. The sandbox exercises every metric.
- "Engagements" is derived from the parts each platform reports, and the missing parts are listed.
- Reports are e-mailed as signed links, not attachments. Logo upload on reports is left for M6 white-label; M5 uses name, accent colour and footer.
- Default AI model is `claude-opus-5-5` (plan D15 said Sonnet). Set `AI_MODEL=claude-sonnet-5-5` for lower cost.

---

## M6 — Commercial & advanced ✅ implemented 31 Oct 2026 (staging drills pending)

**Deliverables**
- Billing: plans/prices admin, Razorpay subscriptions (monthly/annual), trials, coupons, GST invoices (CGST/SGST vs IGST by place of supply, sequential numbering per financial year), payment webhooks (signature + dedupe), dunning → grace → suspended gating, plan limit enforcement across the app (clients, accounts, users, storage, AI credits, scheduled/published volume).
- Social inbox on permitted surfaces only (doc 05 §4): webhook ingestion, polling fallback, assignment, tags, saved replies, internal notes, moderation where supported, retention policy.
- White-label: agency logo/colours, branded report templates (custom domains dropped: one address for all agencies), optional custom email sender (verified domain via SES/Postmark).
- Super admin: system dashboard, provisioning, payments, integrations, quota/rate dashboards, failed-jobs view, token-expiry alerts, incidents, backup status, support access audit.
- Compliance: tenant data export (ZIP of JSON/CSV + media manifest), tenant deletion workflow (grace period → final backup → drop), retention jobs, DPDPA privacy notice support.
- Production hardening: load test (k6) of dispatcher and composer, security review (OWASP ASVS L2 checklist), pen-test fixes, backup restore drill, chaos tests on workers.

**Acceptance**
- [x] Suspended tenant: login allowed, billing page only, publishing paused (jobs deferred, not dropped). Over-limit: creation blocked with an upgrade prompt.
- [x] All money in paise. Invoice totals reconcile (sum of lines + tax = total) in property tests.
- [x] Webhook replay (billing + inbox) is idempotent.
- [ ] Per-tenant restore drill documented (docs/runbooks/backup-restore.md); **passing it in staging is still to do**, as it needs staging infrastructure.

**Delivered differently or deferred**
- GST is computed once and split CGST/SGST or charged as IGST, so the gross is the same in both regimes. Invoices are derived from the amount paid. Refunds (admin-initiated or from the Razorpay dashboard) issue GST credit notes in their own series (added 31 Oct 2026).
- Coupons need a matching Razorpay offer (`gateway_offer_id`) so the charged amount includes the discount.
- Inbox: comment threads for Facebook, Instagram, Threads and YouTube (sandbox for the rest). DMs and reviews stay blocked/unavailable per the capability data. LinkedIn comments show as configurable (needs Community Management API approval). There is one `messages` table for comments and replies instead of a separate `comments` table.
- White-label sends e-mail under the agency name, and from the agency's own address once its domain passes SES Easy DKIM verification (SES only; Postmark not built).
- Hardening: k6 scripts, the ASVS L2 checklist and the restore runbook are in the repo. Running the load test, the external pen test, the restore drill and worker chaos tests needs staging and is still to do. Security headers (CSP with script nonces, HSTS, frame denial) are sent by middleware; CSP still allows `unsafe-eval` for Alpine.
- Super admin: billing, operations (failed jobs, backups, token alerts, usage, incidents with a banner in affected workspaces). Quota dashboards per provider are summarised from usage_summaries, not live provider quotas.
