2026-04-15-4
This commit is contained in:
@@ -0,0 +1,142 @@
|
||||
import { Injectable, Logger, BadRequestException, NotFoundException } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { Subscription, SubscriptionStatus } from './entities/subscription.entity';
|
||||
import { Transaction, PaymentStatus, PaymentProvider } from './entities/transaction.entity';
|
||||
import { Tenant, TenantPlan } from '../auth/entities/tenant.entity';
|
||||
import { MailService } from '../common/mail.service';
|
||||
|
||||
@Injectable()
|
||||
export class BillingService {
|
||||
private readonly logger = new Logger(BillingService.name);
|
||||
|
||||
constructor(
|
||||
@InjectRepository(Subscription)
|
||||
private subscriptionRepository: Repository<Subscription>,
|
||||
@InjectRepository(Transaction)
|
||||
private transactionRepository: Repository<Transaction>,
|
||||
@InjectRepository(Tenant)
|
||||
private tenantRepository: Repository<Tenant>,
|
||||
private configService: ConfigService,
|
||||
private mailService: MailService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Get configured PayMob Iframe ID
|
||||
*/
|
||||
getIframeId(): string {
|
||||
return this.configService.get('PAYMOB_IFRAME_ID', '837992');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current subscription for a tenant
|
||||
*/
|
||||
async getSubscription(tenantId: string): Promise<Subscription> {
|
||||
let sub = await this.subscriptionRepository.findOne({ where: { tenantId } });
|
||||
|
||||
// Auto-initialize if not exists
|
||||
if (!sub) {
|
||||
sub = await this.subscriptionRepository.save({
|
||||
tenantId,
|
||||
plan: 'FREE',
|
||||
monthlyRequestLimit: 8000,
|
||||
status: SubscriptionStatus.ACTIVE,
|
||||
});
|
||||
}
|
||||
|
||||
return sub;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle successful payment from any provider
|
||||
*/
|
||||
async processSuccessfulPayment(
|
||||
externalTxId: string,
|
||||
provider: PaymentProvider,
|
||||
amount: number,
|
||||
metadata: any
|
||||
) {
|
||||
// 1. Find or create transaction
|
||||
let txn = await this.transactionRepository.findOne({ where: { externalTransactionId: externalTxId } });
|
||||
|
||||
if (txn && txn.status === PaymentStatus.SUCCESS) {
|
||||
this.logger.warn(`Transaction ${externalTxId} already processed.`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!txn) {
|
||||
// This might happen if webhook arrives before front-end redirect
|
||||
// Logic to resolve tenantId from metadata should be here
|
||||
const tenantId = metadata.tenantId;
|
||||
if (!tenantId) throw new BadRequestException('No tenantId found in payment metadata');
|
||||
|
||||
txn = await this.transactionRepository.save({
|
||||
tenantId,
|
||||
externalTransactionId: externalTxId,
|
||||
amount,
|
||||
provider,
|
||||
status: PaymentStatus.SUCCESS,
|
||||
metadata,
|
||||
});
|
||||
} else {
|
||||
txn.status = PaymentStatus.SUCCESS;
|
||||
txn.metadata = { ...txn.metadata, ...metadata };
|
||||
await this.transactionRepository.save(txn);
|
||||
}
|
||||
|
||||
// 2. Upgrade the subscription and tenant plan
|
||||
const tenantId = txn.tenantId;
|
||||
const plan = metadata.plan || 'PRO'; // Default to PRO for paid txns
|
||||
|
||||
await this.upgradeTenantPlan(tenantId, plan as TenantPlan);
|
||||
|
||||
// 3. Send Invoice Email
|
||||
try {
|
||||
const tenant = await this.tenantRepository.findOne({ where: { id: tenantId } });
|
||||
if (tenant && tenant.email) {
|
||||
await this.mailService.sendInvoiceEmail(tenant.email, {
|
||||
tenantName: tenant.name,
|
||||
plan: plan,
|
||||
amount: `${amount} EGP`,
|
||||
transactionId: externalTxId,
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
this.logger.error(`Failed to send invoice email for tx ${externalTxId}: ${err.message}`);
|
||||
}
|
||||
|
||||
this.logger.log(`✅ Tenant ${tenantId} upgraded to ${plan} after payment ${externalTxId}`);
|
||||
}
|
||||
|
||||
private async upgradeTenantPlan(tenantId: string, plan: TenantPlan) {
|
||||
// Update Tenant
|
||||
await this.tenantRepository.update(tenantId, { plan });
|
||||
|
||||
// Update Subscription
|
||||
const limits = {
|
||||
[TenantPlan.FREE]: 8000,
|
||||
[TenantPlan.PRO]: 50000,
|
||||
[TenantPlan.ENTERPRISE]: 1000000,
|
||||
};
|
||||
|
||||
const sub = await this.getSubscription(tenantId);
|
||||
sub.plan = plan;
|
||||
sub.monthlyRequestLimit = limits[plan] || 8000;
|
||||
sub.status = SubscriptionStatus.ACTIVE;
|
||||
sub.currentPeriodStart = new Date();
|
||||
|
||||
const nextMonth = new Date();
|
||||
nextMonth.setMonth(nextMonth.getMonth() + 1);
|
||||
sub.currentPeriodEnd = nextMonth;
|
||||
|
||||
await this.subscriptionRepository.save(sub);
|
||||
}
|
||||
|
||||
async getTransactionHistory(tenantId: string): Promise<Transaction[]> {
|
||||
return this.transactionRepository.find({
|
||||
where: { tenantId },
|
||||
order: { createdAt: 'DESC' },
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user