Skip to main content

Subscription Module

The Subscription module is a comprehensive subscription management system for the Comdeall platform that handles the complete lifecycle of subscription plans, child subscriptions, multi-platform integration (iOS, Android, Web), payment processing, and automated subscription workflows. It integrates with multiple payment providers including Razorpay, App Store Connect, and Play Store to provide seamless subscription experiences across all platforms.

Table of Contents

  1. Module Structure
  2. Subscription Endpoints
  3. Core Features
  4. Multi-Platform Architecture
  5. Subscription Lifecycle
  6. Payment Integration
  7. Webhook System
  8. Database Functions & Triggers
  9. Plan Management
  10. Child Subscription Management
  11. Background Job Processing
  12. Data Models
  13. Security & Validation

Module Structure

The Subscription module follows a multi-provider architecture with comprehensive platform support:

@Module({
imports: [
forwardRef(() => PaymentModule),
DBModule,
RedisModule,
JwtModule.register({}),
forwardRef(() => BackgroundModule)
],
controllers: [SubscriptionController],
providers: [
SubscriptionService,
AppstoreJwtService,
AppstoreProvider,
RazorpaySubscriptionsProvider,
],
exports: [SubscriptionService],
})
export class SubscriptionModule {}

Components:

  • SubscriptionController: API endpoints for subscription operations
  • SubscriptionService: Business logic and orchestration
  • AppstoreProvider: Apple App Store integration
  • RazorpaySubscriptionsProvider: Razorpay subscription management
  • AppstoreJwtService: App Store JWT token handling
  • SubscriptionDBService: Database operations

Subscription Endpoints

Admin Plan Management

EndpointMethodDescriptionAuth RequiredRoles
/apple-subscriptionsGETGet untracked App Store subscriptionsJWTADMIN
/create-subscription-plansPOSTCreate new subscription planJWTADMIN
/update-subscription-planPOSTUpdate existing planJWTADMIN
/disable-subscription-planPOSTEnable/disable planJWTADMIN
/delete-subscription-planPOSTDelete planJWTADMIN

Parent Subscription Operations

EndpointMethodDescriptionAuth RequiredRoles
/create-razorpay-subscriptionPOSTCreate Razorpay subscriptionJWTPARENT
/cancel-razorpay-subscriptionPOSTCancel subscriptionJWTPARENT
/get-update-subscription-dataPOSTGet upgrade/downgrade dataJWTPARENT
/update-razorpay-subscriptionPOSTUpgrade/downgrade subscriptionJWTPARENT
/confirm-razorpay-subscription-paymentPOSTConfirm subscription paymentJWTPARENT
/confirm-razorpay-upgrade-paymentPOSTConfirm upgrade paymentJWTPARENT

App Store Integration

EndpointMethodDescriptionAuth RequiredRoles
/app-store-confirm-paymentPOSTConfirm App Store purchaseJWTPARENT
/app-store-subscription-webhookPOSTHandle App Store webhooksNonePublic
/razorpay-subscription-webhookPOSTHandle Razorpay webhooksNonePublic

Core Features

1. Multi-Platform Subscription Support

  • iOS (App Store): Direct App Store Connect integration with JWT verification
  • Android (Razorpay): Comprehensive Razorpay subscription management
  • Web (Razorpay): Payment links and subscription portal
  • Cross-platform synchronization for subscription status

2. Intelligent Plan Management

// Automatic platform-specific plan creation
products.push(
{
product_id: monthlyRazorpayPlan.id,
platform: StorePlatform.ANDROID,
subscription_period: SubscriptionPeriod.MONTHLY,
price: monthly_price
},
{
product_id: yearlyRazorpayPlan.id,
platform: StorePlatform.ANDROID,
subscription_period: SubscriptionPeriod.YEARLY,
price: yearly_price
}
);

