Skip to main content

Subscriptions

Subscriptions manages membership plans, entitlements, billing cycles, and renewals. This page outlines the Web/Admin operations and API interactions with critical business logic for billing calculations, proration handling, and subscription lifecycle management.

Core Concepts

  • Subscription Plans: Tiered plans with different features, pricing, and billing cycles
  • Billing Cycles: MONTHLY/ANNUAL with automatic renewal and proration logic
  • Status Management: ACTIVE, EXPIRED, CANCELLED with grace periods and reactivation
  • Proration Logic: Mid-cycle plan changes with automatic credit/debit calculations
  • Feature Entitlements: Role-based access to platform features based on subscription tier
  • Renewal Management: Automatic billing, payment retry logic, and expiration handling

Web/Admin Capabilities

1) Plans List & Filter

Business Rules:

  • Filter by billing period, plan tier, and active status
  • Display pricing with proration calculations for mid-cycle changes
  • Show feature entitlements and usage limits for each plan
  • Highlight popular plans and conversion rates

Implementation:

// Filter logic with pricing and feature calculations
const filtered = plans.filter((plan) => {
// Basic filtering
const matchesPeriod = !periodFilter || plan.billing_period === periodFilter;
const matchesActive = !activeOnly || plan.isActive;
const matchesTier = !tierFilter || plan.tier === tierFilter;

// Calculate proration rates for display
plan.prorationRates = calculateProrationRates(plan);

// Calculate feature limits
plan.featureLimits = calculateFeatureLimits(plan);

// Calculate conversion metrics
plan.conversionMetrics = calculateConversionMetrics(plan);

return matchesPeriod && matchesActive && matchesTier;
});

// Proration calculation for mid-cycle plan changes
const calculateProrationRates = (plan) => {
const dailyRate = plan.price / (plan.billing_period === 'MONTHLY' ? 30 : 365);
const weeklyRate = dailyRate * 7;

return {
daily: Math.round(dailyRate * 100) / 100,
weekly: Math.round(weeklyRate * 100) / 100,
monthly:
plan.billing_period === 'MONTHLY'
? plan.price
: Math.round(dailyRate * 30 * 100) / 100,
};
};

// Feature limits calculation
const calculateFeatureLimits = (plan) => {
return {
max_appointments: plan.features.max_appointments || 'unlimited',
max_assessments: plan.features.max_assessments || 'unlimited',
max_lesson_plans: plan.features.max_lesson_plans || 'unlimited',
priority_support: plan.features.priority_support || false,
advanced_analytics: plan.features.advanced_analytics || false,
};
};

2) Create/Update Plan (GraphQL)

Plan Management Business Rules:

  • Pricing Validation: Ensure pricing is competitive and follows business rules
  • Feature Configuration: Define feature entitlements and usage limits
  • Billing Cycle Logic: Handle different billing periods with proration calculations
  • Plan Hierarchy: Maintain tier structure (Basic, Premium, Enterprise)
  • Migration Rules: Define upgrade/downgrade paths and restrictions

Plan Validation Logic (Updated - Store Linked):

// Validate plan configuration (store-linked, no manual price fields)
const validatePlan = (input: {
name: string;
description: string;
features_ids: string[];
app_store_product_ids: string[]; // Apple
playstore_product_ids: string[]; // Google
child_count: number | string;
}) => {
const errors: string[] = [];

if (!input.name?.trim()) errors.push('Plan name is required');
if (!input.description?.trim()) errors.push('Description is required');

if (!Array.isArray(input.features_ids) || input.features_ids.length < 1) {
errors.push('At least one feature must be selected');
}

if (
!Array.isArray(input.app_store_product_ids) ||
input.app_store_product_ids.length !== 2
) {
errors.push('Exactly two Apple App Store plans must be linked');
}
if (
!Array.isArray(input.playstore_product_ids) ||
input.playstore_product_ids.length !== 2
) {
errors.push('Exactly two Google Play Store plans must be linked');
}

const childCount = Number(input.child_count);
if (!Number.isFinite(childCount) || childCount <= 0) {
errors.push('Child count must be a positive number');
}

return errors;
};

