144 lines
4.5 KiB
TypeScript
144 lines
4.5 KiB
TypeScript
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: 5000,
|
|
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]: 5000,
|
|
[TenantPlan.STARTER]: 25000,
|
|
[TenantPlan.PRO]: 100000,
|
|
[TenantPlan.ENTERPRISE]: 500000,
|
|
};
|
|
|
|
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' },
|
|
});
|
|
}
|
|
}
|