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.
- package/api/src/libs/event.ts +22 -2
- package/api/src/libs/invoice.ts +142 -0
- package/api/src/queues/invoice.ts +21 -7
- package/api/src/queues/payment.ts +26 -10
- package/api/src/queues/payout.ts +21 -7
- package/api/src/routes/checkout-sessions.ts +26 -12
- package/api/src/routes/connect/recharge-account.ts +13 -1
- package/api/src/routes/connect/recharge.ts +13 -1
- package/api/src/routes/connect/shared.ts +54 -36
- package/api/src/routes/invoices.ts +51 -1
- package/api/src/store/models/customer.ts +27 -0
- package/blocklet.yml +1 -1
- package/package.json +12 -12
- package/src/pages/customer/index.tsx +1 -1
- package/src/pages/customer/recharge/account.tsx +12 -10
- package/src/pages/customer/subscription/embed.tsx +24 -9
package/api/src/libs/event.ts
CHANGED
|
@@ -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
|
};
|
package/api/src/libs/invoice.ts
CHANGED
|
@@ -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
|
-
|
|
300
|
-
id
|
|
301
|
-
|
|
302
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
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
|
});
|
package/api/src/queues/payout.ts
CHANGED
|
@@ -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
|
-
|
|
293
|
-
id
|
|
294
|
-
|
|
295
|
-
|
|
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
|
-
|
|
861
|
-
|
|
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,
|
|
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
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
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
|
-
|
|
1121
|
-
const
|
|
1122
|
-
const
|
|
1123
|
-
|
|
1124
|
-
|
|
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
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
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:
|
|
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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "payment-kit",
|
|
3
|
-
"version": "1.18.
|
|
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.
|
|
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.
|
|
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.
|
|
52
|
+
"@arcblock/ux": "^2.12.52",
|
|
53
53
|
"@arcblock/validator": "^1.19.15",
|
|
54
|
-
"@blocklet/js-sdk": "^1.16.
|
|
55
|
-
"@blocklet/logger": "^1.16.
|
|
56
|
-
"@blocklet/payment-react": "1.18.
|
|
57
|
-
"@blocklet/sdk": "^1.16.
|
|
58
|
-
"@blocklet/ui-react": "^2.12.
|
|
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.
|
|
122
|
+
"@abtnode/types": "^1.16.41",
|
|
123
123
|
"@arcblock/eslint-config-ts": "^0.3.3",
|
|
124
|
-
"@blocklet/payment-types": "1.18.
|
|
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": "
|
|
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'
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
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(
|
|
130
|
-
|
|
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
|
-
|
|
218
|
+
24
|
|
216
219
|
)}
|
|
217
|
-
|
|
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
|
);
|