From 04d37a9d1259e09ff18274c08d682bd8b33357aa Mon Sep 17 00:00:00 2001 From: Mahdi Date: Sun, 20 Sep 2026 10:54:29 +0330 Subject: [PATCH] Add design spec for BillingAccount, per-org modules, and /adm polish. EOF Co-authored-by: Cursor --- ...26-09-20-billing-account-modules-design.md | 202 ++++++++++++++++++ 1 file changed, 202 insertions(+) create mode 100644 docs/superpowers/specs/2026-09-20-billing-account-modules-design.md diff --git a/docs/superpowers/specs/2026-09-20-billing-account-modules-design.md b/docs/superpowers/specs/2026-09-20-billing-account-modules-design.md new file mode 100644 index 0000000..b2035cc --- /dev/null +++ b/docs/superpowers/specs/2026-09-20-billing-account-modules-design.md @@ -0,0 +1,202 @@ +# 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.