// Calculate effective pricing with discounts
const calculateEffectivePricing = (plan) => {
const basePrice = plan.price;
const billingPeriod = plan.billing_period;

if (billingPeriod === 'ANNUAL') {
const monthlyEquivalent = basePrice / 12;
const monthlyPlanPrice = getMonthlyPlanPrice(plan.tier);
const discount = (monthlyPlanPrice - monthlyEquivalent) / monthlyPlanPrice;

return {
base_price: basePrice,
monthly_equivalent: monthlyEquivalent,
discount_percentage: Math.round(discount * 100),
savings_amount: monthlyPlanPrice * 12 - basePrice,
};
}

return {
base_price: basePrice,
monthly_equivalent: basePrice,
discount_percentage: 0,
savings_amount: 0,
};
};

GraphQL Implementation (Updated payloads):

import { gql, ApolloClient } from '@apollo/client';

const UPSERT_PLAN = gql`
mutation UpsertPlan($input: UpsertPlanInput!) {
upsertPlan(input: $input) {
success
message
data {
id
name
features
child_count
subscription_products {
platform
product_id
billing_frequency
price
}
}
}
}
`;

async function upsertPlan(
client: ApolloClient<unknown>,
input: {
name: string;
description: string;
features_ids: string[];
app_store_product_ids: string[];
playstore_product_ids: string[];
child_count: number;
},
) {
// Validate (store-linked plan requirements)
const validationErrors = validatePlan(input);
if (validationErrors.length > 0) {
throw new Error(`Plan validation failed: ${validationErrors.join(', ')}`);
}

// Build payload without manual prices (derived from store products)
const processedInput = {
name: input.name.trim(),
description: input.description.trim(),
features_ids: input.features_ids,
app_store_product_ids: input.app_store_product_ids,
playstore_product_ids: input.playstore_product_ids,
child_count: Number(input.child_count),
created_by: currentUser.id,
last_updated: new Date().toISOString(),
} as const;

const { data } = await client.mutate({
mutation: UPSERT_PLAN,
variables: { input: processedInput },
});

if (!data?.upsertPlan?.success) {
throw new Error(data?.upsertPlan?.message || 'Save plan failed');
}

return data.upsertPlan.data;
}

2.1) Plan Details & Price Ranges (Display)

Monthly/yearly price ranges are computed from linked store products rather than stored as direct fields.

// Build platform-specific plan lists
const appleAppstorePlans = details.subscription_products
.filter((p) => p.platform === 'IOS' && !p.is_deleted)
.map((p) => ({
id: p.product_id,
name: p.product_name,
price: p.price,
billing_frequency: p.billing_frequency,
}));

const googlePlaystorePlans = details.subscription_products
.filter((p) => p.platform === 'ANDROID' && !p.is_deleted)
.map((p) => ({
id: p.product_id,
name: p.product_name,
price: p.price,
billing_frequency: p.billing_frequency,
}));

// Price range calculation
const calculatePriceRange = (plans: any[], frequency: 'MONTHLY' | 'YEARLY') => {
const filtered = plans.filter(
(plan) => plan.billing_frequency === frequency && plan.price,
);
if (filtered.length === 0) return null;
const prices = filtered.map((plan) => Number(plan.price));
const minPrice = Math.min(...prices);
const maxPrice = Math.max(...prices);
return minPrice === maxPrice ? `${minPrice}` : `${minPrice}/${maxPrice}`;
};

const monthlyRange = [
calculatePriceRange(appleAppstorePlans, 'MONTHLY'),
calculatePriceRange(googlePlaystorePlans, 'MONTHLY'),
]
.filter(Boolean)
.join('/');

