payment-kit 1.18.24 → 1.18.25

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.
@@ -13,8 +13,28 @@ export const events = new EventEmitter() as MyEventType;
13
13
 
14
14
  export const emitAsync = (event: string, ...args: any[]) => {
15
15
  return new Promise((resolve, reject) => {
16
+ const timeout = setTimeout(() => {
17
+ cleanup();
18
+ reject(new Error(`Event ${event} timed out after 10000ms`));
19
+ }, 10000);
20
+
21
+ const cleanup = () => {
22
+ clearTimeout(timeout);
23
+ events.removeListener(`${event}.done`, handleDone);
24
+ events.removeListener(`${event}.error`, handleError);
25
+ };
26
+
27
+ const handleDone = (...results: any[]) => {
28
+ cleanup();
29
+ resolve(results.length > 1 ? results : results[0]);
30
+ };
31
+
32
+ const handleError = (error: any) => {
33
+ cleanup();
34
+ reject(error);
35
+ };
36
+ events.once(`${event}.done`, handleDone);
37
+ events.once(`${event}.error`, handleError);
16
38
  events.emit(event, ...args);
17
- events.once(`${event}.done`, resolve);
18
- events.once(`${event}.error`, reject);
19
39
  });
20
40
  };
@@ -23,6 +23,7 @@ import {
23
23
  TInvoice,
24
24
  TLineItemExpanded,
25
25
  UsageRecord,
26
+ Lock,
26
27
  } from '../store/models';
27
28
  import { getConnectQueryParam } from './util';
28
29
  import { expandLineItems, getPriceUintAmountByCurrency } from './session';
@@ -37,6 +38,7 @@ import {
37
38
  import logger from './logger';
38
39
  import { ensureOverdraftProtectionPrice } from './overdraft-protection';
39
40
  import { CHARGE_SUPPORTED_CHAIN_TYPES } from './constants';
41
+ import { emitAsync } from './event';
40
42
 
41
43
  export function getCustomerInvoicePageUrl({
42
44
  invoiceId,
@@ -930,3 +932,143 @@ export async function handleOverdraftProtectionInvoiceAfterPayment(invoice: Invo
930
932
  });
931
933
  }
932
934
  }
