2026-04-15-4

This commit is contained in:
Hamza-Ayed
2026-04-15 19:56:49 +03:00
parent 9cd1ac4c1d
commit 3d61362602
54 changed files with 12659 additions and 436 deletions
+101
View File
@@ -0,0 +1,101 @@
import { Controller, Get, Post, Body, Query, UseGuards, Req, Logger, BadRequestException } from '@nestjs/common';
import { BillingService } from './billing.service';
import { PayMobProvider } from './providers/paymob.provider';
import { BinanceProvider } from './providers/binance.provider';
import { FirebaseAuthGuard } from '../auth/guards/firebase-auth.guard';
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
import { PaymentProvider, PaymentStatus } from './entities/transaction.entity';
@ApiTags('billing')
@Controller('billing')
export class BillingController {
private readonly logger = new Logger(BillingController.name);
constructor(
private billingService: BillingService,
private paymobProvider: PayMobProvider,
private binanceProvider: BinanceProvider,
) {}
@ApiBearerAuth()
@UseGuards(FirebaseAuthGuard)
@Get('subscription')
@ApiOperation({ summary: 'Get current subscription details' })
async getSubscription(@Req() req: any) {
return this.billingService.getSubscription(req.tenant.id);
}
@ApiBearerAuth()
@UseGuards(FirebaseAuthGuard)
@Post('checkout')
@ApiOperation({ summary: 'Initialize a checkout session' })
async checkout(@Req() req: any, @Body() body: { plan: string; provider: PaymentProvider }) {
const tenantId = req.tenant.id;
const plan = body.plan;
const amount = plan === 'PRO' ? 40 : 0; // Price logic
if (body.provider === PaymentProvider.PAYMOB) {
const { paymentKey, orderId } = await this.paymobProvider.createPaymentKey(tenantId, amount, plan);
// Iframe URL (Using the new provided Iframe ID)
const iframeId = this.billingService.getIframeId();
const checkoutUrl = `https://accept.paymob.com/api/acceptance/iframes/${iframeId}?payment_token=${paymentKey}`;
return { checkoutUrl, orderId };
}
if (body.provider === PaymentProvider.BINANCE) {
return this.binanceProvider.createOrder(tenantId, amount, plan);
}
throw new BadRequestException('Invalid payment provider');
}
/**
* PayMob Transaction Processed Webhook
* This is called by PayMob when a transaction is attempted
*/
@Post('webhooks/paymob')
@ApiOperation({ summary: 'PayMob Payment Webhook' })
async handlePaymobWebhook(@Body() body: any, @Query('hmac') hmac: string) {
this.logger.log(`📥 PayMob Webhook Received: Transaction ID ${body.obj?.id}`);
// 1. Verify HMAC
if (!this.paymobProvider.verifyHmac(body.obj, hmac)) {
this.logger.error('❌ PayMob HMAC Verification Failed');
throw new BadRequestException('Invalid signature');
}
// 2. Process payment if successful
if (body.obj.success === true) {
// Extract metadata from extra_description (format: tenantId|plan)
const extraDesc = body.obj.order?.shipping_data?.extra_description || "";
const [tenantId, plan] = extraDesc.split('|');
if (!tenantId) {
this.logger.error(`❌ PayMob Webhook failed: No tenantId in metadata. Raw: ${extraDesc}`);
return { status: 'error', message: 'No tenantId found' };
}
await this.billingService.processSuccessfulPayment(
body.obj.id.toString(),
PaymentProvider.PAYMOB,
body.obj.amount_cents / 100,
{
tenantId,
plan: plan || 'PRO'
}
);
}
return { status: 'success' };
}
@ApiBearerAuth()
@UseGuards(FirebaseAuthGuard)
@Get('invoices')
@ApiOperation({ summary: 'Get payment history' })
async getInvoices(@Req() req: any) {
return this.billingService.getTransactionHistory(req.tenant.id);
}
}