const yearlyRange = [
calculatePriceRange(appleAppstorePlans, 'YEARLY'),
calculatePriceRange(googlePlaystorePlans, 'YEARLY'),
]
.filter(Boolean)
.join('/');

3) Assign Subscription to User (GraphQL)

Subscription Assignment Business Rules:

  • Plan Compatibility: Ensure user can access the assigned plan features
  • Proration Logic: Calculate credits/debits for mid-cycle plan changes
  • Billing Integration: Set up automatic billing and payment methods
  • Feature Activation: Immediately activate plan features upon assignment
  • Migration Handling: Handle upgrades/downgrades with proper proration

Proration Calculation Logic:

// Calculate proration for plan changes
const calculateProration = (currentSubscription, newPlan, changeDate) => {
const currentPlan = currentSubscription.plan;
const currentBillingCycle = currentSubscription.billing_cycle;

// Calculate remaining days in current cycle
const cycleEndDate = new Date(currentBillingCycle.end_date);
const remainingDays = Math.ceil(
(cycleEndDate - changeDate) / (1000 * 60 * 60 * 24),
);

// Calculate daily rates
const currentDailyRate =
currentPlan.price / (currentPlan.billing_period === 'MONTHLY' ? 30 : 365);
const newDailyRate =
newPlan.price / (newPlan.billing_period === 'MONTHLY' ? 30 : 365);

// Calculate credit for unused portion of current plan
const creditAmount = currentDailyRate * remainingDays;

// Calculate charge for new plan
const chargeAmount = newDailyRate * remainingDays;

// Net proration amount
const prorationAmount = chargeAmount - creditAmount;

return {
remaining_days: remainingDays,
credit_amount: Math.round(creditAmount * 100) / 100,
charge_amount: Math.round(chargeAmount * 100) / 100,
net_proration: Math.round(prorationAmount * 100) / 100,
proration_type: prorationAmount > 0 ? 'CHARGE' : 'CREDIT',
};
};

// Validate subscription assignment
const validateSubscriptionAssignment = (user, plan) => {
const errors = [];

// Check if user already has an active subscription
const existingSubscription = getUserActiveSubscription(user.id);
if (existingSubscription && existingSubscription.status === 'ACTIVE') {
errors.push('User already has an active subscription');
}

// Check plan availability
if (!plan.isActive) {
errors.push('Plan is not currently available');
}

// Check user eligibility (e.g., organization limits)
const userOrganization = getUserOrganization(user.id);
if (plan.tier === 'ENTERPRISE' && userOrganization.size < 50) {
errors.push('Enterprise plan requires organization with 50+ users');
}

return errors;
};

GraphQL Implementation:

const ASSIGN_SUBSCRIPTION = gql`
mutation AssignSubscription($input: AssignSubscriptionInput!) {
assignSubscription(input: $input) {
success
message
data {
id
user_id
plan_id
status
start_date
end_date
billing_cycle {
start_date
end_date
next_billing_date
}
proration_details {
remaining_days
credit_amount
charge_amount
net_proration
proration_type
}
features_activated
}
}
}
`;

async function assignSub(
client: ApolloClient<unknown>,
input: AssignSubscriptionInput,
) {
// Validate assignment
const user = await getUserDetails(input.user_id);
const plan = await getPlanDetails(input.plan_id);
const validationErrors = validateSubscriptionAssignment(user, plan);

if (validationErrors.length > 0) {
throw new Error(
`Assignment validation failed: ${validationErrors.join(', ')}`,
);
}

// Calculate proration if changing from existing subscription
const existingSubscription = getUserActiveSubscription(input.user_id);
let prorationDetails = null;

if (existingSubscription) {
prorationDetails = calculateProration(
existingSubscription,
plan,
input.start_date || new Date(),
);
}

const processedInput = {
...input,
start_date: input.start_date || new Date().toISOString(),
proration_details: prorationDetails,
assigned_by: currentUser.id,
status: 'ACTIVE',
};

const { data } = await client.mutate({
mutation: ASSIGN_SUBSCRIPTION,
variables: { input: processedInput },
});

if (!data?.assignSubscription?.success) {
throw new Error(data?.assignSubscription?.message || 'Assign failed');
}

return data.assignSubscription.data;
}