935
+
936
+ /**
937
+ * retry uncollectible invoices
938
+ * @param options
939
+ */
940
+ export async function retryUncollectibleInvoices(options: {
941
+ customerId?: string;
942
+ subscriptionId?: string;
943
+ invoiceId?: string;
944
+ invoiceIds?: string[];
945
+ currencyId?: string;
946
+ }) {
947
+ const lockKey = `retry-uncollectible-${JSON.stringify(options)}`;
948
+
949
+ const isLocked = await Lock.isLocked(lockKey);
950
+ if (isLocked) {
951
+ logger.warn('Retry uncollectible invoices already in progress', {
952
+ lockKey,
953
+ options,
954
+ });
955
+ throw new Error('Retry already in progress');
956
+ }
957
+
958
+ try {
959
+ await Lock.acquire(lockKey, dayjs().add(5, 'minutes').unix());
960
+
961
+ const { customerId, subscriptionId, invoiceId, invoiceIds, currencyId } = options;
962
+
963
+ const where: any = {
964
+ status: { [Op.in]: ['uncollectible'] },
965
+ payment_intent_id: { [Op.ne]: null },
966
+ };
967
+
968
+ if (customerId) {
969
+ where.customer_id = customerId;
970
+ }
971
+
972
+ if (subscriptionId) {
973
+ where.subscription_id = subscriptionId;
974
+ }
975
+
976
+ if (invoiceId) {
977
+ where.id = invoiceId;
978
+ }
979
+
980
+ if (invoiceIds && invoiceIds.length > 0) {
981
+ where.id = { [Op.in]: invoiceIds };
982
+ }
983
+
984
+ if (currencyId) {
985
+ where.currency_id = currencyId;
986
+ }
987
+
988
+ const overdueInvoices = (await Invoice.findAll({
989
+ where,
990
+ include: [{ model: PaymentIntent, as: 'paymentIntent' }],
991
+ attributes: ['id', 'payment_intent_id', 'subscription_id', 'customer_id', 'created_at', 'status', 'currency_id'],
992
+ order: [['created_at', 'ASC']],
993
+ })) as (Invoice & { paymentIntent?: PaymentIntent })[];
994
+
995
+ const startTime = Date.now();
996
+ logger.info('Found uncollectible invoices to retry', {
997
+ count: overdueInvoices.length,
998
+ criteria: options,
999
+ invoiceIds: overdueInvoices.map((inv) => inv.id),
1000
+ });
1001
+
1002
+ const results = {
1003
+ processed: overdueInvoices.length,
1004
+ successful: [] as string[],
1005
+ failed: [] as Array<{ id: string; reason: string }>,
1006
+ };
1007
+
1008
+ const settledResults = await Promise.allSettled(
1009
+ overdueInvoices.map(async (invoice) => {
1010
+ const { paymentIntent } = invoice;
1011
+ if (!paymentIntent) {
1012
+ throw new Error('No payment intent found');
1013
+ }
1014
+
1015
+ await paymentIntent.update({ status: 'requires_capture' });
1016
+ await emitAsync(
1017
+ 'payment.queued',
1018
+ paymentIntent.id,
1019
+ { paymentIntentId: paymentIntent.id, retryOnError: true, ignoreMaxRetryCheck: true },
1020
+ { sync: false }
1021
+ );
1022
+
1023
+ return invoice;
1024
+ })
1025
+ );
1026
+
1027
+ settledResults.forEach((result, index) => {
1028
+ const invoice = overdueInvoices[index];
1029
+ if (!invoice) {
1030
+ return;
1031
+ }
1032
+ if (result.status === 'fulfilled') {
1033
+ results.successful.push(invoice.id);
1034
+ logger.info('Successfully queued uncollectible invoice retry', {
1035
+ invoiceId: invoice.id,
1036
+ customerId: invoice.customer_id,
1037
+ paymentIntentId: invoice.payment_intent_id,
1038
+ });
1039
+ } else {
1040
+ const error = result.reason;
1041
+ const errorType = error.name || 'Unknown';
1042
+ const errorCode = error.code || 'UNKNOWN_ERROR';
1043
+
1044
+ results.failed.push({
1045
+ id: invoice.id,
1046
+ reason: error.message || 'Unknown error',
1047
+ });
1048
+
1049
+ logger.error('Failed to queue uncollectible invoice retry', {
1050
+ invoiceId: invoice.id,
1051
+ customerId: invoice.customer_id,
1052
+ subscriptionId: invoice.subscription_id,
1053
+ paymentIntentId: invoice.payment_intent_id,
1054
+ errorType,
1055
+ errorCode,
1056
+ error,
1057
+ });
1058
+ }
1059
+ });
1060
+
1061
+ const processingTime = Date.now() - startTime;
1062
+ logger.info('Completed retrying uncollectible invoices', {
1063
+ totalProcessed: results.processed,
1064
+ successful: results.successful.length,
1065
+ failed: results.failed.length,
1066
+ processingTimeMs: processingTime,
1067
+ });
1068
+
1069
+ return results;
1070
+ } finally {
1071
+ await Lock.release(lockKey);
1072
+ logger.info('Released retry uncollectible lock', { lockKey });
1073
+ }
1074
+ }
@@ -289,16 +289,30 @@ events.on('invoice.queued', async (id, job, args = {}) => {
289
289
  job,
290
290
  ...extraArgs,
291
291
  });
292
- events.emit('invoice.queued.done');
292
+ events.emit('invoice.queued.done', { id });
293
293
  } catch (error) {
294
294
  logger.error('Error in invoice.queued', { id, job, error });
295
- events.emit('invoice.queued.error', error);
295
+ events.emit('invoice.queued.error', { id, job, error });
296
296
  }
297
297
  return;
298
298
  }
299
- invoiceQueue.push({
300
- id,
301
- job,
302
- ...extraArgs,
303
- });
299
+ try {
300
+ const existJob = await invoiceQueue.get(id);
301
+ if (existJob) {
302
+ await invoiceQueue.delete(id);
303
+ logger.info('Removed existing invoice job for immediate execution', {
304
+ id,
305
+ originalRunAt: existJob.runAt,
306
+ });
307
+ }
308
+ invoiceQueue.push({
309
+ id,
310
+ job,
311
+ ...extraArgs,
312
+ });
313
+ events.emit('invoice.queued.done', { id });
314
+ } catch (error) {
315
+ logger.error('Error in invoice.queued', { id, job, error });
316
+ events.emit('invoice.queued.error', { id, job, error });
317
+ }
304
318
  });
@@ -45,6 +45,8 @@ type PaymentJob = {
45
45
  paymentIntentId: string;
46
46
  paymentSettings?: PaymentSettings;
47
47
  retryOnError?: boolean;
48
+ ignoreMaxRetryCheck?: boolean;
49
+ immediateRetry?: boolean;
48
50
  };
49
51
 
