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 Redirection Callback (Success/Fail) * This handles the GET request when PayMob redirects the user back to our site */ @Get('callback/paymob') @ApiOperation({ summary: 'PayMob Redirection Handler' }) async handlePaymobCallback(@Query() query: any, @Req() req: any) { this.logger.log(`🔄 PayMob Redirect Callback: ID ${query.id}, Success: ${query.success}`); // Fallback: If success=true, proactively fetch transaction details and upgrade if (query.success === 'true' && query.id) { try { const txDetails = await this.paymobProvider.getTransactionDetails(query.id); if (txDetails && txDetails.success === true) { const extraDesc = txDetails.order?.shipping_data?.extra_description || ""; const [tenantId, plan] = extraDesc.split('|'); if (tenantId) { await this.billingService.processSuccessfulPayment( query.id.toString(), PaymentProvider.PAYMOB, txDetails.amount_cents / 100, { tenantId, plan: plan || 'PRO' } ); this.logger.log(`🚀 Immediate Activation triggered via Callback for Tenant ${tenantId}`); } } } catch (e) { this.logger.error(`Failed during immediate activation fallback: ${e.message}`); } } // Redirect back to dashboard with status const targetUrl = `https://map-dashbord.intaleqapp.com/dashboard.html#billing?payment_status=${query.success === 'true' ? 'success' : 'failed'}&id=${query.id}`; return ` Intaleq Maps | تمت العملية بنجاح

شكراً لك، حمزة!

تم تفعيل خطة PRO بنجاح. يتم الآن توجيهك إلى لوحة التحكم...

`; } /** * 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); } }