payment-kit 1.18.15 → 1.18.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/api/src/index.ts +2 -0
- package/api/src/libs/invoice.ts +5 -3
- package/api/src/libs/notification/template/customer-reward-succeeded.ts +32 -14
- package/api/src/libs/session.ts +9 -1
- package/api/src/libs/util.ts +12 -4
- package/api/src/routes/checkout-sessions.ts +286 -120
- package/api/src/routes/connect/change-payment.ts +9 -1
- package/api/src/routes/connect/change-plan.ts +9 -1
- package/api/src/routes/connect/collect-batch.ts +7 -5
- package/api/src/routes/connect/pay.ts +1 -1
- package/api/src/routes/connect/recharge-account.ts +124 -0
- package/api/src/routes/connect/setup.ts +8 -1
- package/api/src/routes/connect/shared.ts +175 -54
- package/api/src/routes/connect/subscribe.ts +11 -1
- package/api/src/routes/customers.ts +150 -7
- package/api/src/routes/donations.ts +1 -1
- package/api/src/routes/invoices.ts +47 -1
- package/api/src/routes/subscriptions.ts +0 -3
- package/blocklet.yml +2 -1
- package/package.json +16 -16
- package/src/app.tsx +11 -3
- package/src/components/info-card.tsx +6 -2
- package/src/components/info-row.tsx +1 -0
- package/src/components/invoice/recharge.tsx +85 -56
- package/src/components/invoice/table.tsx +7 -1
- package/src/components/subscription/portal/actions.tsx +1 -1
- package/src/components/subscription/portal/list.tsx +6 -0
- package/src/locales/en.tsx +9 -0
- package/src/locales/zh.tsx +9 -0
- package/src/pages/admin/payments/payouts/detail.tsx +16 -5
- package/src/pages/customer/index.tsx +226 -284
- package/src/pages/customer/invoice/detail.tsx +24 -16
- package/src/pages/customer/invoice/past-due.tsx +46 -23
- package/src/pages/customer/payout/detail.tsx +16 -5
- package/src/pages/customer/recharge/account.tsx +513 -0
- package/src/pages/customer/{recharge.tsx → recharge/subscription.tsx} +22 -19
- package/src/pages/customer/subscription/embed.tsx +16 -1
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import type { Transaction } from '@ocap/client';
|
|
2
|
+
import { fromAddress } from '@ocap/wallet';
|
|
3
|
+
import { fromTokenToUnit } from '@ocap/util';
|
|
4
|
+
import type { CallbackArgs } from '../../libs/auth';
|
|
5
|
+
import { getGasPayerExtra } from '../../libs/payment';
|
|
6
|
+
import { getTxMetadata } from '../../libs/util';
|
|
7
|
+
import { ensureAccountRecharge, getAuthPrincipalClaim } from './shared';
|
|
8
|
+
import logger from '../../libs/logger';
|
|
9
|
+
import { ensureRechargeInvoice } from '../../libs/invoice';
|
|
10
|
+
|
|
11
|
+
export default {
|
|
12
|
+
action: 'recharge-account',
|
|
13
|
+
authPrincipal: false,
|
|
14
|
+
claims: {
|
|
15
|
+
authPrincipal: async ({ extraParams }: CallbackArgs) => {
|
|
16
|
+
const { paymentMethod } = await ensureAccountRecharge(extraParams.customerDid, extraParams.currencyId);
|
|
17
|
+
return getAuthPrincipalClaim(paymentMethod, 'recharge-account');
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
onConnect: async ({ userDid, userPk, extraParams }: CallbackArgs) => {
|
|
21
|
+
const { customerDid, currencyId } = extraParams;
|
|
22
|
+
let { amount } = extraParams;
|
|
23
|
+
const { paymentMethod, paymentCurrency, customer } = await ensureAccountRecharge(customerDid, currencyId);
|
|
24
|
+
amount = fromTokenToUnit(amount, paymentCurrency.decimal).toString();
|
|
25
|
+
if (paymentMethod.type === 'arcblock') {
|
|
26
|
+
const tokens = [{ address: paymentCurrency.contract as string, value: amount }];
|
|
27
|
+
// @ts-ignore
|
|
28
|
+
const itx: TransferV3Tx = {
|
|
29
|
+
outputs: [{ owner: customerDid, tokens, assets: [] }],
|
|
30
|
+
data: getTxMetadata({
|
|
31
|
+
rechargeCustomerId: customer.id,
|
|
32
|
+
customerDid,
|
|
33
|
+
currencyId: paymentCurrency.id,
|
|
34
|
+
amount,
|
|
35
|
+
action: 'recharge-account',
|
|
36
|
+
}),
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const claims: { [key: string]: object } = {
|
|
40
|
+
prepareTx: {
|
|
41
|
+
type: 'TransferV3Tx',
|
|
42
|
+
description: `Recharge account for ${customerDid}`,
|
|
43
|
+
partialTx: { from: userDid, pk: userPk, itx },
|
|
44
|
+
requirement: { tokens },
|
|
45
|
+
chainInfo: {
|
|
46
|
+
host: paymentMethod.settings?.arcblock?.api_host as string,
|
|
47
|
+
id: paymentMethod.settings?.arcblock?.chain_id as string,
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
return claims;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
throw new Error(`Payment method ${paymentMethod.type} not supported`);
|
|
56
|
+
},
|
|
57
|
+
onAuth: async ({ request, userDid, claims, extraParams }: CallbackArgs) => {
|
|
58
|
+
const { customerDid, currencyId } = extraParams;
|
|
59
|
+
const { paymentMethod, paymentCurrency, customer } = await ensureAccountRecharge(customerDid, currencyId);
|
|
60
|
+
let { amount } = extraParams;
|
|
61
|
+
amount = fromTokenToUnit(amount, paymentCurrency.decimal).toString();
|
|
62
|
+
|
|
63
|
+
const afterTxExecution = async (paymentDetails: any) => {
|
|
64
|
+
await ensureRechargeInvoice(
|
|
65
|
+
{
|
|
66
|
+
total: amount,
|
|
67
|
+
description: 'Add funds for account',
|
|
68
|
+
currency_id: paymentCurrency.id,
|
|
69
|
+
metadata: {
|
|
70
|
+
payment_details: {
|
|
71
|
+
[paymentMethod.type]: paymentDetails,
|
|
72
|
+
receiverAddress: customerDid,
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
livemode: paymentCurrency?.livemode,
|
|
76
|
+
payment_settings: {
|
|
77
|
+
payment_method_types: [paymentMethod.type],
|
|
78
|
+
payment_method_options: {
|
|
79
|
+
[paymentMethod.type]: {
|
|
80
|
+
payer: userDid,
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
null,
|
|
86
|
+
paymentMethod,
|
|
87
|
+
customer!
|
|
88
|
+
);
|
|
89
|
+
};
|
|
90
|
+
if (paymentMethod.type === 'arcblock') {
|
|
91
|
+
try {
|
|
92
|
+
const client = paymentMethod.getOcapClient();
|
|
93
|
+
const claim = claims.find((x) => x.type === 'prepareTx');
|
|
94
|
+
|
|
95
|
+
const tx: Partial<Transaction> = client.decodeTx(claim.finalTx);
|
|
96
|
+
if (claim.delegator && claim.from) {
|
|
97
|
+
tx.delegator = claim.delegator;
|
|
98
|
+
tx.from = claim.from;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// @ts-ignore
|
|
102
|
+
const { buffer } = await client.encodeTransferV3Tx({ tx });
|
|
103
|
+
const txHash = await client.sendTransferV3Tx(
|
|
104
|
+
// @ts-ignore
|
|
105
|
+
{ tx, wallet: fromAddress(userDid) },
|
|
106
|
+
getGasPayerExtra(buffer, client.pickGasPayerHeaders(request))
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
await afterTxExecution({
|
|
110
|
+
tx_hash: txHash,
|
|
111
|
+
payer: userDid,
|
|
112
|
+
type: 'transfer',
|
|
113
|
+
});
|
|
114
|
+
return { hash: txHash };
|
|
115
|
+
} catch (err) {
|
|
116
|
+
console.error(err);
|
|
117
|
+
logger.error('Failed to finalize recharge on arcblock', { receiverAddress: customerDid, error: err });
|
|
118
|
+
throw err;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
throw new Error(`Payment method ${paymentMethod.type} not supported`);
|
|
123
|
+
},
|
|
124
|
+
};
|
|
@@ -132,6 +132,9 @@ export default {
|
|
|
132
132
|
result.push({
|
|
133
133
|
step,
|
|
134
134
|
claim: claims?.[0],
|
|
135
|
+
stepRequest: {
|
|
136
|
+
headers: request?.headers,
|
|
137
|
+
},
|
|
135
138
|
});
|
|
136
139
|
|
|
137
140
|
// 判断是否为最后一步
|
|
@@ -199,13 +202,17 @@ export default {
|
|
|
199
202
|
if (paymentMethod.type === 'arcblock') {
|
|
200
203
|
try {
|
|
201
204
|
await prepareTxExecution();
|
|
205
|
+
const requestArray = result
|
|
206
|
+
.map((item: { stepRequest?: Request }) => item.stepRequest)
|
|
207
|
+
.filter(Boolean) as Request[];
|
|
208
|
+
const requestSource = requestArray.length > 0 ? requestArray : request;
|
|
202
209
|
|
|
203
210
|
const { stakingAmount, ...paymentDetails } = await executeOcapTransactions(
|
|
204
211
|
userDid,
|
|
205
212
|
userPk,
|
|
206
213
|
claimsList,
|
|
207
214
|
paymentMethod,
|
|
208
|
-
|
|
215
|
+
requestSource,
|
|
209
216
|
subscription?.id,
|
|
210
217
|
paymentCurrency?.contract
|
|
211
218
|
);
|
|
@@ -6,14 +6,14 @@ import type { Transaction } from '@ocap/client';
|
|
|
6
6
|
import { BN, fromTokenToUnit, toBase58 } from '@ocap/util';
|
|
7
7
|
import { fromPublicKey } from '@ocap/wallet';
|
|
8
8
|
import type { Request } from 'express';
|
|
9
|
-
import isEmpty from 'lodash
|
|
9
|
+
import { isEmpty } from 'lodash';
|
|
10
10
|
|
|
11
11
|
import { estimateMaxGasForTx, hasStakedForGas } from '../../integrations/arcblock/stake';
|
|
12
12
|
import { encodeApproveItx } from '../../integrations/ethereum/token';
|
|
13
13
|
import { blocklet, ethWallet, wallet } from '../../libs/auth';
|
|
14
14
|
import logger from '../../libs/logger';
|
|
15
15
|
import { getGasPayerExtra, getTokenLimitsForDelegation } from '../../libs/payment';
|
|
16
|
-
import { getFastCheckoutAmount, getStatementDescriptor } from '../../libs/session';
|
|
16
|
+
import { getFastCheckoutAmount, getStatementDescriptor, isDonationCheckoutSession } from '../../libs/session';
|
|
17
17
|
import {
|
|
18
18
|
expandSubscriptionItems,
|
|
19
19
|
getSubscriptionCreateSetup,
|
|
@@ -59,7 +59,7 @@ export async function ensureCheckoutSession(checkoutSessionId: string) {
|
|
|
59
59
|
return checkoutSession;
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
export async function ensurePaymentIntent(checkoutSessionId: string, userDid?: string): Promise<Result> {
|
|
62
|
+
export async function ensurePaymentIntent(checkoutSessionId: string, userDid?: string, skipCustomer?: boolean): Promise<Result> {
|
|
63
63
|
const checkoutSession = await ensureCheckoutSession(checkoutSessionId);
|
|
64
64
|
|
|
65
65
|
let paymentCurrencyId;
|
|
@@ -98,12 +98,69 @@ export async function ensurePaymentIntent(checkoutSessionId: string, userDid?: s
|
|
|
98
98
|
paymentCurrencyId = subscription.currency_id;
|
|
99
99
|
paymentMethodId = subscription.default_payment_method_id;
|
|
100
100
|
}
|
|
101
|
-
|
|
102
|
-
|
|
101
|
+
let customer = null;
|
|
102
|
+
if (!skipCustomer) {
|
|
103
|
+
// 检查是否为打赏场景
|
|
104
|
+
const isDonation = isDonationCheckoutSession(checkoutSession);
|
|
105
|
+
|
|
106
|
+
// if donation, create customer if not exists
|
|
107
|
+
if (isDonation && !checkoutSession.customer_id && userDid) {
|
|
108
|
+
customer = await Customer.findByPkOrDid(userDid);
|
|
109
|
+
if (!customer) {
|
|
110
|
+
const { user } = await blocklet.getUser(userDid);
|
|
111
|
+
if (user) {
|
|
112
|
+
customer = await Customer.create({
|
|
113
|
+
did: userDid,
|
|
114
|
+
email: user.email,
|
|
115
|
+
name: user.fullName || userDid,
|
|
116
|
+
description: user.remark,
|
|
117
|
+
metadata: { fromDonation: true },
|
|
118
|
+
livemode: checkoutSession.livemode,
|
|
119
|
+
phone: user.phone,
|
|
120
|
+
delinquent: false,
|
|
121
|
+
balance: '0',
|
|
122
|
+
next_invoice_sequence: 1,
|
|
123
|
+
invoice_prefix: Customer.getInvoicePrefix(),
|
|
124
|
+
});
|
|
125
|
+
logger.info('Customer created for donation', { userDid, customerId: customer.id });
|
|
126
|
+
} else {
|
|
127
|
+
customer = await Customer.create({
|
|
128
|
+
did: userDid,
|
|
129
|
+
email: '',
|
|
130
|
+
name: 'anonymous',
|
|
131
|
+
description: 'Anonymous customer',
|
|
132
|
+
metadata: { fromDonation: true, anonymous: true },
|
|
133
|
+
livemode: checkoutSession.livemode,
|
|
134
|
+
phone:'',
|
|
135
|
+
delinquent: false,
|
|
136
|
+
balance: '0',
|
|
137
|
+
next_invoice_sequence: 1,
|
|
138
|
+
invoice_prefix: Customer.getInvoicePrefix(),
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (customer) {
|
|
144
|
+
await checkoutSession.update({ customer_id: customer.id, customer_did: customer.did });
|
|
145
|
+
if (paymentIntent) {
|
|
146
|
+
await paymentIntent.update({ customer_id: customer.id });
|
|
147
|
+
}
|
|
148
|
+
logger.info('Customer associated with donation', {
|
|
149
|
+
userDid,
|
|
150
|
+
customerId: customer.id,
|
|
151
|
+
checkoutSessionId: checkoutSession.id,
|
|
152
|
+
paymentIntentId: paymentIntent?.id
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
} else {
|
|
156
|
+
// 非打赏场景或已有客户ID的情况
|
|
157
|
+
customer = await Customer.findByPk(checkoutSession.customer_id);
|
|
158
|
+
}
|
|
103
159
|
if (!customer) {
|
|
104
160
|
throw new Error('Customer not found');
|
|
105
161
|
}
|
|
106
|
-
|
|
162
|
+
|
|
163
|
+
if (userDid && !isDonation) {
|
|
107
164
|
const { user } = await blocklet.getUser(userDid, { enableConnectedAccount: true });
|
|
108
165
|
if (!user) {
|
|
109
166
|
throw new Error('Seems you have not connected to this app before');
|
|
@@ -113,6 +170,8 @@ export async function ensurePaymentIntent(checkoutSessionId: string, userDid?: s
|
|
|
113
170
|
throw new Error('This is not your payment intent');
|
|
114
171
|
}
|
|
115
172
|
}
|
|
173
|
+
}
|
|
174
|
+
|
|
116
175
|
|
|
117
176
|
const [paymentMethod, paymentCurrency] = await Promise.all([
|
|
118
177
|
PaymentMethod.findByPk(paymentMethodId),
|
|
@@ -134,7 +193,7 @@ export async function ensurePaymentIntent(checkoutSessionId: string, userDid?: s
|
|
|
134
193
|
return {
|
|
135
194
|
checkoutSession,
|
|
136
195
|
paymentIntent,
|
|
137
|
-
customer,
|
|
196
|
+
customer: customer as Customer,
|
|
138
197
|
subscription,
|
|
139
198
|
paymentMethod,
|
|
140
199
|
paymentCurrency,
|
|
@@ -481,6 +540,25 @@ export async function ensureSubscriptionRecharge(subscriptionId: string) {
|
|
|
481
540
|
};
|
|
482
541
|
}
|
|
483
542
|
|
|
543
|
+
export async function ensureAccountRecharge(customerId: string, currencyId: string) {
|
|
544
|
+
const customer = await Customer.findByPkOrDid(customerId);
|
|
545
|
+
if (!customer) {
|
|
546
|
+
throw new Error(`Customer ${customerId} not found`);
|
|
547
|
+
}
|
|
548
|
+
const paymentCurrency = await PaymentCurrency.findByPk(currencyId);
|
|
549
|
+
if (!paymentCurrency) {
|
|
550
|
+
throw new Error(`Currency ${currencyId} not found`);
|
|
551
|
+
}
|
|
552
|
+
const paymentMethod = await PaymentMethod.findByPk(paymentCurrency.payment_method_id);
|
|
553
|
+
if (!paymentMethod) {
|
|
554
|
+
throw new Error(`Payment method ${paymentCurrency.payment_method_id} not found`);
|
|
555
|
+
}
|
|
556
|
+
return {
|
|
557
|
+
paymentCurrency: paymentCurrency as PaymentCurrency,
|
|
558
|
+
paymentMethod: paymentMethod as PaymentMethod,
|
|
559
|
+
customer,
|
|
560
|
+
};
|
|
561
|
+
}
|
|
484
562
|
export function getAuthPrincipalClaim(method: PaymentMethod, action: string) {
|
|
485
563
|
let chainInfo = { type: 'none', id: 'none', host: 'none' };
|
|
486
564
|
if (method.type === 'arcblock') {
|
|
@@ -934,10 +1012,16 @@ export async function ensureChangePaymentContext(subscriptionId: string) {
|
|
|
934
1012
|
};
|
|
935
1013
|
}
|
|
936
1014
|
|
|
937
|
-
export async function ensureSubscriptionForCollectBatch(
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
1015
|
+
export async function ensureSubscriptionForCollectBatch(
|
|
1016
|
+
subscriptionId?: string,
|
|
1017
|
+
currencyId?: string,
|
|
1018
|
+
customerId?: string
|
|
1019
|
+
) {
|
|
1020
|
+
if (!currencyId) {
|
|
1021
|
+
throw new Error('Currency ID must be provided');
|
|
1022
|
+
}
|
|
1023
|
+
if (!subscriptionId && !customerId) {
|
|
1024
|
+
throw new Error('Either subscriptionId or customerId must be provided');
|
|
941
1025
|
}
|
|
942
1026
|
|
|
943
1027
|
const paymentCurrency = await PaymentCurrency.findByPk(currencyId);
|
|
@@ -945,16 +1029,36 @@ export async function ensureSubscriptionForCollectBatch(subscriptionId: string,
|
|
|
945
1029
|
throw new Error(`PaymentCurrency ${currencyId} not found when prepare batch collect`);
|
|
946
1030
|
}
|
|
947
1031
|
|
|
1032
|
+
let subscription;
|
|
1033
|
+
let searchCustomerId = customerId;
|
|
1034
|
+
|
|
1035
|
+
if (subscriptionId) {
|
|
1036
|
+
subscription = await Subscription.findByPk(subscriptionId);
|
|
1037
|
+
if (!subscription) {
|
|
1038
|
+
throw new Error(`Subscription ${subscriptionId} not found when prepare batch collect`);
|
|
1039
|
+
}
|
|
1040
|
+
searchCustomerId = subscription.customer_id;
|
|
1041
|
+
} else if (customerId) {
|
|
1042
|
+
const customer = await Customer.findByPkOrDid(customerId);
|
|
1043
|
+
if (!customer) {
|
|
1044
|
+
throw new Error(`Customer ${customerId} not found when prepare batch collect`);
|
|
1045
|
+
}
|
|
1046
|
+
searchCustomerId = customer.id;
|
|
1047
|
+
}
|
|
1048
|
+
|
|
948
1049
|
const [summary, detail] = await Invoice.getUncollectibleAmount({
|
|
949
|
-
subscriptionId:
|
|
950
|
-
customerId:
|
|
1050
|
+
...(subscriptionId ? { subscriptionId } : {}),
|
|
1051
|
+
customerId: searchCustomerId,
|
|
951
1052
|
currencyId,
|
|
952
1053
|
});
|
|
953
1054
|
if (isEmpty(summary) || !summary[currencyId] || summary[currencyId] === '0') {
|
|
954
|
-
throw new Error(
|
|
1055
|
+
throw new Error('No uncollectible invoice found to batch collect');
|
|
955
1056
|
}
|
|
956
1057
|
|
|
957
1058
|
const paymentMethod = await PaymentMethod.findByPk(paymentCurrency.payment_method_id);
|
|
1059
|
+
if (!paymentMethod) {
|
|
1060
|
+
throw new Error(`Payment method not found for currency ${currencyId}`);
|
|
1061
|
+
}
|
|
958
1062
|
|
|
959
1063
|
return {
|
|
960
1064
|
subscription,
|
|
@@ -1005,7 +1109,7 @@ export async function executeOcapTransactions(
|
|
|
1005
1109
|
userPk: string,
|
|
1006
1110
|
claims: any[],
|
|
1007
1111
|
paymentMethod: PaymentMethod,
|
|
1008
|
-
|
|
1112
|
+
requestSource: Request | Request[] | any[],
|
|
1009
1113
|
subscriptionId?: string,
|
|
1010
1114
|
paymentCurrencyContract?: string,
|
|
1011
1115
|
nonce?: string
|
|
@@ -1022,47 +1126,64 @@ export async function executeOcapTransactions(
|
|
|
1022
1126
|
const stakingAmount =
|
|
1023
1127
|
staking?.requirement?.tokens?.find((x: any) => x.address === paymentCurrencyContract)?.value || '0';
|
|
1024
1128
|
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
}
|
|
1036
|
-
|
|
1037
|
-
// @ts-ignore
|
|
1038
|
-
const { buffer } = await client[`encode${type}Tx`]({ tx });
|
|
1039
|
-
// @ts-ignore
|
|
1040
|
-
const txHash = await client[`send${type}Tx`](
|
|
1041
|
-
// @ts-ignore
|
|
1042
|
-
{ tx, wallet: fromPublicKey(userPk, toTypeInfo(userDid)) },
|
|
1043
|
-
getGasPayerExtra(buffer, client.pickGasPayerHeaders(request))
|
|
1044
|
-
);
|
|
1045
|
-
|
|
1046
|
-
return txHash;
|
|
1047
|
-
})
|
|
1048
|
-
);
|
|
1129
|
+
try {
|
|
1130
|
+
const getHeaders = (index: number): Record<string, string> => {
|
|
1131
|
+
if (Array.isArray(requestSource)) {
|
|
1132
|
+
if (requestSource.length === 0) {
|
|
1133
|
+
return {};
|
|
1134
|
+
}
|
|
1135
|
+
const headerIndex = index < requestSource.length ? index : 0;
|
|
1136
|
+
const req = requestSource[headerIndex];
|
|
1137
|
+
return req ? client.pickGasPayerHeaders(req) : {};
|
|
1138
|
+
}
|
|
1049
1139
|
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
address: await getCustomerStakeAddress(userDid, nonce || subscriptionId || ''),
|
|
1057
|
-
},
|
|
1058
|
-
stakingAmount,
|
|
1059
|
-
};
|
|
1060
|
-
} catch (err) {
|
|
1061
|
-
logger.error('executeOcapTransactions failed', err);
|
|
1062
|
-
throw err;
|
|
1063
|
-
}
|
|
1064
|
-
}
|
|
1140
|
+
if (requestSource && typeof requestSource === 'object') {
|
|
1141
|
+
return client.pickGasPayerHeaders(requestSource);
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
return {};
|
|
1145
|
+
};
|
|
1065
1146
|
|
|
1147
|
+
const [delegationTxHash, stakingTxHash] = await Promise.all(
|
|
1148
|
+
transactions.map(async ([claim, type], index) => {
|
|
1149
|
+
if (!claim) {
|
|
1150
|
+
return '';
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
const tx: Partial<Transaction> = client.decodeTx(claim.finalTx || claim.origin);
|
|
1154
|
+
if (claim.sig) {
|
|
1155
|
+
tx.signature = claim.sig;
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
// @ts-ignore
|
|
1159
|
+
const { buffer } = await client[`encode${type}Tx`]({ tx });
|
|
1160
|
+
const gasPayerHeaders = getHeaders(index);
|
|
1161
|
+
// @ts-ignore
|
|
1162
|
+
const txHash = await client[`send${type}Tx`](
|
|
1163
|
+
// @ts-ignore
|
|
1164
|
+
{ tx, wallet: fromPublicKey(userPk, toTypeInfo(userDid)) },
|
|
1165
|
+
getGasPayerExtra(buffer, gasPayerHeaders)
|
|
1166
|
+
);
|
|
1167
|
+
|
|
1168
|
+
return txHash;
|
|
1169
|
+
})
|
|
1170
|
+
);
|
|
1171
|
+
|
|
1172
|
+
return {
|
|
1173
|
+
tx_hash: delegationTxHash,
|
|
1174
|
+
payer: userDid,
|
|
1175
|
+
type: 'delegate',
|
|
1176
|
+
staking: {
|
|
1177
|
+
tx_hash: stakingTxHash,
|
|
1178
|
+
address: await getCustomerStakeAddress(userDid, nonce || subscriptionId || ''),
|
|
1179
|
+
},
|
|
1180
|
+
stakingAmount,
|
|
1181
|
+
};
|
|
1182
|
+
} catch (err) {
|
|
1183
|
+
logger.error('executeOcapTransactions failed', err);
|
|
1184
|
+
throw err;
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1066
1187
|
|
|
1067
1188
|
export async function updateStripeSubscriptionAfterChangePayment(setupIntent: SetupIntent, subscription: Subscription) {
|
|
1068
1189
|
const { from_method: fromMethodId, to_method: toMethodId } = setupIntent.metadata || {};
|
|
@@ -129,6 +129,9 @@ export default {
|
|
|
129
129
|
result.push({
|
|
130
130
|
step,
|
|
131
131
|
claim: claims?.[0],
|
|
132
|
+
stepRequest: {
|
|
133
|
+
headers: request?.headers,
|
|
134
|
+
},
|
|
132
135
|
});
|
|
133
136
|
const staking = result.find((x: any) => x.claim?.type === 'prepareTx' && x.claim?.meta?.purpose === 'staking');
|
|
134
137
|
const isFinalStep = (paymentMethod.type === 'arcblock' && staking) || paymentMethod.type !== 'arcblock';
|
|
@@ -183,12 +186,19 @@ export default {
|
|
|
183
186
|
if (invoice) {
|
|
184
187
|
await invoice.update({ payment_settings: paymentSettings });
|
|
185
188
|
}
|
|
189
|
+
|
|
190
|
+
const requestArray = result
|
|
191
|
+
.map((item: { stepRequest?: Request }) => item.stepRequest)
|
|
192
|
+
.filter(Boolean) as Request[];
|
|
193
|
+
|
|
194
|
+
const requestSource = requestArray.length > 0 ? requestArray : request;
|
|
195
|
+
|
|
186
196
|
const { stakingAmount, ...paymentDetails } = await executeOcapTransactions(
|
|
187
197
|
userDid,
|
|
188
198
|
userPk,
|
|
189
199
|
claimsList,
|
|
190
200
|
paymentMethod,
|
|
191
|
-
|
|
201
|
+
requestSource,
|
|
192
202
|
subscription?.id,
|
|
193
203
|
paymentCurrency?.contract
|
|
194
204
|
);
|
|
@@ -4,14 +4,26 @@ import Joi from 'joi';
|
|
|
4
4
|
import pick from 'lodash/pick';
|
|
5
5
|
import isEmail from 'validator/es/lib/isEmail';
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import { Op } from 'sequelize';
|
|
8
|
+
import { BN } from '@ocap/util';
|
|
9
|
+
import { getStakeSummaryByDid, getTokenSummaryByDid, getTokenByAddress } from '../integrations/arcblock/stake';
|
|
8
10
|
import { createListParamSchema, getWhereFromKvQuery, getWhereFromQuery, MetadataSchema } from '../libs/api';
|
|
9
11
|
import { authenticate } from '../libs/security';
|
|
10
12
|
import { formatMetadata } from '../libs/util';
|
|
11
13
|
import { Customer } from '../store/models/customer';
|
|
12
14
|
import { blocklet } from '../libs/auth';
|
|
13
15
|
import logger from '../libs/logger';
|
|
14
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
Invoice,
|
|
18
|
+
PaymentCurrency,
|
|
19
|
+
PaymentMethod,
|
|
20
|
+
Price,
|
|
21
|
+
Product,
|
|
22
|
+
Subscription,
|
|
23
|
+
SubscriptionItem,
|
|
24
|
+
} from '../store/models';
|
|
25
|
+
import { getSubscriptionPaymentAddress } from '../libs/subscription';
|
|
26
|
+
import { expandLineItems } from '../libs/session';
|
|
15
27
|
|
|
16
28
|
const router = Router();
|
|
17
29
|
const auth = authenticate<Customer>({ component: true, roles: ['owner', 'admin'] });
|
|
@@ -102,7 +114,7 @@ router.get('/me', sessionMiddleware(), async (req, res) => {
|
|
|
102
114
|
did: req.user.did,
|
|
103
115
|
name: user.fullName,
|
|
104
116
|
email: user.email,
|
|
105
|
-
phone:
|
|
117
|
+
phone: user.phone,
|
|
106
118
|
address: {},
|
|
107
119
|
description: user.remark,
|
|
108
120
|
metadata: {},
|
|
@@ -166,16 +178,48 @@ router.get('/:id/overdue/invoices', auth, async (req, res) => {
|
|
|
166
178
|
if (!doc) {
|
|
167
179
|
return res.status(404).json({ error: 'Customer not found' });
|
|
168
180
|
}
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
|
|
181
|
+
const { rows: invoices, count } = await Invoice.findAndCountAll({
|
|
182
|
+
where: {
|
|
183
|
+
customer_id: doc.id,
|
|
184
|
+
status: ['uncollectible'],
|
|
185
|
+
amount_remaining: { [Op.gt]: '0' },
|
|
186
|
+
},
|
|
187
|
+
include: [
|
|
188
|
+
{ model: PaymentCurrency, as: 'paymentCurrency' },
|
|
189
|
+
{ model: PaymentMethod, as: 'paymentMethod' },
|
|
190
|
+
],
|
|
191
|
+
});
|
|
192
|
+
if (count === 0) {
|
|
193
|
+
return res.json({
|
|
194
|
+
summary: null,
|
|
195
|
+
invoices: [],
|
|
196
|
+
subscriptionCount: 0,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
const summary: Record<string, { amount: string; currency: PaymentCurrency; method: PaymentMethod }> = {};
|
|
200
|
+
invoices.forEach((invoice) => {
|
|
201
|
+
const key = invoice.currency_id;
|
|
202
|
+
if (!summary[key]) {
|
|
203
|
+
summary[key] = {
|
|
204
|
+
amount: '0',
|
|
205
|
+
// @ts-ignore
|
|
206
|
+
currency: invoice.paymentCurrency,
|
|
207
|
+
// @ts-ignore
|
|
208
|
+
method: invoice.paymentMethod,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
if (invoice && summary[key]) {
|
|
212
|
+
// @ts-ignore
|
|
213
|
+
summary[key].amount = new BN(summary[key]?.amount || '0')
|
|
214
|
+
.add(new BN(invoice.amount_remaining || '0'))
|
|
215
|
+
.toString();
|
|
216
|
+
}
|
|
172
217
|
});
|
|
173
218
|
const subscriptionCount = new Set(invoices.map((x) => x.subscription_id)).size;
|
|
174
219
|
return res.json({
|
|
175
220
|
summary,
|
|
176
221
|
invoices,
|
|
177
222
|
subscriptionCount,
|
|
178
|
-
detail,
|
|
179
223
|
});
|
|
180
224
|
} catch (err) {
|
|
181
225
|
logger.error(err);
|
|
@@ -183,6 +227,105 @@ router.get('/:id/overdue/invoices', auth, async (req, res) => {
|
|
|
183
227
|
}
|
|
184
228
|
});
|
|
185
229
|
|
|
230
|
+
router.get('/recharge', sessionMiddleware(), async (req, res) => {
|
|
231
|
+
if (!req.user) {
|
|
232
|
+
return res.status(403).json({ error: 'Unauthorized' });
|
|
233
|
+
}
|
|
234
|
+
if (!req.query.currencyId) {
|
|
235
|
+
return res.status(400).json({ error: 'Currency ID is required' });
|
|
236
|
+
}
|
|
237
|
+
try {
|
|
238
|
+
const customer = await Customer.findByPkOrDid(req.user.did as string);
|
|
239
|
+
if (!customer) {
|
|
240
|
+
return res.status(404).json({ error: 'Customer not found' });
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const paymentCurrency = await PaymentCurrency.findByPk(req.query.currencyId as string);
|
|
244
|
+
if (!paymentCurrency) {
|
|
245
|
+
return res.status(404).json({ error: 'Currency not found' });
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const paymentMethod = await PaymentMethod.findByPk(paymentCurrency.payment_method_id);
|
|
249
|
+
if (!paymentMethod) {
|
|
250
|
+
return res.status(404).json({ error: 'Payment method not found' });
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
let subscriptions = await Subscription.findAll({
|
|
254
|
+
where: {
|
|
255
|
+
customer_id: customer.id,
|
|
256
|
+
currency_id: paymentCurrency.id,
|
|
257
|
+
status: {
|
|
258
|
+
[Op.in]: ['active', 'trialing', 'past_due'],
|
|
259
|
+
},
|
|
260
|
+
},
|
|
261
|
+
include: [{ model: SubscriptionItem, as: 'items' }],
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
const products = (await Product.findAll()).map((x) => x.toJSON());
|
|
265
|
+
const prices = (await Price.findAll()).map((x) => x.toJSON());
|
|
266
|
+
subscriptions = subscriptions.map((x) => x.toJSON());
|
|
267
|
+
// @ts-ignore
|
|
268
|
+
subscriptions.forEach((x) => expandLineItems(x.items, products, prices));
|
|
269
|
+
|
|
270
|
+
const relatedSubscriptions = subscriptions.filter((sub) => {
|
|
271
|
+
const payerAddress = getSubscriptionPaymentAddress(sub, paymentMethod.type);
|
|
272
|
+
return payerAddress === customer.did;
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
return res.json({
|
|
276
|
+
currency: {
|
|
277
|
+
...paymentCurrency.toJSON(),
|
|
278
|
+
paymentMethod,
|
|
279
|
+
},
|
|
280
|
+
relatedSubscriptions,
|
|
281
|
+
});
|
|
282
|
+
} catch (err) {
|
|
283
|
+
logger.error('Error getting balance recharge info', err);
|
|
284
|
+
return res.status(500).json({ error: err.message });
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
// get address token
|
|
289
|
+
router.get('/payer-token', sessionMiddleware(), async (req, res) => {
|
|
290
|
+
if (!req.user) {
|
|
291
|
+
return res.status(403).json({ error: 'Unauthorized' });
|
|
292
|
+
}
|
|
293
|
+
if (!req.query.currencyId) {
|
|
294
|
+
return res.status(400).json({ error: 'Currency ID is required' });
|
|
295
|
+
}
|
|
296
|
+
try {
|
|
297
|
+
const customer = await Customer.findByPkOrDid(req.user.did as string);
|
|
298
|
+
if (!customer) {
|
|
299
|
+
return res.status(404).json({ error: 'Customer not found' });
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const paymentCurrency = await PaymentCurrency.findByPk(req.query.currencyId as string);
|
|
303
|
+
if (!paymentCurrency) {
|
|
304
|
+
return res.status(404).json({ error: 'Currency not found' });
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const paymentMethod = await PaymentMethod.findByPk(paymentCurrency.payment_method_id);
|
|
308
|
+
if (!paymentMethod) {
|
|
309
|
+
return res.status(404).json({ error: 'Payment method not found' });
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (paymentMethod.type !== 'arcblock') {
|
|
313
|
+
return res.status(400).json({ error: `Payment method not supported: ${paymentMethod.type}` });
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const paymentAddress = customer.did;
|
|
317
|
+
if (!paymentAddress) {
|
|
318
|
+
return res.status(400).json({ error: `Payment address not found for customer: ${customer.id}` });
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const token = await getTokenByAddress(paymentAddress, paymentMethod, paymentCurrency);
|
|
322
|
+
return res.json({ token, paymentAddress });
|
|
323
|
+
} catch (err) {
|
|
324
|
+
logger.error('Error getting customer payer token', err);
|
|
325
|
+
return res.status(500).json({ error: err.message });
|
|
326
|
+
}
|
|
327
|
+
});
|
|
328
|
+
|
|
186
329
|
router.get('/:id', auth, async (req, res) => {
|
|
187
330
|
try {
|
|
188
331
|
const doc = await Customer.findByPkOrDid(req.params.id as string);
|