3. Advanced Subscription Operations

  • Upgrades with prorated billing: Immediate upgrades with calculated proration for international cards
  • Scheduled downgrades: Downgrade at cycle end to prevent immediate revenue loss for international cards
  • Automatic renewals: Seamless renewal processing with webhook automation

4. Subscription State Management

enum SubscriptionPaymentType {
INITIAL = 'INITIAL',
RENEWAL = 'RENEWAL',
UPGRADE = 'UPGRADE',
DOWNGRADE = 'DOWNGRADE'
}

enum SubscriptionPeriod {
MONTHLY = 'MONTHLY',
YEARLY = 'YEARLY'
}

5. Robust Webhook Processing

  • Signature verification for all webhook sources
  • Idempotent processing with Redis locking
  • Background job queuing for heavy operations
  • Comprehensive error handling with retry mechanisms

Multi-Platform Architecture

1. iOS App Store Integration

JWT-Based Verification:

const { notification, transactionInfo, renewalInfo } = 
await this.appstoreJwtService.decodeWebhookPayload({ signedPayload });

Supported Events:

  • SUBSCRIBED: New subscription or resubscription
  • DID_RENEW: Subscription renewal
  • DID_CHANGE_RENEWAL_PREF: Plan changes (upgrade/downgrade)
  • EXPIRED: Subscription expiration

Transaction Verification:

// Server-side verification with App Store
const transactionDetails = await this.appstoreProvider.getTransactionDetails(
transactionInfo.transactionId
);

// Bundle ID validation
if (transactionInfo.bundleId !== process.env.APP_STORE_BUNDLE_ID) {
throw new BadRequestException('Transaction not for this application');
}

2. Android Razorpay Integration

Subscription Creation:

const newSubscription = await this.razorpayProvider.createSubscription(
product.product_id,
{ child_id, user_id, organization }
);

Payment Verification:

const isValid = await this.razorpayProvider.validatePaymentVerification(
subscription_id, payment_id, signature
);

Advanced Features:

  • Proration calculations for mid-cycle upgrades
  • Cycle-end scheduling for downgrades
  • International card validation for plan updates
  • Automatic cancellation for unsupported payment methods

3. Platform-Specific Logic

// Platform-specific subscription handling
switch (platform) {
case StorePlatform.IOS:
// Handle App Store subscriptions
await this.handleNewSubscription(StorePlatform.IOS, ...params);
break;
case StorePlatform.ANDROID:
// Handle Razorpay subscriptions
await this.createSubscription(product_id, child_id, user_id);
break;
}

Subscription Lifecycle

1. Plan Creation Flow

// Multi-platform plan creation workflow
1. Create Razorpay plans (monthly/yearly)
2. Validate App Store product IDs
3. Create database subscription plan
4. Link features and products
5. Set plan status and pricing

2. Subscription Initiation

// Platform-specific subscription creation
1. Validate child eligibility
2. Check screening assessment completion
3. Create payment intent/subscription
4. Generate platform-specific response
5. Queue confirmation workflow

3. Payment Confirmation

// Multi-step confirmation process
1. Verify payment signature/transaction
2. Acquire Redis locks for concurrency
3. Validate subscription status
4. Update database records
5. Trigger feature activation
6. Send confirmation notifications

4. Subscription Management

// Ongoing subscription operations
1. Monitor renewal cycles
2. Handle plan changes (upgrade/downgrade)
3. Process payment failures
4. Manage subscription cancellations
5. Automatic free plan downgrades

Payment Integration

1. Razorpay Subscription Processing

Proration Calculation:

// Dynamic proration for upgrades
const daysRemaining = calculateDaysRemaining(subscriptionExpiryDate);
const amount = await this.razorpayProvider.calculateProratedAmount(
currentPlanId, newPlanId, daysRemaining
);

Payment Validation:

// Multi-retry payment confirmation
let attempt = 0;
while (attempt < this.retryCount) {
razorpaySubscription = await this.razorpayProvider.getSubscription(subscription_id);
if (['authenticated', 'active'].includes(razorpaySubscription.status)) {
break;
}
attempt++;
await sleep(Math.pow(2, attempt) * 1000);
}

