Skip to main content

Overview

Subscriptions in Tresa Contafy manage user access to features and enforce plan limits. Each subscription is linked to a user account and integrates with Stripe for payment processing.
Subscriptions determine profile limits, storage quotas, and feature access based on the selected plan tier.

Why Subscriptions Matter

Proper subscription management ensures:
  • Access Control: Users can only access features available in their plan
  • Resource Limits: Enforce profile and data limits per plan tier
  • Billing Automation: Stripe handles recurring payments and invoice generation
  • Upgrade Path: Clear upgrade paths for growing businesses

Data Structure

interface SubscriptionAttributes {
  id: string;                    // UUID primary key
  user_id: string;               // User reference
  
  // Stripe integration
  stripe_customer_id: string | null;
  stripe_subscription_id: string | null;
  
  // Plan details
  plan: 'FREE' | 'BASIC' | 'PRO' | 'ENTERPRISE';
  plan_price: number;            // Monthly price in MXN
  status: SubscriptionStatus;
  
  // Billing cycle
  current_period_start: Date | null;
  current_period_end: Date | null;
  
  // Cancellation
  cancel_at_period_end: boolean;
  canceled_at: Date | null;
  
  created_at: Date;
  updated_at: Date;
}

type SubscriptionStatus = 
  | 'ACTIVE'      // Active subscription
  | 'CANCELLED'   // User cancelled
  | 'EXPIRED'     // Period ended without renewal
  | 'PAST_DUE'    // Payment failed, grace period
  | 'UNPAID'      // Payment failed, suspended
  | 'TRIALING';   // Free trial period

Plan Tiers

FREE

Free Forever
  • 1 active profile
  • Basic features
  • Manual XML uploads only
  • Community support
Price: $0 MXN/month

BASIC

Starter Plan
  • 3 active profiles
  • SAT automatic sync
  • Email support
  • Monthly reports
Price: ~$299 MXN/month

PRO

Professional Plan
  • 10 active profiles
  • Advanced analytics
  • Priority support
  • API access
  • Custom categories
Price: ~$799 MXN/month

ENTERPRISE

Enterprise Plan
  • Unlimited profiles
  • Dedicated account manager
  • SLA guarantees
  • Custom integrations
  • White-label option
Price: Custom pricing

Subscription Status

Normal active subscription. User has full access to plan features.
{
  "status": "ACTIVE",
  "current_period_start": "2026-03-01T00:00:00.000Z",
  "current_period_end": "2026-04-01T00:00:00.000Z",
  "cancel_at_period_end": false
}
User is in a free trial period (typically 14 days for paid plans).
{
  "status": "TRIALING",
  "current_period_start": "2026-03-01T00:00:00.000Z",
  "current_period_end": "2026-03-15T00:00:00.000Z"
}
Full plan features available during trial.
Payment failed but in grace period. User retains limited access.
{
  "status": "PAST_DUE",
  "current_period_end": "2026-03-01T00:00:00.000Z"
}
Stripe automatically retries payment. Profile creation may be frozen.
Payment failed after retry attempts. Access severely restricted.
{
  "status": "UNPAID"
}
User can view data but cannot create new profiles or upload CFDIs.
User cancelled subscription but still has access until period end.
{
  "status": "CANCELLED",
  "cancel_at_period_end": true,
  "canceled_at": "2026-03-15T10:00:00.000Z",
  "current_period_end": "2026-04-01T00:00:00.000Z"
}
After period_end, status changes to EXPIRED.
Subscription ended. User downgraded to FREE plan.
{
  "status": "EXPIRED",
  "canceled_at": "2026-03-15T10:00:00.000Z"
}
Existing profiles beyond FREE limit are frozen.

Stripe Integration

Subscriptions integrate with Stripe for payment processing:

Customer Creation

When a user subscribes to a paid plan:
// Stripe customer created
{
  "stripe_customer_id": "cus_1234567890abcdef",
  "stripe_subscription_id": null
}

Subscription Creation

{
  "stripe_customer_id": "cus_1234567890abcdef",
  "stripe_subscription_id": "sub_1234567890abcdef",
  "plan": "PRO",
  "plan_price": 799.00,
  "status": "ACTIVE",
  "current_period_start": "2026-03-01T00:00:00.000Z",
  "current_period_end": "2026-04-01T00:00:00.000Z"
}

Webhook Handling

Tresa processes Stripe webhooks to keep subscription status synchronized:
  • customer.subscription.created: New subscription activated
  • customer.subscription.updated: Plan changed or renewed
  • customer.subscription.deleted: Subscription cancelled/expired
  • invoice.payment_succeeded: Payment successful, renew period
  • invoice.payment_failed: Payment failed, mark as PAST_DUE
All webhook events are verified using Stripe signature validation to ensure authenticity.

Plan Limits Enforcement

Profile Limits

The system enforces profile limits based on the user’s plan:
const PLAN_LIMITS = {
  FREE: { profiles: 1 },
  BASIC: { profiles: 3 },
  PRO: { profiles: 10 },
  ENTERPRISE: { profiles: Infinity }
};
When creating a profile:
// Check if user can create a profile
const canCreate = await ProfileService.canCreateProfile(userId, currentPlan);

