feat: add Binance Pay webhook handler and implement payment order creation in billing service

This commit is contained in:
Hamza-Ayed
2026-07-14 21:46:48 +03:00
parent cc62019609
commit 790bfcefc8
29 changed files with 6366 additions and 122 deletions
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
@@ -185,6 +185,78 @@ export class BillingController {
return { status: 'success' };
}
/**
* Binance Pay Webhook
* Called by Binance when a payment status changes
*/
@Post('webhooks/binance')
@ApiOperation({ summary: 'Binance Payment Webhook' })
async handleBinanceWebhook(
@Body() body: any,
@Req() req: any
) {
const timestamp = req.headers['binancepay-timestamp'] as string;
const nonce = req.headers['binancepay-nonce'] as string;
const signature = req.headers['binancepay-signature'] as string;
this.logger.log(`📥 Binance Webhook Received: Transaction ID ${body.bizId || 'UNKNOWN'}`);
if (!timestamp || !nonce || !signature) {
this.logger.error('❌ Binance Webhook: Missing required headers');
throw new BadRequestException('Missing headers');
}
// 1. Verify Signature
if (!this.binanceProvider.verifySignature(timestamp, nonce, signature, body)) {
this.logger.error('❌ Binance Signature Verification Failed');
throw new BadRequestException('Invalid signature');
}
// 2. Process Payment
if (body.bizStatus === 'PAY_SUCCESS') {
try {
let dataObj: any = {};
if (body.data) {
dataObj = JSON.parse(body.data);
}
let tenantId = '';
let plan = 'PRO';
if (dataObj.passThroughInfo) {
const passThrough = JSON.parse(dataObj.passThroughInfo);
tenantId = passThrough.tenantId;
plan = passThrough.plan;
}
if (!tenantId) {
this.logger.error(`❌ Binance Webhook failed: No tenantId found. Body: ${JSON.stringify(body)}`);
return { returnCode: 'FAIL', returnMessage: 'No tenantId found' };
}
const externalTxId = dataObj.merchantTradeNo || body.bizId?.toString();
await this.billingService.processSuccessfulPayment(
externalTxId,
PaymentProvider.BINANCE,
0,
{ tenantId, plan }
);
this.logger.log(`✅ Binance Payment Processed Successfully for Tenant ${tenantId}`);
} catch (err: any) {
this.logger.error(`❌ Failed to process Binance payment: ${err.message}`);
return { returnCode: 'FAIL', returnMessage: err.message };
}
}
return {
returnCode: 'SUCCESS',
returnMessage: null,
};
}
@ApiBearerAuth()
@UseGuards(FirebaseAuthGuard)
@Get('invoices')
@@ -1,33 +1,115 @@
import { Injectable, Logger } from '@nestjs/common';
import { Injectable, Logger, InternalServerErrorException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import axios from 'axios';
import * as crypto from 'crypto';
@Injectable()
export class BinanceProvider {
private readonly logger = new Logger(BinanceProvider.name);
private readonly apiUrl = 'https://bpay.binanceapi.com/binancepay/openapi/v2/order';
constructor(private configService: ConfigService) {}
/**
* Mock Binance Pay Order Creation
* Will lead to a success/fail redirect for testing
*/
async createOrder(tenantId: string, amount: number, plan: string) {
this.logger.log(`[Mock] Creating Binance Pay order for ${tenantId} - ${plan}`);
// In production, this calls https://bpay.binanceapi.com/binancepay/openapi/v2/order
// and returns a checkoutUrl.
return {
checkoutUrl: `https://map-dashbord.intaleqapp.com/api/billing/mock-binance-success?tenantId=${tenantId}&plan=${plan}`,
prepayId: `mock_${Date.now()}`
};
private get apiKey(): string {
return this.configService.get<string>('BINANCE_PAY_API_KEY');
}
private get secretKey(): string {
return this.configService.get<string>('BINANCE_PAY_SECRET_KEY');
}
/**
* Mock Webhook Signature Verification
* Create the Binance Pay Signature
*/
verifySignature(payload: any, signature: string): boolean {
// In production, uses HMAC-SHA512 with Binance Secret Key
return true;
private generateSignature(timestamp: string, nonce: string, body: any): string {
const payload = timestamp + '\n' + nonce + '\n' + JSON.stringify(body) + '\n';
return crypto
.createHmac('sha512', this.secretKey)
.update(payload)
.digest('hex')
.toUpperCase();
}
/**
* Create a Binance Pay Order
*/
async createOrder(tenantId: string, amount: number, plan: string) {
this.logger.log(`Creating Binance Pay order for ${tenantId} - ${plan}`);
if (!this.apiKey || !this.secretKey) {
this.logger.error('Binance Pay API keys are not configured');
throw new InternalServerErrorException('Payment provider is not configured properly');
}
const nonce = crypto.randomBytes(16).toString('hex');
const timestamp = Date.now().toString();
const merchantTradeNo = `txn_${Date.now()}_${Math.floor(Math.random() * 10000)}`;
const body = {
env: {
terminalType: 'WEB',
},
merchantTradeNo: merchantTradeNo,
orderAmount: amount,
currency: 'USDT',
goods: {
goodsType: '01',
goodsCategory: 'Z000',
referenceGoodsId: plan,
goodsName: `${plan} Plan Subscription`,
goodsDetail: `Subscription to ${plan} plan for Maps SaaS`,
},
passThroughInfo: JSON.stringify({ tenantId, plan }),
returnUrl: `https://map-dashboard.intaleqapp.com/dashboard.html#billing`,
};
const signature = this.generateSignature(timestamp, nonce, body);
try {
const response = await axios.post(this.apiUrl, body, {
headers: {
'Content-Type': 'application/json',
'BinancePay-Timestamp': timestamp,
'BinancePay-Nonce': nonce,
'BinancePay-Certificate-SN': this.apiKey,
'BinancePay-Signature': signature,
},
});
if (response.data && response.data.status === 'SUCCESS') {
return {
checkoutUrl: response.data.data.checkoutUrl,
orderId: merchantTradeNo,
prepayId: response.data.data.prepayId,
};
} else {
this.logger.error(`Binance API Error: ${JSON.stringify(response.data)}`);
throw new InternalServerErrorException('Failed to create Binance order');
}
} catch (error: any) {
this.logger.error(`Error communicating with Binance API: ${error.message}`);
throw new InternalServerErrorException('Failed to communicate with payment provider');
}
}
/**
* Verify Webhook Signature
*/
verifySignature(timestamp: string, nonce: string, signature: string, payloadBody: any): boolean {
const payload = timestamp + '\n' + nonce + '\n' + JSON.stringify(payloadBody) + '\n';
const expectedSignature = crypto
.createHmac('sha512', this.secretKey)
.update(payload)
.digest('hex')
.toUpperCase();
try {
return crypto.timingSafeEqual(
Buffer.from(signature || ''),
Buffer.from(expectedSignature)
);
} catch (e) {
return false;
}
}
}