2. App Store Transaction Processing

Concurrent Payment Handling:

// Redis-based locking for duplicate prevention
lockKey = `payment:sync:${transactionInfo.originalTransactionId}`;
acquireLock = await this.redisClient.set(lockKey, 'true', 'EX', 120, 'NX');

if (!acquireLock) {
return { status: 'processing', message: 'Payment being processed' };
}

Price Conversion:

// App Store price handling (prices in cents)
const price = transactionInfo.price / 1000;
const subscriptionExpiryDate = new Date(transactionInfo.expiresDate * 1000);

3. Upgrade/Downgrade Logic

Immediate Upgrades:

// Instant upgrade with prorated billing
if (updateType === 'upgrade') {
const paymentIntent = await this.razorpayProvider.createPaymentIntentForUpgradeProration(
subscription_id, amount * 100, newPlanId, childId, userId
);
orderId = paymentIntent.id;
}

Scheduled Downgrades:

// Downgrade at cycle end
if (updateType === 'downgrade') {
await this.razorpayProvider.updateSubscriptionCycleEnd(
subscription_id, newPlan.product_id
);
planEffectiveDate = new Date(subscription.subscription_expiry);
}

Webhook System

1. App Store Webhook Processing

JWT Payload Decoding:

const { notification, transactionInfo, renewalInfo } = 
await this.appstoreJwtService.decodeWebhookPayload({ signedPayload });

Event-Specific Handling:

switch (notification.notificationType) {
case AppStoreNotificationType.SUBSCRIBED:
await this.handleNewSubscription(...params);
break;
case AppStoreNotificationType.DID_RENEW:
await this.handleRenewal(...params);
break;
case AppStoreNotificationType.DID_CHANGE_RENEWAL_PREF:
await this.handleImmediateUpgrade(...params);
break;
case AppStoreNotificationType.EXPIRED:
await this.downgradeToFreePlan(transactionId);
break;
}

2. Razorpay Webhook Processing

Signature Verification:

const isValid = await this.razorpayProvider.validateWebhook(
JSON.stringify(body), signature
);

Background Job Queuing:

const jobId = `razorpay-subscription:${event_id}`;
this.backgroundService.addRazorpaySubscriptionWebhookJob(body, jobId);

Event Processing:

switch (body.event) {
case 'subscription.charged':
return await this.handleRazorpayRenewal(body);
case 'subscription.halted':
case 'subscription.completed':
return await this.handleRazorpaySubscriptionExpired(body);
}

3. Webhook Security Features

  • HMAC signature verification for Razorpay webhooks
  • JWT token validation for App Store webhooks
  • Redis-based deduplication using event IDs
  • Concurrent processing protection with distributed locks

Database Functions & Triggers

1. Child Deactivation Protection Trigger

Function: restrict_child_deactivation_if_has_active_subscription() Trigger: check_child_subscription_before_status_update

Purpose: Prevents deactivating children with active paid subscriptions

CREATE TRIGGER check_child_subscription_before_status_update
BEFORE UPDATE OF status ON child
FOR EACH ROW
EXECUTE FUNCTION restrict_child_deactivation_if_has_active_subscription();

Validation Logic:

IF NEW.status = 'INACTIVE' THEN
IF EXISTS (
SELECT 1
FROM child_subscription cs
JOIN subscription_products sp ON sp.id = cs.subscription_products_id
JOIN subscription_plans spp ON spp.id = sp.subscription_plan_id
WHERE cs.child_id = NEW.id
AND cs.in_use = TRUE
AND spp.subscription_plan_enum != 'FREE'
) THEN
RAISE EXCEPTION 'Child has an active subscription';
END IF;
END IF;