4) Cancel / Renew Subscription (GraphQL)

Cancellation Business Rules:

  • Grace Period: Allow continued access until end of billing cycle
  • Refund Policy: Prorated refunds based on cancellation timing
  • Feature Deactivation: Gradual feature restriction during grace period
  • Retention Logic: Offer discounts or alternative plans to retain users

Renewal Business Rules:

  • Automatic Renewal: Process renewals before expiration date
  • Payment Retry: Multiple retry attempts for failed payments
  • Grace Period: Allow continued access during payment issues
  • Plan Updates: Handle plan changes during renewal process

Cancellation Logic:

// Calculate cancellation refund and grace period
const calculateCancellationDetails = (subscription, cancellationDate) => {
const billingCycle = subscription.billing_cycle;
const cycleEndDate = new Date(billingCycle.end_date);
const remainingDays = Math.ceil(
(cycleEndDate - cancellationDate) / (1000 * 60 * 60 * 24),
);

// Calculate refund amount (prorated)
const dailyRate =
subscription.plan.price /
(subscription.plan.billing_period === 'MONTHLY' ? 30 : 365);
const refundAmount = dailyRate * remainingDays;

// Grace period (allow access until cycle end)
const gracePeriodEnd = cycleEndDate;

return {
remaining_days: remainingDays,
refund_amount: Math.round(refundAmount * 100) / 100,
grace_period_end: gracePeriodEnd,
immediate_cancellation: remainingDays <= 7, // Cancel immediately if less than 7 days
feature_restrictions: calculateFeatureRestrictions(
subscription,
remainingDays,
),
};
};

// Calculate feature restrictions during grace period
const calculateFeatureRestrictions = (subscription, remainingDays) => {
const restrictions = [];

if (remainingDays <= 3) {
restrictions.push('LIMITED_APPOINTMENTS'); // Max 2 appointments
restrictions.push('NO_NEW_ASSESSMENTS'); // Cannot create new assessments
}

if (remainingDays <= 1) {
restrictions.push('READ_ONLY_ACCESS'); // View-only mode
}

return restrictions;
};

GraphQL Implementation:

const CANCEL_SUBSCRIPTION = gql`
mutation CancelSubscription($input: CancelSubscriptionInput!) {
cancelSubscription(input: $input) {
success
message
data {
id
status
cancellation_date
grace_period_end
refund_amount
feature_restrictions
retention_offers {
discount_percentage
alternative_plans
}
}
}
}
`;

const RENEW_SUBSCRIPTION = gql`
mutation RenewSubscription($input: RenewSubscriptionInput!) {
renewSubscription(input: $input) {
success
message
data {
id
status
new_billing_cycle {
start_date
end_date
next_billing_date
}
payment_status
features_restored
}
}
}
`;

async function cancelSub(client: ApolloClient<unknown>, id: string) {
// Get subscription details
const subscription = await getSubscriptionDetails(id);
const cancellationDetails = calculateCancellationDetails(
subscription,
new Date(),
);

const { data } = await client.mutate({
mutation: CANCEL_SUBSCRIPTION,
variables: {
input: {
subscription_id: id,
cancellation_details: cancellationDetails,
},
},
});

if (!data?.cancelSubscription?.success) {
throw new Error(data?.cancelSubscription?.message || 'Cancel failed');
}

return data.cancelSubscription.data;
}

async function renewSub(client: ApolloClient<unknown>, id: string) {
const { data } = await client.mutate({
mutation: RENEW_SUBSCRIPTION,
variables: { input: { subscription_id: id } },
});

if (!data?.renewSubscription?.success) {
throw new Error(data?.renewSubscription?.message || 'Renewal failed');
}

return data.renewSubscription.data;
}

