import { Injectable, Logger } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import * as crypto from 'crypto'; import { PaymentProviderPort, PaymentProviderType, PaymentSessionRequest, PaymentSession, WebhookResult, } from '../../../domain/ports/payment-provider.port'; import { InvoiceCurrency } from '../../../domain/entities/invoice.entity'; @Injectable() export class WeChatPayProvider implements PaymentProviderPort { readonly providerType = PaymentProviderType.WECHAT_PAY; private readonly logger = new Logger(WeChatPayProvider.name); private mchId: string; private apiKeyV3: string; private appId: string; private configured = false; // WeChat Pay v3 API base private static readonly BASE_URL = 'https://api.mch.weixin.qq.com'; constructor(private readonly configService: ConfigService) { this.mchId = this.configService.get('WECHAT_MCH_ID', ''); this.apiKeyV3 = this.configService.get('WECHAT_API_KEY_V3', ''); this.appId = this.configService.get('WECHAT_APP_ID', ''); this.configured = !!(this.mchId && this.apiKeyV3 && this.appId); } isConfigured(): boolean { return this.configured; } async createPaymentSession(req: PaymentSessionRequest): Promise { if (!this.configured) throw new Error('WeChat Pay not configured'); if (req.currency !== InvoiceCurrency.CNY) { throw new Error('WeChat Pay only supports CNY payments'); } const outTradeNo = `it0-${req.invoiceId}-${Date.now()}`; const notifyUrl = this.configService.get( 'WECHAT_NOTIFY_URL', 'https://it0api.szaiai.com/api/v1/billing/webhooks/wechat', ); const body = { appid: this.appId, mchid: this.mchId, description: req.description, out_trade_no: outTradeNo, notify_url: notifyUrl, amount: { total: req.amountCents, currency: 'CNY' }, }; const response = await this.request('POST', '/v3/pay/transactions/native', body); const qrCodeUrl = response.code_url; return { sessionId: outTradeNo, providerPaymentId: outTradeNo, qrCodeUrl, redirectUrl: undefined, clientSecret: undefined, expiresAt: new Date(Date.now() + 2 * 60 * 60 * 1000), }; } async handleWebhook(rawBody: Buffer, headers: Record): Promise { if (!this.configured) throw new Error('WeChat Pay not configured'); // Verify HMAC-SHA256 signature const timestamp = headers['wechatpay-timestamp']; const nonce = headers['wechatpay-nonce']; const signature = headers['wechatpay-signature']; const message = `${timestamp}\n${nonce}\n${rawBody.toString()}\n`; const expectedSig = crypto .createHmac('sha256', this.apiKeyV3) .update(message) .digest('base64'); if (signature !== expectedSig) { throw new Error('WeChat Pay webhook signature invalid'); } const payload = JSON.parse(rawBody.toString()); const eventType = payload.event_type; if (eventType === 'TRANSACTION.SUCCESS') { // Decrypt resource const resource = payload.resource; const decrypted = this.decryptResource(resource.ciphertext, resource.associated_data, resource.nonce); const transaction = JSON.parse(decrypted); return { type: 'payment_succeeded', providerPaymentId: transaction.out_trade_no }; } else if (eventType === 'TRANSACTION.FAIL') { const resource = payload.resource; const decrypted = this.decryptResource(resource.ciphertext, resource.associated_data, resource.nonce); const transaction = JSON.parse(decrypted); return { type: 'payment_failed', providerPaymentId: transaction.out_trade_no }; } return { type: 'unknown' }; } private decryptResource(ciphertext: string, associatedData: string, nonce: string): string { const key = Buffer.from(this.apiKeyV3, 'utf-8'); const ciphertextBuffer = Buffer.from(ciphertext, 'base64'); const authTag = ciphertextBuffer.slice(-16); const encrypted = ciphertextBuffer.slice(0, -16); const decipher = crypto.createDecipheriv('aes-256-gcm', key, nonce); decipher.setAuthTag(authTag); decipher.setAAD(Buffer.from(associatedData)); return Buffer.concat([decipher.update(encrypted), decipher.final()]).toString('utf-8'); } private async request(method: string, path: string, body: any): Promise { const url = `${WeChatPayProvider.BASE_URL}${path}`; const timestamp = Math.floor(Date.now() / 1000); const nonce = crypto.randomBytes(16).toString('hex'); const bodyStr = JSON.stringify(body); const message = `${method}\n${path}\n${timestamp}\n${nonce}\n${bodyStr}\n`; // HMAC-SHA256 signature for request const signature = crypto .createHmac('sha256', this.apiKeyV3) .update(message) .digest('base64'); const authorization = `WECHATPAY2-SHA256-RSA2048 mchid="${this.mchId}",` + `nonce_str="${nonce}",timestamp="${timestamp}",` + `signature="${signature}"`; const res = await fetch(url, { method, headers: { 'Content-Type': 'application/json', Authorization: authorization, Accept: 'application/json', }, body: bodyStr, }); if (!res.ok) { const text = await res.text(); throw new Error(`WeChat Pay API error ${res.status}: ${text}`); } return res.json(); } }