Protection Features:

  • Active subscription detection: Checks for non-free, in-use subscriptions
  • Data integrity: Prevents orphaned subscription records
  • Revenue protection: Ensures active subscribers remain accessible
  • Clear error messaging: Provides specific error for UI handling

2. Free Plan Assignment in Child Creation

Integration: Embedded in add_child() function

Auto-Subscription Logic:

-- Get free plan details
SELECT sp.id, spl.status
INTO v_free_plan_product_id, v_free_plan_status
FROM subscription_products sp
INNER JOIN subscription_plans spl ON spl.id = sp.subscription_plan_id
WHERE spl.subscription_plan_enum = 'FREE'
AND sp.platform = 'NONE'
AND sp.is_deleted = false;

-- Create child subscription with free plan
INSERT INTO child_subscription (
child_id,
subscription_products_id,
subscription_status,
in_use,
platform
) VALUES (
v_child_id,
v_free_plan_product_id,
'ACTIVE',
true,
'NONE'
);

Automatic Features:

  • Immediate activation: Every child gets free plan access instantly
  • Platform neutrality: Free plan works across all platforms
  • Validation: Ensures free plan exists and is active
  • Seamless onboarding: No additional subscription steps required

3. Feature-Based Validation

Lesson Plan Assignment Validation:

-- Check subscription features for lesson plan assignment
SELECT EXISTS(
SELECT 1 FROM child_subscription cs
JOIN subscription_products sp ON sp.id = cs.subscription_products_id
JOIN subscription_plans spp ON spp.id = sp.subscription_plan_id
JOIN subscription_plan_features spf ON spf.subscription_plan_id = spp.id
JOIN subscription_features sf ON sf.id = spf.subscription_feature_id
WHERE cs.child_id = input_child_id
AND sf.feature_name = 'lesson_plan_assignment'
AND cs.in_use = true
) INTO v_assessment_assignment_feature;

IF NOT v_assessment_assignment_feature THEN
RAISE EXCEPTION 'Child subscription does not have "assessment_assignment" feature';
END IF;

4. Update Triggers

Standard timestamp triggers:

-- Child subscription updates
CREATE TRIGGER set_public_child_subscription_updated_at
BEFORE UPDATE ON child_subscription
EXECUTE FUNCTION set_updated_at_column();

-- Subscription plan updates
CREATE TRIGGER set_public_subscription_plans_updated_at
BEFORE UPDATE ON subscription_plans
EXECUTE FUNCTION set_updated_at_column();

-- Subscription features updates
CREATE TRIGGER set_public_subscription_features_updated_at
BEFORE UPDATE ON subscription_features
EXECUTE FUNCTION set_updated_at_column();

Plan Management

1. Multi-Platform Plan Creation

Validation Workflow:

// Price validation
if (monthly_price >= yearly_price) {
throw new BadRequestException('Monthly price should be less than yearly price');
}

// Feature validation
const featuresCount = await this.subscriptionDBService.getFeaturesCount(features_ids);
if (featuresCount !== features_ids.length) {
throw new BadRequestException('Invalid features IDs');
}

Platform-Specific Creation:

// Create Razorpay plans for Android/Web
const [monthlyPlan, yearlyPlan] = await Promise.allSettled([
this.razorpayProvider.createSubscriptionPlan(monthlyParams),
this.razorpayProvider.createSubscriptionPlan(yearlyParams)
]);

// Validate App Store products for iOS
const validAppStoreProducts = untrackedSubscriptions.filter(
subscription => app_store_product_ids.includes(subscription.product_id)
);

2. Plan Updates with Price Changes

Background Job Scheduling:

// Schedule price updates for existing subscriptions
for (const price of updateSubscriptionPrice) {
this.backgroundService.addScheduleSubscriptionPriceUpdateJob({
new_plan_id: price.plan_id
});
}

Update Strategy:

  • International cards: Immediate update with prorated billing
  • Domestic cards: Notification and cancellation
  • Within 7 days of renewal: Post-renewal update
  • Beyond 7 days: Immediate update

