2026-03-05 01:48:10 +08:00
import type {
PaymentProvider ,
PaymentType ,
CreatePaymentRequest ,
CreatePaymentResponse ,
QueryOrderResponse ,
PaymentNotification ,
RefundRequest ,
RefundResponse ,
} from '@/lib/payment/types' ;
import { pageExecute , execute } from './client' ;
import { verifySign } from './sign' ;
import { getEnv } from '@/lib/config' ;
2026-03-10 18:20:36 +08:00
import type { AlipayTradeQueryResponse , AlipayTradeRefundResponse , AlipayTradeCloseResponse } from './types' ;
2026-03-10 11:52:37 +08:00
import { parseAlipayNotificationParams } from './codec' ;
export interface BuildAlipayPaymentUrlInput {
orderId : string ;
amount : number ;
subject : string ;
notifyUrl? : string ;
returnUrl? : string | null ;
isMobile? : boolean ;
}
function isTradeNotExistError ( error : unknown ) : boolean {
if ( ! ( error instanceof Error ) ) return false ;
return error . message . includes ( '[ACQ.TRADE_NOT_EXIST]' ) ;
}
function getRequiredParam ( params : Record < string , string > , key : string ) : string {
const value = params [ key ] ? . trim ( ) ;
if ( ! value ) {
throw new Error ( ` Alipay notification missing required field: ${ key } ` ) ;
}
return value ;
}
export function buildAlipayPaymentUrl ( input : BuildAlipayPaymentUrlInput ) : string {
const method = input . isMobile ? 'alipay.trade.wap.pay' : 'alipay.trade.page.pay' ;
const productCode = input . isMobile ? 'QUICK_WAP_WAY' : 'FAST_INSTANT_TRADE_PAY' ;
return pageExecute (
{
out_trade_no : input.orderId ,
product_code : productCode ,
total_amount : input.amount.toFixed ( 2 ) ,
subject : input.subject ,
} ,
{
notifyUrl : input.notifyUrl ,
returnUrl : input.returnUrl ,
method ,
} ,
) ;
}
export function buildAlipayEntryUrl ( orderId : string ) : string {
const env = getEnv ( ) ;
return new URL ( ` /pay/ ${ orderId } ` , env . NEXT_PUBLIC_APP_URL ) . toString ( ) ;
}
2026-03-05 01:48:10 +08:00
export class AlipayProvider implements PaymentProvider {
2026-03-06 15:33:22 +08:00
readonly name = 'alipay-direct' ;
2026-03-05 01:52:59 +08:00
readonly providerKey = 'alipay' ;
2026-03-06 15:33:22 +08:00
readonly supportedTypes : PaymentType [ ] = [ 'alipay_direct' ] ;
2026-03-05 01:48:10 +08:00
readonly defaultLimits = {
2026-03-08 00:06:23 +08:00
alipay_direct : { singleMax : 1000 , dailyMax : 10000 } ,
2026-03-05 01:48:10 +08:00
} ;
async createPayment ( request : CreatePaymentRequest ) : Promise < CreatePaymentResponse > {
2026-03-10 11:52:37 +08:00
if ( ! request . isMobile ) {
const entryUrl = buildAlipayEntryUrl ( request . orderId ) ;
return {
tradeNo : request.orderId ,
payUrl : entryUrl ,
qrCode : entryUrl ,
} ;
2026-03-06 22:40:09 +08:00
}
2026-03-05 01:48:10 +08:00
2026-03-10 11:52:37 +08:00
const payUrl = buildAlipayPaymentUrl ( {
orderId : request.orderId ,
amount : request.amount ,
subject : request.subject ,
notifyUrl : request.notifyUrl ,
returnUrl : request.returnUrl ,
isMobile : true ,
} ) ;
return { tradeNo : request.orderId , payUrl } ;
2026-03-05 01:48:10 +08:00
}
async queryOrder ( tradeNo : string ) : Promise < QueryOrderResponse > {
2026-03-10 11:52:37 +08:00
let result : AlipayTradeQueryResponse ;
try {
result = await execute < AlipayTradeQueryResponse > ( 'alipay.trade.query' , {
out_trade_no : tradeNo ,
} ) ;
} catch ( error ) {
if ( isTradeNotExistError ( error ) ) {
return {
tradeNo ,
status : 'pending' ,
amount : 0 ,
} ;
}
throw error ;
}
2026-03-05 01:48:10 +08:00
let status : 'pending' | 'paid' | 'failed' | 'refunded' ;
switch ( result . trade_status ) {
case 'TRADE_SUCCESS' :
case 'TRADE_FINISHED' :
status = 'paid' ;
break ;
case 'TRADE_CLOSED' :
status = 'failed' ;
break ;
default :
status = 'pending' ;
}
2026-03-13 23:03:01 +08:00
const amount = parseFloat ( result . total_amount || '0' ) ;
if ( ! Number . isFinite ( amount ) || amount <= 0 ) {
throw new Error ( ` Alipay queryOrder: invalid total_amount " ${ result . total_amount } " for trade ${ tradeNo } ` ) ;
}
2026-03-05 01:48:10 +08:00
return {
tradeNo : result.trade_no || tradeNo ,
status ,
2026-03-13 23:03:01 +08:00
amount : Math.round ( amount * 100 ) / 100 ,
2026-03-05 01:48:10 +08:00
paidAt : result.send_pay_date ? new Date ( result . send_pay_date ) : undefined ,
} ;
}
2026-03-10 11:52:37 +08:00
async verifyNotification ( rawBody : string | Buffer , headers : Record < string , string > ) : Promise < PaymentNotification > {
2026-03-05 01:48:10 +08:00
const env = getEnv ( ) ;
2026-03-10 11:52:37 +08:00
const params = parseAlipayNotificationParams ( rawBody , headers ) ;
2026-03-05 01:48:10 +08:00
2026-03-10 11:52:37 +08:00
if ( params . sign_type && params . sign_type . toUpperCase ( ) !== 'RSA2' ) {
2026-03-06 22:57:55 +08:00
throw new Error ( 'Unsupported sign_type, only RSA2 is accepted' ) ;
}
2026-03-10 11:52:37 +08:00
const sign = getRequiredParam ( params , 'sign' ) ;
2026-03-06 13:57:52 +08:00
if ( ! env . ALIPAY_PUBLIC_KEY || ! verifySign ( params , env . ALIPAY_PUBLIC_KEY , sign ) ) {
2026-03-10 11:52:37 +08:00
throw new Error (
'Alipay notification signature verification failed (check ALIPAY_PUBLIC_KEY uses Alipay public key, not app public key, and rebuild/redeploy the latest image)' ,
) ;
2026-03-05 01:48:10 +08:00
}
2026-03-10 11:52:37 +08:00
const tradeNo = getRequiredParam ( params , 'trade_no' ) ;
const orderId = getRequiredParam ( params , 'out_trade_no' ) ;
const tradeStatus = getRequiredParam ( params , 'trade_status' ) ;
const appId = getRequiredParam ( params , 'app_id' ) ;
if ( appId !== env . ALIPAY_APP_ID ) {
2026-03-06 22:57:55 +08:00
throw new Error ( 'Alipay notification app_id mismatch' ) ;
}
2026-03-10 11:52:37 +08:00
const amount = Number . parseFloat ( getRequiredParam ( params , 'total_amount' ) ) ;
if ( ! Number . isFinite ( amount ) || amount <= 0 ) {
throw new Error ( 'Alipay notification invalid total_amount' ) ;
}
2026-03-05 01:48:10 +08:00
return {
2026-03-10 11:52:37 +08:00
tradeNo ,
orderId ,
amount : Math.round ( amount * 100 ) / 100 ,
2026-03-10 18:20:36 +08:00
status : tradeStatus === 'TRADE_SUCCESS' || tradeStatus === 'TRADE_FINISHED' ? 'success' : 'failed' ,
2026-03-05 01:48:10 +08:00
rawData : params ,
} ;
}
async refund ( request : RefundRequest ) : Promise < RefundResponse > {
const result = await execute < AlipayTradeRefundResponse > ( 'alipay.trade.refund' , {
out_trade_no : request.orderId ,
refund_amount : request.amount.toFixed ( 2 ) ,
refund_reason : request.reason || '' ,
2026-03-06 22:57:55 +08:00
out_request_no : request.orderId + '-refund' ,
2026-03-05 01:48:10 +08:00
} ) ;
return {
refundId : result.trade_no || ` ${ request . orderId } -refund ` ,
status : result.fund_change === 'Y' ? 'success' : 'pending' ,
} ;
}
async cancelPayment ( tradeNo : string ) : Promise < void > {
2026-03-10 11:52:37 +08:00
try {
await execute < AlipayTradeCloseResponse > ( 'alipay.trade.close' , {
out_trade_no : tradeNo ,
} ) ;
} catch ( error ) {
if ( isTradeNotExistError ( error ) ) {
return ;
}
throw error ;
}
2026-03-05 01:48:10 +08:00
}
}