if (!canCreate) {
  return res.status(403).json({
    error: 'Has alcanzado el límite de perfiles para tu plan',
    limit: PLAN_LIMITS[currentPlan].profiles,
    current: activeProfileCount,
    code: 'PROFILE_LIMIT_REACHED'
  });
}

Profile Freezing

When a user downgrades or subscription expires:
  1. Count active profiles
  2. If exceeds new plan limit, freeze excess profiles
  3. Set frozen_reason: 'plan_limit' on frozen profiles
// Example: User downgrades from PRO (10) to BASIC (3)
// Profiles 4-10 are frozen
{
  "frozen": true,
  "frozen_reason": "plan_limit",
  "frozen_at": "2026-03-01T00:00:00.000Z"
}
Frozen profiles:
  • Cannot upload new CFDIs
  • Cannot create expenses
  • Existing data remains viewable
  • Can be unfrozen by upgrading plan

Relationships

User Relationship

Subscription.belongsTo(User, { foreignKey: 'user_id', as: 'user' });
User.hasMany(Subscription, { foreignKey: 'user_id', as: 'subscriptions' });
Each subscription belongs to one user. Users can have multiple subscription records (historical), but only one active subscription at a time.

Indexes

indexes: [
  { fields: ['user_id'] },               // Get user's subscription
  { fields: ['stripe_customer_id'] },    // Stripe customer lookup
  { fields: ['stripe_subscription_id'] }, // Stripe subscription lookup
  { fields: ['status'] },                // Filter by status
]

Usage Examples

Get Current Subscription

GET /api/subscription
Authorization: Bearer <token>
Response:
{
  "message": "Suscripción obtenida exitosamente",
  "data": {
    "id": "990e8400-e29b-41d4-a716-446655440000",
    "user_id": "660e8400-e29b-41d4-a716-446655440000",
    "stripe_customer_id": "cus_1234567890abcdef",
    "stripe_subscription_id": "sub_1234567890abcdef",
    "plan": "PRO",
    "plan_price": 799.00,
    "status": "ACTIVE",
    "current_period_start": "2026-03-01T00:00:00.000Z",
    "current_period_end": "2026-04-01T00:00:00.000Z",
    "cancel_at_period_end": false,
    "canceled_at": null,
    "created_at": "2026-03-01T00:00:00.000Z",
    "updated_at": "2026-03-01T00:00:00.000Z"
  }
}

Create Checkout Session

POST /api/subscription/checkout
Content-Type: application/json
Authorization: Bearer <token>

{
  "plan": "PRO",
  "success_url": "https://app.tresacontafy.com/dashboard?checkout=success",
  "cancel_url": "https://app.tresacontafy.com/pricing"
}
Response:
{
  "sessionId": "cs_test_1234567890abcdef",
  "url": "https://checkout.stripe.com/pay/cs_test_1234567890abcdef"
}
Redirect user to the Stripe checkout URL.

Cancel Subscription

POST /api/subscription/cancel
Authorization: Bearer <token>
Response:
{
  "message": "Suscripción cancelada. Tendrás acceso hasta el final del período actual.",
  "data": {
    "status": "CANCELLED",
    "cancel_at_period_end": true,
    "canceled_at": "2026-03-15T10:00:00.000Z",
    "current_period_end": "2026-04-01T00:00:00.000Z"
  }
}
Cancellation takes effect at the end of the current billing period. User retains access until then.

Resume Subscription

POST /api/subscription/resume
Authorization: Bearer <token>
Only works if subscription is in CANCELLED status with cancel_at_period_end: true.

Change Plan

POST /api/subscription/change-plan
Content-Type: application/json
Authorization: Bearer <token>

{
  "new_plan": "ENTERPRISE"
}
Stripe prorates the charge/credit for immediate plan changes.

Best Practices

Handle Webhooks Properly

Always verify Stripe webhook signatures and handle all subscription events to keep status synchronized.

Graceful Degradation

When subscription expires, freeze profiles gracefully rather than deleting data.

Clear Upgrade Prompts

Show clear upgrade prompts when users hit plan limits, with direct links to checkout.

Trial Management

Offer trials for paid plans to reduce friction and increase conversions.

Common Scenarios

New User Signup

  1. User registers → FREE plan subscription created automatically
  2. Status: ACTIVE, plan: FREE
  3. Can create 1 profile immediately

Upgrading to Paid Plan

  1. User clicks “Upgrade to PRO”
  2. Checkout session created with Stripe
  3. After payment: webhook updates subscription
  4. Status: ACTIVE, plan: PRO, 10 profile limit
  5. Previously frozen profiles (if any) can be unfrozen

Payment Failure

  1. Payment fails on renewal date
  2. Webhook: invoice.payment_failed
  3. Status changes to PAST_DUE
  4. Stripe retries payment automatically
  5. If all retries fail: Status → UNPAID, profiles frozen

Cancellation Flow

  1. User cancels subscription
  2. cancel_at_period_end: true
  3. Status: CANCELLED (but still active)
  4. At period end: Status → EXPIRED
  5. Plan → FREE, excess profiles frozen

Profiles

Profile limits enforced by subscription

Invoices

Storage limits based on plan

Build docs developers (and LLMs) love