3. Plan Lifecycle Management

Enable/Disable Logic:

// Cannot disable free plan
if (existingPlan.subscription_plan_enum === 'FREE') {
throw new BadRequestException('Cannot enable/disable free plan');
}

Deletion Validation:

// Check for active users
const subscriptionPlanUser = await this.subscriptionDBService.subscriptionPlanUser(plan_id);
if (subscriptionPlanUser > 0) {
throw new BadRequestException('Plan is in use');
}

Child Subscription Management

1. Subscription Creation Flow

Eligibility Validation:

// Screening assessment requirement
const screeningAssessmentResponses = await this.subscriptionDBService
.getScreeningAssessmentResponses(child_id);

if (screeningAssessmentResponses.length === 0) {
throw new BadRequestException('Please complete screening assessment');
}

Conflict Detection:

// Prevent multiple paid subscriptions
if (childSubscription.subscription_products.subscription_plans.subscription_plan_enum !== 'FREE') {
throw new BadRequestException('Already subscribed to a different plan');
}

2. Subscription Upgrades/Downgrades

Upgrade with Proration:

// Calculate prorated amount for immediate upgrade
const subscriptionExpiryDate = childSubscription.subscription_expiry;
const daysRemaining = calculateDaysRemaining(subscriptionExpiryDate);
const amount = await this.razorpayProvider.calculateProratedAmount(
currentPlanId, newPlanId, daysRemaining
);

// Create payment intent for upgrade
const paymentIntent = await this.razorpayProvider.createPaymentIntentForUpgradeProration(
subscription_id, amount * 100, newPlanId, childId, userId
);

Downgrade Scheduling:

// Schedule downgrade at cycle end
await this.razorpayProvider.updateSubscriptionCycleEnd(subscription_id, newPlan.product_id);

// Create change request record
await this.subscriptionDBService.upsertSubscriptionChangeRequest(
requestId, subscription_id, newPlan.id, oldProductId
);

3. Subscription Status Management

Payment Failure Handling:

// Record failed payment attempts
if (body.payload?.payment?.entity?.status === 'failed') {
await this.subscriptionDBService.subscriptionPaymentFailed(
childSubscription.id, amountPaid, paymentMethod, paymentType, tax, platformFee, baseAmount
);
}

Automatic Downgrades:

// Downgrade to free plan on expiration
await this.subscriptionDBService.subscribeToFreePlanBySubscriptionId(
subscription.id, freePlan.id
);

Background Job Processing

1. Price Update Jobs

Immediate Price Updates:

this.backgroundService.addUpdateSubscriptionPriceJob({
subscription_id: subscription.provider_subscription_id,
new_plan_id: newPlanId,
user_id: subscription.child.parent.user_id,
child_name: subscription.child.name
});

Post-Renewal Updates:

this.backgroundService.addUpdateSubscriptionPriceAfterRenewalJob({
subscription_id, new_plan_id, user_id, child_name
});

Cancellation Due to Price Changes:

this.backgroundService.addCancelSubscriptionDueToPriceUpdateJob({
child_name, child_subscription_id, razorpay_subscription_id, user_id
});

2. Notification Jobs

Renewal Reminders:

// 3-day renewal notifications
async subscriptionPlanRenewalNotification() {
const subscriptions = await this.subscriptionDBService
.getActiveSubscriptionsExpiringInExactly3Days();

this.backgroundService.addSendCustomNotificationJob({
subject: 'Plan renewal in 3 days',
user_ids: subscriptions.map(s => s.child.parent.user_id)
});
}

Status Change Notifications:

// Upgrade confirmation notifications
this.backgroundService.addSendCustomNotificationJob({
subject: 'Subscription Updated',
description: 'Your subscription has been upgraded successfully',
user_ids: [parentId]
});

3. Webhook Job Processing

Asynchronous Webhook Handling:

