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

8.9 KiB
Raw Permalink Blame History

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.