|
@@ -16,6 +16,7 @@ import { OrderEntity } from '../entity/order';
|
|
|
import { PayeeAddressEntity } from '../entity/payee_address';
|
|
|
import { Utils } from '../../../comm/utils';
|
|
|
import { AmountCalculator } from '../../../comm/amount-calculator';
|
|
|
+import axios from 'axios';
|
|
|
|
|
|
/**
|
|
|
* 描述
|
|
@@ -62,6 +63,15 @@ export class OrderService extends BaseService {
|
|
|
return merchant
|
|
|
}
|
|
|
|
|
|
+
|
|
|
+ async getCustomer(customer_id): Promise<CustomerEntity> {
|
|
|
+ return await this.customerEntity.findOneBy({
|
|
|
+ customer_id: customer_id,
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
getMerchantId(params) {
|
|
|
let merchantId;
|
|
|
if (this._isOpenApi) {
|
|
@@ -247,6 +257,196 @@ export class OrderService extends BaseService {
|
|
|
throw new CoolCommException('出金处理失败:' + err.message);
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+ /* openapi */
|
|
|
+ async openApiCreatePayOut(params) {
|
|
|
+ let {
|
|
|
+ channel = 'SUNPAY', // 渠道
|
|
|
+ currency, // 币种
|
|
|
+ amount, // 金额
|
|
|
+ beneficiary_address_id, // 账户地址id
|
|
|
+ customer_id, // 客户ID
|
|
|
+ } = params;
|
|
|
+ if (amount < 100) {
|
|
|
+ throw new CoolCommException('金额不能小于100');
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ // 检查对应钱包余额
|
|
|
+ const wallet = await this.walletEntity.findOne({
|
|
|
+ where: {
|
|
|
+ customerId: customer_id,
|
|
|
+ channel: channel,
|
|
|
+ currency: currency,
|
|
|
+ },
|
|
|
+ });
|
|
|
+
|
|
|
+ // 检查可用余额是否足够(余额减去已冻结金额)
|
|
|
+ const availableBalance = this.amountCalculator.subtract(
|
|
|
+ wallet.balance,
|
|
|
+ wallet.frozenAmount
|
|
|
+ );
|
|
|
+ if (!wallet || availableBalance < amount) {
|
|
|
+ throw new CoolCommException('可用余额不足');
|
|
|
+ }
|
|
|
+
|
|
|
+ const customer = await this.getCustomer(customer_id)
|
|
|
+
|
|
|
+ if (!customer) {
|
|
|
+ throw new CoolCommException('客户不存在');
|
|
|
+ }
|
|
|
+
|
|
|
+ const payeeAddress = await this.payeeAddressEntity.findOne({
|
|
|
+ where: {
|
|
|
+ id: beneficiary_address_id,
|
|
|
+ customerId: customer_id,
|
|
|
+ },
|
|
|
+ });
|
|
|
+ if (!payeeAddress) {
|
|
|
+ throw new CoolCommException('收款账户不存在');
|
|
|
+ }
|
|
|
+ console.log(currency);
|
|
|
+ const type = ['ERC20', 'TRC20'].includes(currency) ? 'CRYPTO' : 'BANK';
|
|
|
+ const rate = await this.rateEntity.findOne({
|
|
|
+ where: {
|
|
|
+ currency: params.currency,
|
|
|
+ channelId: channel,
|
|
|
+ merchantId: customer.merchantId,
|
|
|
+ type: 'WITHDRAWAL',
|
|
|
+ },
|
|
|
+ });
|
|
|
+ if (!rate) {
|
|
|
+ throw new CoolCommException('支付方式不存在,请联系客服');
|
|
|
+ }
|
|
|
+ let orderData;
|
|
|
+ // 计算手续费率 (rate.rate 为百分比)
|
|
|
+ const feeRate = rate.rate / 100;
|
|
|
+ // 计算手续费金额
|
|
|
+ const feeAmount = this.amountCalculator.multiply(amount, feeRate);
|
|
|
+ // 扣除手续费后的最终金额
|
|
|
+ amount = this.amountCalculator.subtract(amount, feeAmount);
|
|
|
+
|
|
|
+ if (amount < 100) {
|
|
|
+ throw new CoolCommException('扣除手续费后金额小于100,无法提交');
|
|
|
+ }
|
|
|
+ switch (channel) {
|
|
|
+ case 'SUNPAY':
|
|
|
+ orderData = await this.generateOrderDataBySunpay(type, {
|
|
|
+ ...params,
|
|
|
+ amount: amount,
|
|
|
+ merchantId: customer.merchantId,
|
|
|
+ customerId: customer.customer_id,
|
|
|
+ beneficiary_address_id: payeeAddress.beneficiary_address_id,
|
|
|
+ cryptoAddress: payeeAddress.address,
|
|
|
+ paymentType: rate.method,
|
|
|
+ });
|
|
|
+ // orderData = {
|
|
|
+ // ...params
|
|
|
+ // }
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ // 先冻结金额
|
|
|
+ wallet.frozenAmount = this.amountCalculator.add(
|
|
|
+ wallet.frozenAmount,
|
|
|
+ params.amount
|
|
|
+ );
|
|
|
+ await this.walletEntity.save(wallet);
|
|
|
+ // 1. 创建订单
|
|
|
+ const order = await this.orderEntity.save({
|
|
|
+ merchantId: customer.merchantId,
|
|
|
+ channel: channel,
|
|
|
+ customer_id: customer_id,
|
|
|
+ order_type: 'WITHDRAW',
|
|
|
+ amount: orderData.amount,
|
|
|
+ currency: params.currency,
|
|
|
+ payment_type: rate.method,
|
|
|
+ status: 'PENDING',
|
|
|
+ out_order_no: orderData.out_order_no, // fusionPay 在 sunpay 创建的订单编号
|
|
|
+ order_no: params.out_order_no, // 下游在 fusionPay 创建的订单编号
|
|
|
+ webhook_url: params.webhook_url,
|
|
|
+ crypto_address: orderData.address ?? '',
|
|
|
+ beneficiary_address_id: beneficiary_address_id ?? '',
|
|
|
+ });
|
|
|
+ try {
|
|
|
+ // 2. 调用出金接口
|
|
|
+ let res;
|
|
|
+ if (type == 'CRYPTO') {
|
|
|
+ res = await this.paymentService .setChannel(channel).payOutByChain(orderData);
|
|
|
+ } else {
|
|
|
+ res = await this.paymentService.setChannel(channel).payOut({
|
|
|
+ ...orderData,
|
|
|
+ webhook_url: this.config.callback.sunpay,
|
|
|
+ });
|
|
|
+ }
|
|
|
+ // 3. 更新订单状态
|
|
|
+ order.status = 'PROCESSING';
|
|
|
+ order.out_order_no = res.data.order_no;
|
|
|
+ await this.orderEntity.update(order.id, order);
|
|
|
+ } catch (error) {
|
|
|
+ // 如果失败,解冻金额
|
|
|
+ wallet.frozenAmount = this.amountCalculator.subtract(
|
|
|
+ wallet.frozenAmount,
|
|
|
+ amount
|
|
|
+ );
|
|
|
+ await this.walletEntity.save(wallet);
|
|
|
+ // 记录错误日志
|
|
|
+ order.status = 'FAILED';
|
|
|
+ await this.orderEntity.update(order.id, order);
|
|
|
+ throw new CoolCommException('出金处理失败:' + error.message);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ async openApiConfirm(orderNo) {
|
|
|
+ const channel = 'SUBPAY'
|
|
|
+ // 查询订单是否存在
|
|
|
+ const orderInfo = await this.orderEntity.findOneBy({
|
|
|
+ order_no: orderNo,
|
|
|
+ order_type: 'WITHDRAW' // 出金
|
|
|
+ })
|
|
|
+
|
|
|
+ if(!orderInfo) {
|
|
|
+ throw new CoolCommException('订单不存在');
|
|
|
+ }
|
|
|
+ await this.paymentService
|
|
|
+ .setChannel(channel)
|
|
|
+ .confirmPayOut(orderInfo.out_order_no);
|
|
|
+ return ''
|
|
|
+ }
|
|
|
+ async openApiCancel(orderNo) {
|
|
|
+ const channel = 'SUBPAY'
|
|
|
+ // 查询订单是否存在
|
|
|
+ const orderInfo = await this.orderEntity.findOneBy({
|
|
|
+ order_no: orderNo,
|
|
|
+ order_type: 'WITHDRAW' // 出金
|
|
|
+ })
|
|
|
+
|
|
|
+ if(!orderInfo) {
|
|
|
+ throw new CoolCommException('订单不存在');
|
|
|
+ }
|
|
|
+
|
|
|
+ if(orderInfo.status === 'PENDING') {
|
|
|
+ await this.paymentService
|
|
|
+ .setChannel(channel)
|
|
|
+ .cancelPayOut(orderInfo.out_order_no);
|
|
|
+ orderInfo.status = 'REJECTED'
|
|
|
+ await this.orderEntity.save(orderInfo)
|
|
|
+ }
|
|
|
+ return ''
|
|
|
+ }
|
|
|
+ async openApiPatOutInfo(orderNo) {
|
|
|
+ // 查询订单是否存在
|
|
|
+ const orderInfo = await this.orderEntity.findOneBy({
|
|
|
+ order_no: orderNo,
|
|
|
+ order_type: 'WITHDRAW' // 出金
|
|
|
+ })
|
|
|
+ if(!orderInfo) {
|
|
|
+ throw new CoolCommException('订单不存在');
|
|
|
+ }
|
|
|
+
|
|
|
+ return orderInfo
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
private generateOrderNo() {
|
|
|
return 'O' + Date.now() + Math.random().toString(36).substring(2, 15);
|
|
|
}
|
|
@@ -400,13 +600,30 @@ export class OrderService extends BaseService {
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
+ const amount = this.getOrderFee(order)
|
|
|
if (order.status == 'SUCCESS') {
|
|
|
+ await this.customeCallback('SUCCESS', biz_type, {
|
|
|
+ order_no: order.out_order_no,
|
|
|
+ out_order_no: order.order_no,
|
|
|
+ out_user_id: order.merchantId,
|
|
|
+ amount: order.amount,
|
|
|
+ fee: Number(amount) - order.amount,
|
|
|
+ currency: order.currency
|
|
|
+ })
|
|
|
return {
|
|
|
is_success: true,
|
|
|
message: '成功',
|
|
|
};
|
|
|
}
|
|
|
if (order.status == 'FAILED') {
|
|
|
+ await this.customeCallback('FAIL',biz_type, {
|
|
|
+ order_no: order.out_order_no,
|
|
|
+ out_order_no: order.order_no,
|
|
|
+ out_user_id: order.merchantId,
|
|
|
+ amount: order.amount,
|
|
|
+ fee: Number(amount) - order.amount,
|
|
|
+ currency: order.currency
|
|
|
+ })
|
|
|
return {
|
|
|
is_success: false,
|
|
|
message: '订单已失效',
|
|
@@ -500,6 +717,7 @@ export class OrderService extends BaseService {
|
|
|
});
|
|
|
|
|
|
if (!wallet) {
|
|
|
+ this.customeCallback('payOut', order)
|
|
|
throw new CoolCommException('钱包不存在');
|
|
|
}
|
|
|
const merchant = await this.merchantEntity.findOne({
|
|
@@ -611,5 +829,65 @@ export class OrderService extends BaseService {
|
|
|
// 更新钱包余额和冻结金额
|
|
|
wallet.frozenAmount = newFrozenAmount;
|
|
|
await manager.save(wallet);
|
|
|
+ this.customeCallback('SUCCESS','PAYOUT', {
|
|
|
+ order_no: order.out_order_no,
|
|
|
+ out_order_no: order.order_no,
|
|
|
+ out_user_id: order.merchantId,
|
|
|
+ amount: order.amount,
|
|
|
+ fee: amount - order.amount,
|
|
|
+ currency: order.currency
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 发送请求到 商户 API
|
|
|
+ */
|
|
|
+ private async customeCallback( biz_status:'SUCCESS'|'FAIL' = 'SUCCESS', biz_type:'PAYOUT'|"PAYIN", data?: any, ) {
|
|
|
+ const {order_no, out_order_no, out_user_id, amount, fee, currency} = data
|
|
|
+ // 查询订单是否存在
|
|
|
+ const orderInfo = await this.orderEntity.findOneBy({
|
|
|
+ out_order_no: out_order_no,
|
|
|
+ order_type: biz_type === 'PAYOUT' ? 'WITHDRAW' : 'DEPOSIT'// // 出金
|
|
|
+ })
|
|
|
+ try {
|
|
|
+ if (orderInfo.webhook_url && orderInfo.webhook_url !== this.config.callback.sunpay) {
|
|
|
+ await axios({
|
|
|
+ method: 'post',
|
|
|
+ url: orderInfo.webhook_url,
|
|
|
+ data: {
|
|
|
+ biz_status,
|
|
|
+ biz_type,
|
|
|
+ data: {
|
|
|
+ order_no,
|
|
|
+ out_order_no,
|
|
|
+ out_user_id,
|
|
|
+ amount,
|
|
|
+ fee,
|
|
|
+ currency
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ }
|
|
|
+ }
|
|
|
+ async getOrderFee(order) {
|
|
|
+ const rate = await this.rateEntity.findOne({
|
|
|
+ where: {
|
|
|
+ currency: order.currency,
|
|
|
+ channelId: 'SUNPAY',
|
|
|
+ merchantId: order.merchantId,
|
|
|
+ type: 'WITHDRAWAL',
|
|
|
+ },
|
|
|
+ });
|
|
|
+ let feeRate;
|
|
|
+ if (!rate) {
|
|
|
+ feeRate = 0.1;
|
|
|
+ } else {
|
|
|
+ feeRate = rate.rate / 100;
|
|
|
+ }
|
|
|
+ // 订单的金额是扣除了手续费的 所以要根据手续费返推之前的金额
|
|
|
+ const amount = this.amountCalculator.divide(order.amount, 1 - feeRate);
|
|
|
+ return amount
|
|
|
}
|
|
|
}
|