feat: add Binance Pay webhook handler and implement payment order creation in billing service
This commit is contained in:
Vendored
BIN
Binary file not shown.
Vendored
BIN
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user