Skip to content

Plans Domain

The Plans module models a plan as a single aggregate split across multiple persistence tables.

Aggregate Sections

Identity

  • name
  • internalAlias (unique)
  • isActive
  • description
  • isPublic
  • isRecommended
  • color

Pricing

  • model: SUBSCRIPTION or ONE_TIME
  • currency: MXN or USD
  • planPrice
  • setupPrice
  • gatewayFeePayer: BUSINESS, CUSTOMER, or AGENCY
  • taxMode: EXCLUSIVE or INCLUSIVE
  • autoTax
  • subscriptionConfig: nullable JSON object that stores monthly, quarterly, semiannual, and annual discount settings when the model is subscription-based; each frequency includes isEnabled, which must be true for that frequency to be selectable when an agency creates a platform-plan subscription (ONE_TIME billing only applies when model is ONE_TIME)

Resource Limits

The aggregate stores five normalized resource entries and maps them back to a stable object in the API contract:

  • users
  • branches
  • storage
  • aiQueries
  • apiRequests

Each resource stores:

  • quantity — canonical unit depends on resourceKey:
    • STORAGEbytes (persisted as bigint to allow TB/PB quotas)
    • USERS, BRANCHES, AI_QUERIES, API_REQUESTS → integer count
    • -1 is reserved as the "unlimited" sentinel for any resource key
  • limitType: HARD or SOFT
  • alertThreshold from 0 to 100

Why bytes for storage? Keeping the entire storage pipeline in one unit (StorageStats.totalSize, Document.size, plan quota, upload guards) removes the class of bugs where a byte-sized value is compared against a GB-sized limit. The conversion used to happen ad hoc in application handlers; it now only happens at the human input boundary (admin UI / plan creation payload).

Persistence Shape

The module keeps the data split into three tables inside the plans schema:

  • plans.plans: main identity row and soft-delete column
  • plans.plan_pricing: one-to-one pricing row with nullable subscription_config jsonb
  • plans.plan_resource_limits: one row per resource key

This shape keeps unique and filterable fields relational while using JSONB only where nested subscription-frequency configuration is genuinely document-like.