50
52
  type DepositVaultJob = {
@@ -721,7 +723,7 @@ export const handlePayment = async (job: PaymentJob) => {
721
723
  }
722
724
 
723
725
  // check max retry before doing any hard work
724
- if (invoice && invoice.attempt_count >= MAX_RETRY_COUNT) {
726
+ if (invoice && invoice.attempt_count >= MAX_RETRY_COUNT && !job.ignoreMaxRetryCheck) {
725
727
  logger.info('PaymentIntent capture aborted since max retry exceeded', { id: paymentIntent.id });
726
728
  const updates = await handlePaymentFailed(
727
729
  paymentIntent,
@@ -882,7 +884,7 @@ export const handlePayment = async (job: PaymentJob) => {
882
884
  };
883
885
 
884
886
  if (!job.retryOnError) {
885
- // To a final state without any retry
887
+ // To a final state without any retry
886
888
  await paymentIntent.update({ status: 'requires_action', last_payment_error: error });
887
889
  if (invoice) {
888
890
  await invoice.update({
@@ -924,7 +926,6 @@ export const handlePayment = async (job: PaymentJob) => {
924
926
  }
925
927
  }
926
928
  }
927
-
928
929
  // reschedule next attempt
929
930
  const retryAt = updates.invoice.next_payment_attempt;
930
931
  if (retryAt) {
@@ -993,16 +994,31 @@ events.on('payment.queued', async (id, job, args = {}) => {
993
994
  job,
994
995
  ...extraArgs,
995
996
  });
996
- events.emit('payment.queued.done');
997
+ events.emit('payment.queued.done', { id, job });
997
998
  } catch (error) {
998
999
  logger.error('Error in payment.queued', { id, job, error });
999
- events.emit('payment.queued.error', error);
1000
+ events.emit('payment.queued.error', { id, job, error });
1000
1001
  }
1001
1002
  return;
1002
1003
  }
1003
- paymentQueue.push({
1004
- id,
1005
- job,
1006
- ...extraArgs,
1007
- });
1004
+
1005
+ try {
1006
+ const existingJob = await paymentQueue.get(id);
1007
+ if (existingJob) {
1008
+ await paymentQueue.delete(id);
1009
+ logger.info('Removed existing payment job for immediate execution', {
1010
+ id,
1011
+ originalRunAt: existingJob.runAt,
1012
+ });
1013
+ }
1014
+ paymentQueue.push({
1015
+ id,
1016
+ job,
1017
+ ...extraArgs,
1018
+ });
1019
+ events.emit('payment.queued.done', { id, job });
1020
+ } catch (error) {
1021
+ logger.error('Error in payment.queued', { id, job, error });
1022
+ events.emit('payment.queued.error', { id, job, error });
1023
+ }
1008
1024
  });
@@ -282,16 +282,30 @@ events.on('payout.queued', async (id, job, args = {}) => {
282
282
  job,
283
283
  ...extraArgs,
284
284
  });
285
- events.emit('payout.queued.done');
285
+ events.emit('payout.queued.done', { id, job });
286
286
  } catch (error) {
287
287
  logger.error('Error in payout.queued', { id, job, error });
288
- events.emit('payout.queued.error', error);
288
+ events.emit('payout.queued.error', { id, job, error });
289
289
  }
290
290
  return;
291
291
  }
292
- payoutQueue.push({
293
- id,
294
- job,
295
- ...extraArgs,
296
- });
292
+ try {
293
+ const existJob = await payoutQueue.get(id);
294
+ if (existJob) {
295
+ await payoutQueue.delete(id);
296
+ logger.info('Removed existing payout job for immediate execution', {
297
+ id,
298
+ originalRunAt: existJob.runAt,
299
+ });
300
+ }
301
+ payoutQueue.push({
302
+ id,
303
+ job,
304
+ ...extraArgs,
305
+ });
306
+ events.emit('payout.queued.done', { id, job });
307
+ } catch (error) {
308
+ logger.error('Error in payout.queued', { id, job, error });
309
+ events.emit('payout.queued.error', { id, job, error });
310
+ }
297
311
  });
@@ -856,10 +856,17 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
856
856
  });
857
857
  logger.info('customer created on checkout session submit', { did: req.user.did, id: customer.id });
858
858
  try {
859
- await blocklet.updateUserAddress({
860
- did: customer.did,
861
- address: Customer.formatAddressFromCustomer(customer),
862
- });
859
+ await blocklet.updateUserAddress(
860
+ {
861
+ did: customer.did,
862
+ address: Customer.formatAddressFromCustomer(customer),
863
+ },
864
+ {
865
+ headers: {
866
+ cookie: req.headers.cookie || '',
867
+ },
868
+ }
869
+ );
863
870
  logger.info('updateUserAddress success', {
864
871
  did: customer.did,
865
872
  });
@@ -870,14 +877,14 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
870
877
  });
871
878
  }
