Add design spec for BillingAccount, per-org modules, and /adm polish.
EOF Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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.
|
||||
Reference in New Issue
Block a user