// Queue webhook processing to avoid blocking
const jobId = `razorpay-subscription:${event_id}`;
this.backgroundService.addRazorpaySubscriptionWebhookJob(body, jobId);

Data Models

Core Subscription Plan Model

interface SubscriptionPlan {
id: string;
name: string;
description: string;
subscription_plan_enum: 'FREE' | 'BASIC' | 'PREMIUM';
status: 'ACTIVE' | 'INACTIVE';
is_deleted: boolean;
subscription_plan_features: SubscriptionPlanFeature[];
subscription_products: SubscriptionProduct[];
}

Subscription Product Model

interface SubscriptionProduct {
id: string;
product_id: string;
platform: StorePlatform;
billing_frequency: SubscriptionPeriod;
price: number;
is_deleted: boolean;
subscription_plan_id: string;
}

Child Subscription Model

interface ChildSubscription {
id: string;
child_id: string;
subscription_products_id: string;
provider_subscription_id?: string;
transaction_id?: string;
subscription_start: Date;
subscription_expiry: Date;
subscription_status: 'ACTIVE' | 'INACTIVE' | 'CANCELLED';
platform: StorePlatform;
in_use: boolean;
is_cancelled: boolean;
payment_method?: string;
}

Subscription Payment Model

interface ChildSubscriptionPayment {
id: string;
child_subscription_id: string;
amount: number;
tax: number;
platform_fee: number;
payment_type: SubscriptionPaymentType;
payment_method: string;
created_at: Date;
}

Security & Validation

1. Multi-Platform Authentication

// App Store JWT verification
const transactionInfo = await this.appstoreJwtService.decodeTransactionInfo(signedTransactionInfo);

// Razorpay signature validation
const isValid = await this.razorpayProvider.validatePaymentVerification(
subscription_id, payment_id, signature
);

2. Concurrency Control

// Redis-based locking for subscription operations
lockKey = `subscription:confirm:${subscription_id}`;
const lockAcquired = await this.redisClient.set(lockKey, 'true', 'EX', 120, 'NX');

if (!lockAcquired) {
return { status: 'processing', message: 'Operation in progress' };
}

3. Input Validation & Authorization

  • DTO validation using class-validator decorators
  • Parent-child relationship verification for all operations
  • Subscription eligibility checks before plan changes
  • Platform compatibility validation for operations
  • Payment method validation for plan updates

4. Error Handling & Resilience

  • Retry mechanisms for payment confirmations
  • Graceful degradation for third-party service failures
  • Comprehensive logging for debugging and audit
  • Transaction rollback on validation failures
  • Background job processing for heavy operations

Key Implementation Details

Price Validation

// Ensure yearly plans are more cost-effective
if (monthly_price >= yearly_price) {
throw new BadRequestException('Monthly price should be less than yearly price');
}

Platform Detection

// Automatic platform assignment based on payment provider
const platform = product_id.startsWith('rzp_') ? StorePlatform.ANDROID : StorePlatform.IOS;

Proration Calculation

// Dynamic proration for mid-cycle upgrades
const daysRemaining = calculateDaysRemaining(subscription_expiry);
const proratedAmount = (newPlanPrice - currentPlanPrice) * (daysRemaining / totalDays);

Free Plan Management

// Automatic free plan assignment on child creation
const freePlan = await this.getFreePlanDetails();
await this.createChildSubscription(childId, freePlan.id, 'NONE');

The Subscription module provides a comprehensive subscription management system with multi-platform support, advanced payment processing, automated subscription workflows, and robust error handling, enabling seamless subscription experiences across iOS, Android, and web platforms for the Comdeall ecosystem.


Conclusion

The Subscription module serves as the financial and access control backbone of the Comdeall platform, providing sophisticated subscription management with multi-platform integration, automated billing workflows, comprehensive webhook processing, and intelligent plan management. Through the integration of App Store Connect, Razorpay, database triggers, and background job processing, it delivers a complete subscription ecosystem that supports the diverse needs of parents, children, and administrators across all supported platforms.