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:
- Plan Management: Create/update plans → Pricing validation → Feature configuration → Proration calculation
- Subscription Assignment: User assignment → Plan compatibility → Proration calculation → Feature activation
- Billing Management: Automatic renewal → Payment processing → Grace period handling → Feature restrictions
- Analytics: Revenue calculation → Usage tracking → Churn analysis → Retention optimization
GraphQL Operations with Business Rules:
| Action | Operation | Business Rules |
|---|---|---|
| List Plans | subscriptionPlans(filter) | Pricing display, feature limits, conversion metrics |
| Upsert Plan | upsertPlan(input) | Pricing validation, tier hierarchy, annual discounts |
| Assign | assignSubscription(input) | Plan compatibility, proration calculation, feature activation |
| Cancel | cancelSubscription(input) | Grace period, refund calculation, feature restrictions |
| Renew | renewSubscription(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