import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository, FindManyOptions } from 'typeorm'; import { SubscriptionPlan } from '../entities/subscription-plan.entity'; import { UserSubscription } from '../entities/user-subscription.entity'; import { SubscriptionHistory } from '../entities/subscription-history.entity'; import { PaymentRecord } from '../entities/payment-record.entity'; export interface SubscriptionQuery { page?: number; limit?: number; status?: string; planId?: string; userId?: string; } export interface PlanStats { planId: string; planName: string; totalSubscriptions: number; activeSubscriptions: number; revenue: number; popularityRank: number; } @Injectable() export class SubscriptionService { constructor( @InjectRepository(SubscriptionPlan) private subscriptionPlanRepository: Repository, @InjectRepository(UserSubscription) private userSubscriptionRepository: Repository, @InjectRepository(SubscriptionHistory) private subscriptionHistoryRepository: Repository, @InjectRepository(PaymentRecord) private paymentRecordRepository: Repository, ) {} // Subscription Plans async getAllPlans() { const plans = await this.subscriptionPlanRepository.find({ where: { isActive: true }, order: { price: 'ASC', isPopular: 'DESC', }, }); return plans; } async getPlan(id: string) { const plan = await this.subscriptionPlanRepository.findOne({ where: { id }, }); if (!plan) { throw new NotFoundException(`Subscription plan with ID ${id} not found`); } return plan; } async getPlanByPlanId(planId: string) { const plan = await this.subscriptionPlanRepository.findOne({ where: { planId }, }); if (!plan) { throw new NotFoundException(`Subscription plan with plan ID ${planId} not found`); } return plan; } // User Subscriptions async getUserSubscriptions(query: SubscriptionQuery = {}) { const { page = 1, limit = 10, status, planId, userId } = query; const skip = (page - 1) * limit; const queryBuilder = this.userSubscriptionRepository .createQueryBuilder('subscription') .leftJoinAndSelect('subscription.subscriptionPlan', 'plan') .leftJoinAndSelect('subscription.userProfile', 'user') .orderBy('subscription.createdAt', 'DESC') .skip(skip) .take(limit); if (status) { queryBuilder.andWhere('subscription.status = :status', { status }); } if (planId) { queryBuilder.andWhere('plan.planId = :planId', { planId }); } if (userId) { queryBuilder.andWhere('subscription.userProfileId = :userId', { userId }); } const [subscriptions, total] = await queryBuilder.getManyAndCount(); return { subscriptions, pagination: { page, limit, total, totalPages: Math.ceil(total / limit), }, }; } async getUserSubscription(userId: string) { const subscription = await this.userSubscriptionRepository .createQueryBuilder('subscription') .leftJoinAndSelect('subscription.subscriptionPlan', 'plan') .leftJoinAndSelect('subscription.userProfile', 'user') .where('subscription.userProfileId = :userId', { userId }) .andWhere('subscription.status IN (:...statuses)', { statuses: ['active', 'trialing'] }) .getOne(); return subscription; } async createSubscription(userId: string, planId: string, subscriptionData: Partial) { const plan = await this.getPlanByPlanId(planId); // Check if user already has an active subscription const existingSubscription = await this.getUserSubscription(userId); if (existingSubscription) { throw new BadRequestException('User already has an active subscription'); } const subscription = this.userSubscriptionRepository.create({ userProfileId: userId, subscriptionPlanId: plan.id, status: 'active', currentPeriodStart: new Date(), currentPeriodEnd: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000), // 30 days ...subscriptionData, }); const savedSubscription = await this.userSubscriptionRepository.save(subscription); // Create subscription history record await this.createSubscriptionHistory(savedSubscription.id, 'created', null, 'active', { planId: plan.planId, createdBy: 'system', }); return savedSubscription; } async updateSubscriptionStatus(subscriptionId: string, status: string, metadata?: any) { const subscription = await this.userSubscriptionRepository.findOne({ where: { id: subscriptionId }, }); if (!subscription) { throw new NotFoundException(`Subscription with ID ${subscriptionId} not found`); } const oldStatus = subscription.status; subscription.status = status as any; subscription.updatedAt = new Date(); if (status === 'canceled') { subscription.canceledAt = new Date(); } const updatedSubscription = await this.userSubscriptionRepository.save(subscription); // Create history record await this.createSubscriptionHistory(subscriptionId, 'updated', oldStatus, status, metadata); return updatedSubscription; } // Subscription History async createSubscriptionHistory( subscriptionId: string, action: string, oldStatus: string | null, newStatus: string | null, metadata?: any, ) { const history = new SubscriptionHistory(); history.userSubscriptionId = subscriptionId; history.action = action as any; history.oldStatus = oldStatus || undefined; history.newStatus = newStatus || undefined; history.metadata = metadata; return await this.subscriptionHistoryRepository.save(history); } async getSubscriptionHistory(subscriptionId: string) { return await this.subscriptionHistoryRepository.find({ where: { userSubscriptionId: subscriptionId }, order: { createdAt: 'DESC' }, }); } // Payment Records async createPaymentRecord(subscriptionId: string, paymentData: Partial) { const payment = this.paymentRecordRepository.create({ userSubscriptionId: subscriptionId, ...paymentData, }); return await this.paymentRecordRepository.save(payment); } async getPaymentRecords(subscriptionId: string) { return await this.paymentRecordRepository.find({ where: { userSubscriptionId: subscriptionId }, order: { createdAt: 'DESC' }, }); } // Statistics and Analytics async getSubscriptionStats() { const totalPlans = await this.subscriptionPlanRepository.count({ where: { isActive: true } }); const totalSubscriptions = await this.userSubscriptionRepository.count(); const activeSubscriptions = await this.userSubscriptionRepository.count({ where: { status: 'active' }, }); const trialSubscriptions = await this.userSubscriptionRepository.count({ where: { status: 'trialing' }, }); const canceledSubscriptions = await this.userSubscriptionRepository.count({ where: { status: 'canceled' }, }); // Calculate total revenue from successful payments const revenueResult = await this.paymentRecordRepository .createQueryBuilder('payment') .select('SUM(payment.amount)', 'total') .where('payment.status = :status', { status: 'succeeded' }) .getRawOne(); const totalRevenue = parseFloat(revenueResult?.total || '0'); // Calculate monthly recurring revenue (MRR) const mrrResult = await this.userSubscriptionRepository .createQueryBuilder('subscription') .leftJoin('subscription.subscriptionPlan', 'plan') .select('SUM(plan.price)', 'mrr') .where('subscription.status IN (:...statuses)', { statuses: ['active', 'trialing'] }) .andWhere('plan.interval = :interval', { interval: 'month' }) .getRawOne(); const monthlyRecurringRevenue = parseFloat(mrrResult?.mrr || '0'); return { totalPlans, totalSubscriptions, activeSubscriptions, trialSubscriptions, canceledSubscriptions, totalRevenue, monthlyRecurringRevenue, }; } async getPlanStats(): Promise { const plans = await this.subscriptionPlanRepository .createQueryBuilder('plan') .leftJoin('plan.userSubscriptions', 'subscription') .leftJoin('subscription.paymentRecords', 'payment', 'payment.status = :paymentStatus', { paymentStatus: 'succeeded' }) .select([ 'plan.planId as planId', 'plan.name as planName', 'COUNT(subscription.id) as totalSubscriptions', 'COUNT(CASE WHEN subscription.status = \'active\' THEN 1 END) as activeSubscriptions', 'COALESCE(SUM(payment.amount), 0) as revenue', ]) .where('plan.isActive = :active', { active: true }) .groupBy('plan.id, plan.planId, plan.name') .orderBy('totalSubscriptions', 'DESC') .getRawMany(); return plans.map((plan, index) => ({ planId: plan.planid, planName: plan.planname, totalSubscriptions: parseInt(plan.totalsubscriptions), activeSubscriptions: parseInt(plan.activesubscriptions), revenue: parseFloat(plan.revenue), popularityRank: index + 1, })); } }