5) Export & Analytics

Export Business Rules:

  • Subscription Reports: Include plan details, billing cycles, and usage metrics
  • Revenue Analytics: Calculate MRR, ARR, and churn rates
  • Feature Usage: Track feature adoption and usage patterns
  • Retention Analysis: Monitor subscription lifecycle and retention rates

Export Implementation:

// Generate comprehensive subscription report
const generateSubscriptionReport = (subscriptions, dateRange) => {
const report = {
summary: {
total_subscriptions: subscriptions.length,
active_subscriptions: subscriptions.filter((s) => s.status === 'ACTIVE')
.length,
monthly_revenue: calculateMRR(subscriptions),
annual_revenue: calculateARR(subscriptions),
churn_rate: calculateChurnRate(subscriptions, dateRange),
},
plan_breakdown: calculatePlanBreakdown(subscriptions),
revenue_metrics: {
mrr: calculateMRR(subscriptions),
arr: calculateARR(subscriptions),
average_revenue_per_user: calculateARPU(subscriptions),
lifetime_value: calculateLTV(subscriptions),
},
subscriptions: subscriptions.map((subscription) => ({
id: subscription.id,
user_name: subscription.user.name,
plan_name: subscription.plan.name,
plan_tier: subscription.plan.tier,
status: subscription.status,
start_date: subscription.start_date,
end_date: subscription.end_date,
billing_cycle: subscription.billing_cycle,
total_revenue: calculateSubscriptionRevenue(subscription),
usage_metrics: calculateUsageMetrics(subscription),
})),
};

return report;
};

// Calculate Monthly Recurring Revenue (MRR)
const calculateMRR = (subscriptions) => {
return subscriptions
.filter((s) => s.status === 'ACTIVE')
.reduce((total, subscription) => {
const monthlyRate =
subscription.plan.billing_period === 'MONTHLY'
? subscription.plan.price
: subscription.plan.price / 12;
return total + monthlyRate;
}, 0);
};

const csv = toCsv(generateSubscriptionReport(rows, dateRange));
downloadCsv('subscription-analytics-report.csv', csv);

Data Flow (Web ↔ Backend)

Critical Business Logic Flow:

  1. Plan Management: Create/update plans → Pricing validation → Feature configuration → Proration calculation
  2. Subscription Assignment: User assignment → Plan compatibility → Proration calculation → Feature activation
  3. Billing Management: Automatic renewal → Payment processing → Grace period handling → Feature restrictions
  4. Analytics: Revenue calculation → Usage tracking → Churn analysis → Retention optimization

GraphQL Operations with Business Rules:

ActionOperationBusiness Rules
List PlanssubscriptionPlans(filter)Pricing display, feature limits, conversion metrics
Upsert PlanupsertPlan(input)Pricing validation, tier hierarchy, annual discounts
AssignassignSubscription(input)Plan compatibility, proration calculation, feature activation
CancelcancelSubscription(input)Grace period, refund calculation, feature restrictions
RenewrenewSubscription(input)Payment retry, billing cycle update, feature restoration

Error Handling & Validation:

  • Plan Validation: "Plan price must be greater than zero"
  • Annual Discount: "Annual plan must provide at least 10% discount over monthly"
  • User Eligibility: "Enterprise plan requires organization with 50+ users"
  • Active Subscription: "User already has an active subscription"
  • Grace Period: "Subscription will remain active until end of billing cycle"

Security & Access Control

  • Admin-Only Operations: Plan creation and subscription assignment require admin access
  • User Permissions: Users can only view and manage their own subscriptions
  • Feature Access: Role-based feature access based on subscription tier
  • Audit Trail: All subscription changes logged with user information and timestamps
  • Data Privacy: Subscription data protected by role-based permissions and encryption