872
879
  } else {
873
- const updates: Record<string, string> = {};
880
+ const updates: Record<string, any> = {};
874
881
  if (checkoutSession.customer_update?.name) {
875
882
  updates.name = req.body.customer_name;
876
883
  updates.email = req.body.customer_email;
877
884
  updates.phone = req.body.customer_phone;
878
885
  }
879
886
  if (checkoutSession.customer_update?.address) {
880
- updates.address = req.body.billing_address;
887
+ updates.address = Customer.formatUpdateAddress(req.body.billing_address, customer);
881
888
  }
882
889
  if (!customer.invoice_prefix) {
883
890
  updates.invoice_prefix = Customer.getInvoicePrefix();
@@ -885,12 +892,19 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
885
892
 
886
893
  await customer.update(updates);
887
894
  try {
888
- await blocklet.updateUserAddress({
889
- did: customer.did,
890
- address: Customer.formatAddressFromCustomer(customer),
891
- // @ts-ignore
892
- phone: customer.phone,
893
- });
895
+ await blocklet.updateUserAddress(
896
+ {
897
+ did: customer.did,
898
+ address: Customer.formatAddressFromCustomer(customer),
899
+ // @ts-ignore
900
+ phone: customer.phone,
901
+ },
902
+ {
903
+ headers: {
904
+ cookie: req.headers.cookie || '',
905
+ },
906
+ }
907
+ );
894
908
  logger.info('updateUserAddress success', {
895
909
  did: customer.did,
896
910
  });
@@ -6,7 +6,7 @@ import { getGasPayerExtra } from '../../libs/payment';
6
6
  import { getTxMetadata } from '../../libs/util';
7
7
  import { ensureAccountRecharge, getAuthPrincipalClaim } from './shared';
8
8
  import logger from '../../libs/logger';
9
- import { ensureRechargeInvoice } from '../../libs/invoice';
9
+ import { ensureRechargeInvoice, retryUncollectibleInvoices } from '../../libs/invoice';
10
10
 
11
11
  export default {
12
12
  action: 'recharge-account',
@@ -86,6 +86,18 @@ export default {
86
86
  paymentMethod,
87
87
  customer!
88
88
  );
89
+ try {
90
+ retryUncollectibleInvoices({
91
+ customerId: customer.id,
92
+ currencyId: paymentCurrency.id,
93
+ });
94
+ } catch (err) {
95
+ logger.error('Failed to retry uncollectible invoices', {
96
+ error: err,
97
+ customerId: customer.id,
98
+ currencyId: paymentCurrency.id,
99
+ });
100
+ }
89
101
  };
90
102
  if (paymentMethod.type === 'arcblock') {
91
103
  try {
@@ -8,7 +8,7 @@ import { getGasPayerExtra } from '../../libs/payment';
8
8
  import { getTxMetadata } from '../../libs/util';
9
9
  import { ensureSubscriptionRecharge, getAuthPrincipalClaim } from './shared';
10
10
  import logger from '../../libs/logger';
11
- import { ensureRechargeInvoice } from '../../libs/invoice';
11
+ import { ensureRechargeInvoice, retryUncollectibleInvoices } from '../../libs/invoice';
12
12
  import { EVMChainType } from '../../store/models';
13
13
  import { EVM_CHAIN_TYPES } from '../../libs/constants';
14
14
 
@@ -99,6 +99,18 @@ export default {
99
99
  paymentMethod,
100
100
  customer!
101
101
  );
102
+ try {
103
+ retryUncollectibleInvoices({
104
+ currencyId: paymentCurrency.id,
105
+ subscriptionId,
106
+ });
107
+ } catch (err) {
108
+ logger.error('Failed to retry uncollectible invoices', {
109
+ error: err,
110
+ currencyId: paymentCurrency.id,
111
+ subscriptionId,
112
+ });
113
+ }
102
114
  };
103
115
  if (paymentMethod.type === 'arcblock') {
104
116
  try {
@@ -1105,6 +1105,29 @@ export async function ensureSubscriptionForOverdraftProtection(subscriptionId: s
1105
1105
  };
1106
1106
  }
1107
1107
 
1108
+
1109
+ async function executeSingleTransaction(
1110
+ client: any,
1111
+ claim: any,
1112
+ type: 'Delegate' | 'Stake',
1113
+ userDid: string,
1114
+ userPk: string,
1115
+ gasPayerHeaders: Record<string, string>
1116
+ ): Promise<string> {
1117
+ if (!claim) return '';
1118
+
1119
+ const tx: Partial<Transaction> = client.decodeTx(claim.finalTx || claim.origin);
1120
+ if (claim.sig) {
1121
+ tx.signature = claim.sig;
1122
+ }
1123
+
1124
+ const { buffer } = await client[`encode${type}Tx`]({ tx });
1125
+ return client[`send${type}Tx`](
1126
+ { tx, wallet: fromPublicKey(userPk, toTypeInfo(userDid)) },
1127
+ getGasPayerExtra(buffer, gasPayerHeaders)
1128
+ );
1129
+ }
1130
+
1108
1131
  export async function executeOcapTransactions(
1109
1132
  userDid: string,
1110
1133
  userPk: string,
@@ -1116,16 +1139,14 @@ export async function executeOcapTransactions(
1116
1139
  nonce?: string
1117
1140
  ) {
1118
1141
  const client = paymentMethod.getOcapClient();
1119
- logger.info('start executeOcapTransactions', claims);
1120
- const delegation = claims.find((x) => x.type === 'signature' && x.meta?.purpose === 'delegation');
1121
- const staking = claims.find((x) => x.type === 'prepareTx' && x.meta?.purpose === 'staking');
1122
- const transactions = [
1123
- [delegation, 'Delegate'],
1124
- [staking, 'Stake'],
1125
- ];
1126
-
1127
- const stakingAmount =
1128
- staking?.requirement?.tokens?.find((x: any) => x.address === paymentCurrencyContract)?.value || '0';
1142
+ logger.info('start executeOcapTransactions', { userDid, claims });
1143
+
1144
+ const delegation = claims.find(x => x.type === 'signature' && x.meta?.purpose === 'delegation');
1145
+ const staking = claims.find(x => x.type === 'prepareTx' && x.meta?.purpose === 'staking');
1146
+
1147
+ const stakingAmount = staking?.requirement?.tokens?.find(
1148
+ (x: any) => x?.address === paymentCurrencyContract
1149
+ )?.value || '0';
1129
1150
 
1130
1151
  try {
1131
1152
  const getHeaders = (index: number): Record<string, string> => {
@@ -1145,30 +1166,27 @@ export async function executeOcapTransactions(
1145
1166
  return {};
1146
1167
  };
1147
1168
 
1148
- const [delegationTxHash, stakingTxHash] = await Promise.all(
1149
- transactions.map(async ([claim, type], index) => {
1150
- if (!claim) {
1151
- return '';
1152
- }
1153
-
1154
- const tx: Partial<Transaction> = client.decodeTx(claim.finalTx || claim.origin);
1155
- if (claim.sig) {
1156
- tx.signature = claim.sig;
1157
- }
1158
-
1159
- // @ts-ignore
1160
- const { buffer } = await client[`encode${type}Tx`]({ tx });
1161
- const gasPayerHeaders = getHeaders(index);
1162
- // @ts-ignore
1163
- const txHash = await client[`send${type}Tx`](
1164
- // @ts-ignore
1165
- { tx, wallet: fromPublicKey(userPk, toTypeInfo(userDid)) },
1166
- getGasPayerExtra(buffer, gasPayerHeaders)
1167
- );
1168
-
1169
- return txHash;
1170
- })
1171
- );
1169
+ const transactions = [
1170
+ { claim: delegation, type: 'Delegate' },
1171
+ { claim: staking, type: 'Stake' }
1172
+ ];
1173
+
1174
+ const txHashes = [];
1175
+ for (let i = 0; i < transactions.length; i++) {
1176
+ const { claim, type } = transactions[i]!;
1177
+ // eslint-disable-next-line no-await-in-loop
1178
+ const hash = await executeSingleTransaction(
1179
+ client,
1180
+ claim,
1181
+ type as 'Delegate' | 'Stake',
1182
+ userDid,
1183
+ userPk,
1184
+ getHeaders(i)
1185
+ );
1186
+ txHashes.push(hash);
1187
+ }
1188
+
1189
+ const [delegationTxHash, stakingTxHash] = txHashes;
1172
1190
 
1173
1191
  return {
1174
1192
  tx_hash: delegationTxHash,
@@ -1176,12 +1194,12 @@ export async function executeOcapTransactions(
1176
1194
  type: 'delegate',
1177
1195
  staking: {
1178
1196
  tx_hash: stakingTxHash,
1179
- address: await getCustomerStakeAddress(userDid, nonce || subscriptionId || ''),
1197
+ address: await getCustomerStakeAddress(userDid, nonce || subscriptionId || ''),
1180
1198
  },
1181
1199
  stakingAmount,
1182
1200
  };
1183
1201
  } catch (err) {
1184
- logger.error('executeOcapTransactions failed', err);
1202
+ logger.error('executeOcapTransactions failed', { error: err, userDid });
1185
1203
  throw err;
1186
1204
  }
1187
1205
  }
@@ -21,7 +21,7 @@ import { PaymentMethod } from '../store/models/payment-method';
21
21
  import { Price } from '../store/models/price';
22
22
  import { Product } from '../store/models/product';
23
23
  import { Subscription } from '../store/models/subscription';
24
- import { getReturnStakeInvoices, getStakingInvoices } from '../libs/invoice';
24
+ import { getReturnStakeInvoices, getStakingInvoices, retryUncollectibleInvoices } from '../libs/invoice';
25
25
  import { CheckoutSession, PaymentLink, TInvoiceExpanded } from '../store/models';
26
26
  import logger from '../libs/logger';
27
27
 
@@ -253,6 +253,56 @@ router.get('/search', authMine, async (req, res) => {
253
253
  res.json({ count, list, paging: { page, pageSize } });
254
254
  });
255
255
 
256
+ const retryUncollectibleSchema = Joi.object({
257
+ customerId: Joi.string().trim().allow('').optional(),
258
+ subscriptionId: Joi.string().trim().allow('').optional(),
259
+ invoiceId: Joi.string().trim().allow('').optional(),
260
+ invoiceIds: Joi.alternatives()
261
+ .try(
262
+ Joi.array().items(Joi.string().trim()),
263
+ Joi.string()
264
+ .trim()
265
+ .custom((value) => {
266
+ if (!value) return undefined;
267
+ return value
268
+ .split(',')
269
+ .map((id: string) => id.trim())
270
+ .filter(Boolean);
271
+ })
272
+ )
273
+ .optional(),
274
+ currencyId: Joi.string().trim().allow('').optional(),
275
+ });
276
+ router.get('/retry-uncollectible', authAdmin, async (req, res) => {
277
+ try {
278
+ const { error, value } = retryUncollectibleSchema.validate(req.query, {
279
+ stripUnknown: true,
280
+ });
281
+
282
+ if (error) {
283
+ return res.status(400).json({ error: error.message });
284
+ }
285
+
286
+ const { customerId, subscriptionId, invoiceId, invoiceIds, currencyId } = value;
287
+
288
+ const result = await retryUncollectibleInvoices({
289
+ customerId,
290
+ subscriptionId,
291
+ invoiceId,
292
+ invoiceIds,
293
+ currencyId,
294
+ });
295
+
296
+ return res.json(result);
297
+ } catch (error) {
298
+ logger.error('Failed to retry uncollectible invoices', { error });
299
+ return res.status(500).json({
300
+ error: 'Failed to retry uncollectible invoices',
301
+ message: error.message,
302
+ });
303
+ }
304
+ });
305
+
256
306
  router.get('/:id', authPortal, async (req, res) => {
257
307
  try {
258
308
  const doc = (await Invoice.findOne({
@@ -12,6 +12,7 @@ import {
12
12
  Op,
13
13
  } from 'sequelize';
14
14
 
15
+ import merge from 'lodash/merge';
15
16
  import { createEvent } from '../../libs/audit';
16
17
  import CustomError from '../../libs/error';
17
18
  import { getLock } from '../../libs/lock';
@@ -304,6 +305,32 @@ export class Customer extends Model<InferAttributes<Customer>, InferCreationAttr
304
305
  postalCode: customer.address?.postal_code || '',
305
306
  };
306
307
  }
308
+
309
+ public static formatUpdateAddress(address: CustomerAddress, customer: Customer): CustomerAddress {
310
+ const defaultAddress = {
311
+ country: 'us',
312
+ state: '',
313
+ city: '',
314
+ line1: '',
315
+ line2: '',
316
+ postal_code: '',
317
+ };
318
+
319
+ return merge(
320
+ defaultAddress,
321
+ customer.address || {},
322
+ address
323
+ ? {
324
+ country: address.country?.toLowerCase(),
325
+ state: address.state,
326
+ city: address.city,
327
+ line1: address.line1,
328
+ line2: address.line2,
329
+ postal_code: address.postal_code,
330
+ }
331
+ : {}
332
+ );
333
+ }
307
334
  }
308
335
 
309
336
  export type TCustomer = InferAttributes<Customer>;
package/blocklet.yml CHANGED
@@ -14,7 +14,7 @@ repository:
14
14
  type: git
15
15
  url: git+https://github.com/blocklet/payment-kit.git
16
16
  specVersion: 1.2.8
17
- version: 1.18.24
17
+ version: 1.18.25
18
18
  logo: logo.png
19
19
  files:
20
20
  - dist
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "payment-kit",
3
- "version": "1.18.24",
3
+ "version": "1.18.25",
4
4
  "scripts": {
5
5
  "dev": "blocklet dev --open",
6
6
  "eject": "vite eject",
@@ -43,19 +43,19 @@
43
43
  ]
44
44
  },
45
45
  "dependencies": {
46
- "@abtnode/cron": "^1.16.40",
46
+ "@abtnode/cron": "^1.16.41",
47
47
  "@arcblock/did": "^1.19.15",
48
48
  "@arcblock/did-auth-storage-nedb": "^1.7.1",
49
- "@arcblock/did-connect": "^2.12.48",
49
+ "@arcblock/did-connect": "^2.12.52",
50
50
  "@arcblock/did-util": "^1.19.15",
51
51
  "@arcblock/jwt": "^1.19.15",
52
- "@arcblock/ux": "^2.12.48",
52
+ "@arcblock/ux": "^2.12.52",
53
53
  "@arcblock/validator": "^1.19.15",
54
- "@blocklet/js-sdk": "^1.16.40",
55
- "@blocklet/logger": "^1.16.40",
56
- "@blocklet/payment-react": "1.18.24",
57
- "@blocklet/sdk": "^1.16.40",
58
- "@blocklet/ui-react": "^2.12.48",
54
+ "@blocklet/js-sdk": "^1.16.41",
55
+ "@blocklet/logger": "^1.16.41",
56
+ "@blocklet/payment-react": "1.18.25",
57
+ "@blocklet/sdk": "^1.16.41",
58
+ "@blocklet/ui-react": "^2.12.52",
59
59
  "@blocklet/uploader": "^0.1.81",
60
60
  "@blocklet/xss": "^0.1.30",
61
61
  "@mui/icons-material": "^5.16.6",
@@ -119,9 +119,9 @@
119
119
  "web3": "^4.16.0"
120
120
  },
121
121
  "devDependencies": {
122
- "@abtnode/types": "^1.16.40",
122
+ "@abtnode/types": "^1.16.41",
123
123
  "@arcblock/eslint-config-ts": "^0.3.3",
124
- "@blocklet/payment-types": "1.18.24",
124
+ "@blocklet/payment-types": "1.18.25",
125
125
  "@types/cookie-parser": "^1.4.7",
126
126
  "@types/cors": "^2.8.17",
127
127
  "@types/debug": "^4.1.12",
@@ -167,5 +167,5 @@
167
167
  "parser": "typescript"
168
168
  }
169
169
  },
170
- "gitHead": "6c36b088743e696ce250cda11f8502b3886df0ef"
170
+ "gitHead": "1665ec667d621a3e607650e486cb0967371b9771"
171
171
  }
@@ -200,7 +200,7 @@ const isCardVisible = (type: string, config: any, data: any, currency: any, meth
200
200
  data?.summary?.[summaryKey]?.[currency.id] && data?.summary?.[summaryKey]?.[currency.id] !== '0';
201
201
 
202
202
  if (type === 'balance') {
203
- return method?.type === 'arcblock' && (config.alwaysShow || hasSummaryValue);
203
+ return method?.type === 'arcblock' || config.alwaysShow || hasSummaryValue;
204
204
  }
205
205
 
206
206
  return config.alwaysShow || hasSummaryValue;
@@ -26,12 +26,13 @@ import {
26
26
  api,
27
27
  formatBNStr,
28
28
  formatPrice,
29
+ formatNumber,
29
30
  } from '@blocklet/payment-react';
30
31
  import type { TPaymentCurrency, TPaymentMethod } from '@blocklet/payment-types';
31
32
  import { joinURL } from 'ufo';
32
33
  import { AccountBalanceWalletOutlined, ArrowBackOutlined, ArrowForwardOutlined } from '@mui/icons-material';
33
34
  import Empty from '@arcblock/ux/lib/Empty';
34
- import { BN } from '@ocap/util';
35
+ import { BN, fromUnitToToken } from '@ocap/util';
35
36
  import RechargeList from '../../../components/invoice/recharge';
36
37
  import { getTokenBalanceLink, goBackOrFallback } from '../../../libs/util';
37
38
  import { useSessionContext } from '../../../contexts/session';
@@ -120,14 +121,19 @@ export default function BalanceRechargePage() {
120
121
  if (data.recommendedRecharge && data.recommendedRecharge.amount && data.recommendedRecharge.amount !== '0') {
121
122
  const baseAmount = data.recommendedRecharge.amount;
122
123
  const decimal = data.currency.decimal || 0;
124
+ const calcCycleAmount = (cycle: number) => {
125
+ const cycleAmount = fromUnitToToken(new BN(baseAmount).mul(new BN(String(cycle))).toString(), decimal);
126
+ return Math.ceil(parseFloat(cycleAmount)).toString();
127
+ };
123
128
  setUnitCycle({
124
- amount: parseFloat(formatBNStr(baseAmount, decimal, 6, true)).toString(),
129
+ amount: fromUnitToToken(baseAmount, decimal),
125
130
  interval: data.recommendedRecharge.interval as TimeUnit,
126
131
  cycle: data.recommendedRecharge.cycle,
127
132
  });
133
+
128
134
  setPresetAmounts([
129
135
  {
130
- amount: Math.ceil(parseFloat(formatBNStr(baseAmount, decimal, 6, true))).toString(),
136
+ amount: calcCycleAmount(1),
131
137
  multiplier: data.recommendedRecharge.cycle,
132
138
  label: t('common.estimatedDuration', {
133
139
  duration: formatSmartDuration(1, data.recommendedRecharge.interval as TimeUnit, {
@@ -136,9 +142,7 @@ export default function BalanceRechargePage() {
136
142
  }),
137
143
  },
138
144
  {
139
- amount: Math.ceil(
140
- parseFloat(formatBNStr(new BN(baseAmount).mul(new BN('4')).toString(), decimal, 6, true))
141
- ).toString(),
145
+ amount: calcCycleAmount(4),
142
146
  multiplier: data.recommendedRecharge.cycle * 4,
143
147
  label: t('common.estimatedDuration', {
144
148
  duration: formatSmartDuration(4, data.recommendedRecharge.interval as TimeUnit, {
@@ -147,9 +151,7 @@ export default function BalanceRechargePage() {
147
151
  }),
148
152
  },
149
153
  {
150
- amount: Math.ceil(
151
- parseFloat(formatBNStr(new BN(baseAmount).mul(new BN('8')).toString(), decimal, 6, true))
152
- ).toString(),
154
+ amount: calcCycleAmount(8),
153
155
  multiplier: data.recommendedRecharge.cycle * 8,
154
156
  label: t('common.estimatedDuration', {
155
157
  duration: formatSmartDuration(8, data.recommendedRecharge.interval as TimeUnit, {
@@ -442,7 +444,7 @@ export default function BalanceRechargePage() {
442
444
  fontWeight: 600,
443
445
  color: amount === presetAmount && !customAmount ? 'primary.main' : 'text.primary',
444
446
  }}>
445
- {presetAmount} {currency.symbol}
447
+ {formatNumber(presetAmount)} {currency.symbol}
446
448
  </Typography>
447
449
  {multiplier > 0 && label && (
448
450
  <Typography variant="caption" align="center" color="text.secondary">
@@ -1,6 +1,6 @@
1
1
  /* eslint-disable react/no-unstable-nested-components */
2
2
  import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
3
- import DidAddress from '@arcblock/ux/lib/DID';
3
+ import DID from '@arcblock/ux/lib/DID';
4
4
  import {
5
5
  Status,
6
6
  api,
@@ -63,9 +63,9 @@ const fetchSubscriptionData = (id: string, authToken: string): Promise<TSubscrip
63
63
  return api.get(`/api/subscriptions/${id}?authToken=${authToken}`).then((res) => res.data);
64
64
  };
65
65
 
66
- const checkHasPastDue = async (subscriptionId: string): Promise<boolean> => {
66
+ const checkHasPastDue = async (subscriptionId: string, authToken: string): Promise<boolean> => {
67
67
  try {
68
- const res = await api.get(`/api/subscriptions/${subscriptionId}/summary`);
68
+ const res = await api.get(`/api/subscriptions/${subscriptionId}/summary?authToken=${authToken}`);
69
69
  if (!isEmpty(res.data) && Object.keys(res.data).length >= 1) {
70
70
  return true;
71
71
  }
@@ -126,9 +126,12 @@ export default function SubscriptionEmbed() {
126
126
  });
127
127
  }, [subscription]);
128
128
 
129
- const { data: hasPastDue, runAsync: runCheckHasPastDue } = useRequest(() => checkHasPastDue(subscriptionId), {
130
- refreshDeps: [subscriptionId],
131
- });
129
+ const { data: hasPastDue, runAsync: runCheckHasPastDue } = useRequest(
130
+ () => checkHasPastDue(subscriptionId, authToken),
131
+ {
132
+ refreshDeps: [subscriptionId, authToken],
133
+ }
134
+ );
132
135
 
133
136
  if (error) {
134
137
  return (
@@ -212,11 +215,20 @@ export default function SubscriptionEmbed() {
212
215
  logo={getCustomerAvatar(
213
216
  subscription.customer.did,
214
217
  subscription.customer.updated_at ? new Date(subscription.customer.updated_at).toISOString() : '',
215
- 48
218
+ 24
216
219
  )}
217
- name={`${subscription.customer.name} (${subscription.customer.email})`}
218
- description={<DidAddress did={subscription.customer.did} responsive={false} compact />}
220
+ description=""
219
221
  className="owner-info-card"
222
+ name={subscription.customer.name || subscription.customer.email}
223
+ tooltip={
224
+ <Stack>
225
+ <Typography>
226
+ {subscription.customer.name} ({subscription.customer.email})
227
+ </Typography>
228
+ <DID did={subscription.customer.did} />
229
+ </Stack>
230
+ }
231
+ size={24}
220
232
  />
221
233
  ),
222
234
  });
@@ -254,6 +266,9 @@ export default function SubscriptionEmbed() {
254
266
  '.info-row-value': {
255
267
  flex: 'none',
256
268
  },
269
+ '.owner-info-card .info-card': {
270
+ minWidth: 0,
271
+ },
257
272
  }}
258
273
  />
259
274
  );