Files
CRM_Montzar/docs/superpowers/specs/2026-09-20-billing-account-modules-design.md

203 lines
8.9 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Design: BillingAccount + per-org modules + /adm polish
**Date:** 2026-09-20
**Status:** Approved for planning
**Scope:** Superadmin (`/adm`) subscription control, per-organization module entitlements, CRM enforcement, and visual polish of the platform admin UI.
## Goals
1. **Subscription on account owner** via a dedicated `BillingAccount` (not raw fields on `Organization`).
2. **Modules per environment** — every CRM menu surface can be enabled/disabled per org from `/adm`.
3. **Manual plan activation now**; structure ready for a future landing-page payment gateway (no gateway in this phase).
4. **Expiry UX:** show remaining days in the CRM app header; warn from 3 days before; after expiry **block login** with a clear “اشتراک منقضی شده” message.
5. **Migrate existing accounts:** create a `BillingAccount` with plan `trial` and **15 days from migration time** so current customers keep working until extended.
6. **Polish `/adm` UI** to feel more app-like (same Vue + CRM CSS tokens; no separate Ant Design/Tailwind app).
## Non-goals (later)
- Online payment gateway / landing checkout
- Impersonate user login
- Complex multi-tier platform-admin roles
- Automatic invoice generation for Mentekt SaaS itself
## Decisions (locked)
| Topic | Choice |
|---|---|
| Account vs org | Both: subscription on owner (`BillingAccount`); modules on each `Organization` |
| Toggleable modules | Entire menu surface (not only add-ons) |
| Payment | Manual in `/adm` now; `planCode` + optional payment ref reserved for gateway |
| On expiry | Block CRM login; header shows remaining days; warn ≤3 days |
| Existing orgs/users | Backfill trial **15 days** from migrate/seed time |
| Data model style | Dedicated `BillingAccount` entity (approach 2) |
---
## Data model
### `BillingAccount`
One billing account per **owner user** (the SaaS customer).
| Field | Type | Notes |
|---|---|---|
| `id` | cuid | PK |
| `ownerUserId` | string unique | FK → `User` — the account holder |
| `planCode` | string | `trial` \| `monthly` \| `quarterly` \| `semiannual` \| `yearly` \| `custom` |
| `status` | string | `active` \| `expired` \| `suspended` |
| `startsAt` | DateTime | |
| `expiresAt` | DateTime | |
| `maxOrganizations` | int | e.g. trial demo = 1 |
| `externalPaymentRef` | string? | Reserved for future gateway / landing |
| `notes` | string? | Superadmin internal note |
| `createdAt` / `updatedAt` | DateTime | |
Indexes: `ownerUserId` unique; `expiresAt`; `status`.
**Ownership rule:** The billing owner is the user who first registered / is treated as account holder. Membership in other orgs does not create extra billing accounts. Creating a new org as owner still counts against `maxOrganizations` of that user’s `BillingAccount`.
### `Organization.enabledModules`
- `enabledModules String[]` on `Organization` (Prisma).
- Canonical keys live in `@mento/shared` (see Module catalog).
- Empty array after migration for existing orgs means **all modules enabled** (backward compatible), OR we backfill the full catalog — prefer **backfill full catalog** so “empty” can later mean “nothing enabled” if needed.
**Locked choice:** migration sets `enabledModules` to the **full default catalog** for existing orgs. New orgs get a default catalog from the plan template (trial = core set configurable in code; superadmin can change immediately).
### Plan presets (manual buttons in `/adm`)
| planCode | Default duration | Default maxOrgs |
|---|---|---|
| `trial` | 15 days | 1 |
| `monthly` | 30 days | configurable (default 3) |
| `quarterly` | 90 days | configurable (default 5) |
| `semiannual` | 180 days | configurable (default 10) |
| `yearly` | 365 days | configurable (default 20) |
| `custom` | superadmin picks `expiresAt` + `maxOrganizations` | as set |
Extending an active plan from `/adm` sets `startsAt` (if missing), `expiresAt` = now + duration (or custom date), `status = active`, updates `planCode`.
### Backfill migration
1. For every `User` who has at least one `Membership` with `role = owner` (or is the sole owner of an org), ensure one `BillingAccount`:
- Prefer: one account per distinct owner user who owns ≥1 org.
- `planCode = trial`, `status = active`, `startsAt = now`, `expiresAt = now + 15 days`, `maxOrganizations = max(1, current org count they own)` so nobody is suddenly blocked for having multiple orgs already.
2. Set `Organization.enabledModules` to full catalog for all existing orgs.
---
## Module catalog
Keys align with nav hubs/cards (and API surfaces). Shared constant e.g. `ORG_MODULE_KEYS` / labels in `@mento/shared`.
Suggested keys (final list in implementation may split cards 1:1 with nav):
- `customers` (people + companies)
- `pipeline` (sales pipeline / deals)
- `products`
- `invoices`
- `quotes`
- `commissions`
- `sales_forecast`
- `activities`
- `tickets`
- `workflow_automations`
- `message_automations`
- `lead_forms`
- `project_boards`
- `inventory`
- `cheques`
- `attachments`
- `loyalty` (if present in nav/settings)
- `reports` (if in nav)
- `notes` / smart notes if separately gated
- `tahesab`
- `custom_fields`
- `currencies`
- `team_settings` (keep always-on for owners? **Locked:** still toggleable, but warn in UI that disabling team settings locks self-service; login/org switcher remain available)
Settings hub items are individually toggleable. Core shell (home, profile, logout, org switcher) stays available when subscription is active.
---
## Enforcement
### Login / session
On CRM password login and org switch / token refresh:
1. Resolve `BillingAccount` for the user (by `ownerUserId`).
- If user is **not** a billing owner (invited member only): use the **org owner’s** billing account for the active organization (subscription is per owner account that owns the env).
- **Locked rule:** Entitlement for entering an org is the billing account of that org’s **active owner membership** (any active owner). If multiple owners, any one’s active billing is enough; prefer the earliest owner membership’s user.
2. If `status !== active` OR `expiresAt < now` → reject with message اشتراک منقضی / معلق؛ mark status `expired` lazily when detected.
3. Members of an expired owner cannot log into that org either.
### Create organization
- Only if caller has `BillingAccount` with `status = active` and `expiresAt >= now`.
- Count orgs where user has `role = owner` membership; must be `< maxOrganizations`.
### Modules
- **Web:** filter `NAV_HUBS` cards/shortcuts and router guards by `organization.enabledModules` (loaded with org/session payload).
- **API:** guard controllers/services for module-gated routes; return 403 with clear Persian message if module disabled.
- Role permissions (`RolePermissionKey`) remain orthogonal: module must be on **and** role must allow.
### Remaining days UI
- Include `subscription: { planCode, status, expiresAt, daysRemaining, warn }` in login/me/org bootstrap DTO.
- App header chip: «N روز باقی‌مانده».
- If `daysRemaining <= 3` and `> 0`: warning style banner/chip.
- If expired: user never reaches shell (login blocked).
---
## Platform admin (`/adm`) API
Authenticated with existing platform-admin JWT.
- `GET/PATCH` billing for a user: plan, dates, maxOrgs, status, notes.
- Shortcuts: `POST .../billing/apply-plan` with `{ planCode, maxOrganizations?, expiresAt? }`.
- `PATCH /platform/organizations/:id/modules` with `{ enabledModules: string[] }`.
- List users/orgs already exists; extend DTOs with billing summary + module flags.
---
## Platform admin UI polish
Keep Vue 3 + existing CSS variables (`--accent`, `--line`, cards). Improvements:
- Denserv app-like shell: clearer hierarchy, sticky subheaders, soft gradients consistent with CRM auth pages.
- User detail: billing card with plan chips + date picker (Jalali) + max orgs.
- Org detail: module checklist grouped by hub (مشتریان / فروش / …) with select-all per group.
- Dashboard KPIs: optional counts of expiring ≤3 days / expired (nice-to-have in same phase if cheap).
No Ant Design / Tailwind.
---
## Future gateway hook
Landing page later will:
1. Create/find `BillingAccount` by owner.
2. After payment success, set `planCode`, `expiresAt`, `externalPaymentRef`, `status = active`.
No payment code in this phase; do not invent webhook handlers yet.
---
## Risks & edge cases
- Users with multiple owned orgs today: backfill `maxOrganizations` ≥ current owned count.
- Invited users: gated by org owner’s subscription.
- Disabling a module mid-use: existing data kept; API/UI blocked until re-enabled.
- Superadmin `/adm` login unaffected by BillingAccount.
## Testing (manual)
1. Fresh seed/migrate → existing owner has 15-day trial; can log in; header shows days.
2. Set expiresAt to yesterday → login fails with expiry message.
3. Disable `message_automations` on an org → menu gone + API 403.
4. Trial maxOrgs=1 with one owned org → create second org rejected.
5. Apply `monthly` from `/adm` → expiresAt ~+30d, status active.