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.
Files changed (37) hide show
  1. package/api/src/index.ts +2 -0
  2. package/api/src/libs/invoice.ts +5 -3
  3. package/api/src/libs/notification/template/customer-reward-succeeded.ts +32 -14
  4. package/api/src/libs/session.ts +9 -1
  5. package/api/src/libs/util.ts +12 -4
  6. package/api/src/routes/checkout-sessions.ts +286 -120
  7. package/api/src/routes/connect/change-payment.ts +9 -1
  8. package/api/src/routes/connect/change-plan.ts +9 -1
  9. package/api/src/routes/connect/collect-batch.ts +7 -5
  10. package/api/src/routes/connect/pay.ts +1 -1
  11. package/api/src/routes/connect/recharge-account.ts +124 -0
  12. package/api/src/routes/connect/setup.ts +8 -1
  13. package/api/src/routes/connect/shared.ts +175 -54
  14. package/api/src/routes/connect/subscribe.ts +11 -1
  15. package/api/src/routes/customers.ts +150 -7
  16. package/api/src/routes/donations.ts +1 -1
  17. package/api/src/routes/invoices.ts +47 -1
  18. package/api/src/routes/subscriptions.ts +0 -3
  19. package/blocklet.yml +2 -1
  20. package/package.json +16 -16
  21. package/src/app.tsx +11 -3
  22. package/src/components/info-card.tsx +6 -2
  23. package/src/components/info-row.tsx +1 -0
  24. package/src/components/invoice/recharge.tsx +85 -56
  25. package/src/components/invoice/table.tsx +7 -1
  26. package/src/components/subscription/portal/actions.tsx +1 -1
  27. package/src/components/subscription/portal/list.tsx +6 -0
  28. package/src/locales/en.tsx +9 -0
  29. package/src/locales/zh.tsx +9 -0
  30. package/src/pages/admin/payments/payouts/detail.tsx +16 -5
  31. package/src/pages/customer/index.tsx +226 -284
  32. package/src/pages/customer/invoice/detail.tsx +24 -16
  33. package/src/pages/customer/invoice/past-due.tsx +46 -23
  34. package/src/pages/customer/payout/detail.tsx +16 -5
  35. package/src/pages/customer/recharge/account.tsx +513 -0
  36. package/src/pages/customer/{recharge.tsx → recharge/subscription.tsx} +22 -19
  37. 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
- request,
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/isEmpty';
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
- const customer = await Customer.findByPk(checkoutSession.customer_id);
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
- if (userDid) {
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(subscriptionId: string, currencyId: string) {
938
- const subscription = await Subscription.findByPk(subscriptionId);
939
- if (!subscription) {
940
- throw new Error(`Subscription ${subscriptionId} not found when prepare batch collect`);
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: subscription.id,
950
- customerId: subscription.customer_id,
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(`No uncollectible invoice found for subscription ${subscriptionId} to batch collect`);
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
- request: Request,
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
- try {
1026
- const [delegationTxHash, stakingTxHash] = await Promise.all(
1027
- transactions.map(async ([claim, type]) => {
1028
- if (!claim) {
1029
- return '';
1030
- }
1031
-
1032
- const tx: Partial<Transaction> = client.decodeTx(claim.finalTx || claim.origin);
1033
- if (claim.sig) {
1034
- tx.signature = claim.sig;
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
- return {
1051
- tx_hash: delegationTxHash,
1052
- payer: userDid,
1053
- type: 'delegate',
1054
- staking: {
1055
- tx_hash: stakingTxHash,
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
- request,
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 { getStakeSummaryByDid, getTokenSummaryByDid } from '../integrations/arcblock/stake';
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 { Invoice } from '../store/models';
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 [summary, detail, invoices] = await Invoice!.getUncollectibleAmount({
170
- customerId: doc.id,
171
- livemode: req.query.livemode ? !!req.query.livemode : doc.livemode,
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);