payment-kit 1.18.38 → 1.18.40
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/integrations/stripe/handlers/invoice.ts +22 -0
- package/api/src/integrations/stripe/handlers/payment-intent.ts +9 -1
- package/api/src/integrations/stripe/handlers/subscription.ts +137 -1
- package/api/src/libs/subscription.ts +107 -97
- package/api/src/queues/payment.ts +4 -0
- package/api/src/routes/checkout-sessions.ts +3 -1
- package/api/src/routes/subscriptions.ts +27 -18
- package/blocklet.yml +1 -1
- package/package.json +19 -19
- package/scripts/sdk.js +27 -1
- package/src/components/customer/link.tsx +6 -0
- package/src/components/info-card.tsx +2 -2
- package/src/components/info-metric.tsx +1 -1
- package/src/components/metadata/list.tsx +4 -2
- package/src/components/subscription/description.tsx +8 -6
- package/src/components/subscription/portal/actions.tsx +224 -45
- package/src/components/subscription/portal/list.tsx +153 -74
- package/src/components/subscription/status.tsx +17 -5
- package/src/locales/en.tsx +12 -7
- package/src/locales/zh.tsx +6 -2
- package/src/pages/admin/billing/invoices/detail.tsx +1 -5
- package/src/pages/admin/billing/subscriptions/detail.tsx +1 -5
- package/src/pages/admin/customers/customers/detail.tsx +1 -5
- package/src/pages/admin/developers/events/detail.tsx +1 -5
- package/src/pages/admin/developers/index.tsx +1 -1
- package/src/pages/admin/developers/webhooks/detail.tsx +1 -3
- package/src/pages/admin/overview.tsx +4 -4
- package/src/pages/admin/payments/intents/detail.tsx +5 -6
- package/src/pages/admin/payments/payouts/detail.tsx +1 -5
- package/src/pages/admin/payments/refunds/detail.tsx +1 -5
- package/src/pages/admin/products/links/detail.tsx +1 -5
- package/src/pages/admin/products/prices/detail.tsx +1 -5
- package/src/pages/admin/products/pricing-tables/detail.tsx +1 -5
- package/src/pages/admin/products/products/detail.tsx +1 -5
- package/src/pages/admin/settings/payment-methods/index.tsx +1 -1
- package/src/pages/customer/index.tsx +67 -138
- package/src/pages/customer/payout/detail.tsx +37 -49
- package/src/pages/customer/subscription/change-payment.tsx +2 -35
- package/src/pages/customer/subscription/detail.tsx +5 -6
- package/src/pages/integrations/donations/index.tsx +1 -1
- package/src/pages/integrations/overview.tsx +1 -1
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
TEventExpanded,
|
|
20
20
|
TInvoiceItem,
|
|
21
21
|
} from '../../../store/models';
|
|
22
|
+
import { handleSubscriptionOnPaymentFailure } from './subscription';
|
|
22
23
|
|
|
23
24
|
export async function handleStripeInvoicePaid(invoice: Invoice, event: TEventExpanded) {
|
|
24
25
|
logger.info('invoice paid on stripe event', { locale: invoice.id });
|
|
@@ -91,6 +92,16 @@ export async function syncStripeInvoice(invoice: Invoice) {
|
|
|
91
92
|
)
|
|
92
93
|
);
|
|
93
94
|
logger.info('stripe invoice synced', { locale: invoice.id, remote: stripeInvoice.id });
|
|
95
|
+
const failedStatuses = ['uncollectible', 'finalization_failed', 'payment_failed'];
|
|
96
|
+
if (failedStatuses.includes(stripeInvoice.status || '')) {
|
|
97
|
+
logger.info('Invoice failed', { invoiceId: invoice.id });
|
|
98
|
+
if (invoice.subscription_id) {
|
|
99
|
+
const subscription = await Subscription.findByPk(invoice.subscription_id);
|
|
100
|
+
if (subscription) {
|
|
101
|
+
handleSubscriptionOnPaymentFailure(subscription, 'syncStripeInvoice', client);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
94
105
|
}
|
|
95
106
|
}
|
|
96
107
|
|
|
@@ -376,4 +387,15 @@ export async function handleInvoiceEvent(event: TEventExpanded, client: Stripe)
|
|
|
376
387
|
await invoice.update({ status: 'payment_failed' });
|
|
377
388
|
logger.info('invoice payment failed on stripe event', { locale: invoice.id });
|
|
378
389
|
}
|
|
390
|
+
|
|
391
|
+
const failedEvents = ['invoice.marked_uncollectible', 'invoice.finalization_failed', 'invoice.payment_failed'];
|
|
392
|
+
if (failedEvents.includes(event.type)) {
|
|
393
|
+
logger.info('Invoice failed', { invoiceId: invoice.id });
|
|
394
|
+
if (invoice.subscription_id) {
|
|
395
|
+
const subscription = await Subscription.findByPk(invoice.subscription_id);
|
|
396
|
+
if (subscription) {
|
|
397
|
+
handleSubscriptionOnPaymentFailure(subscription, event.type, client);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
379
401
|
}
|
|
@@ -7,9 +7,10 @@ import type Stripe from 'stripe';
|
|
|
7
7
|
import dayjs from '../../../libs/dayjs';
|
|
8
8
|
import logger from '../../../libs/logger';
|
|
9
9
|
import { handlePaymentSucceed } from '../../../queues/payment';
|
|
10
|
-
import { Invoice, PaymentIntent, PaymentMethod, TEventExpanded } from '../../../store/models';
|
|
10
|
+
import { Invoice, PaymentIntent, PaymentMethod, Subscription, TEventExpanded } from '../../../store/models';
|
|
11
11
|
import { handleStripeInvoiceCreated } from './invoice';
|
|
12
12
|
import { events } from '../../../libs/event';
|
|
13
|
+
import { handleSubscriptionOnPaymentFailure } from './subscription';
|
|
13
14
|
|
|
14
15
|
export async function handleStripePaymentSucceed(paymentIntent: PaymentIntent, event?: TEventExpanded) {
|
|
15
16
|
const triggerRenew = paymentIntent.status !== 'succeeded';
|
|
@@ -184,6 +185,13 @@ export async function handlePaymentIntentEvent(event: TEventExpanded, client: St
|
|
|
184
185
|
attempted: true,
|
|
185
186
|
status_transitions: { ...invoice.status_transitions, marked_uncollectible_at: dayjs().unix() },
|
|
186
187
|
});
|
|
188
|
+
|
|
189
|
+
if (invoice.subscription_id) {
|
|
190
|
+
const subscription = await Subscription.findByPk(invoice.subscription_id);
|
|
191
|
+
if (subscription) {
|
|
192
|
+
handleSubscriptionOnPaymentFailure(subscription, event.type, client);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
187
195
|
}
|
|
188
196
|
}
|
|
189
197
|
}
|
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
import pick from 'lodash/pick';
|
|
2
2
|
import type Stripe from 'stripe';
|
|
3
3
|
|
|
4
|
+
import dayjs from '../../../libs/dayjs';
|
|
4
5
|
import logger from '../../../libs/logger';
|
|
5
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
finalizeStripeSubscriptionUpdate,
|
|
8
|
+
getDaysUntilCancel,
|
|
9
|
+
getDaysUntilDue,
|
|
10
|
+
getDueUnit,
|
|
11
|
+
shouldCancelSubscription,
|
|
12
|
+
} from '../../../libs/subscription';
|
|
6
13
|
import { CheckoutSession, PaymentMethod, Subscription, TEventExpanded } from '../../../store/models';
|
|
7
14
|
import { getCheckoutSessionSubscriptionIds } from '../../../libs/session';
|
|
8
15
|
import { createEvent } from '../../../libs/audit';
|
|
@@ -139,3 +146,132 @@ export async function handleSubscriptionEvent(event: TEventExpanded, _: Stripe)
|
|
|
139
146
|
}
|
|
140
147
|
}
|
|
141
148
|
}
|
|
149
|
+
|
|
150
|
+
export async function syncStripeSubscriptionAfterRecovery(subscription: Subscription, paymentIntentId: string) {
|
|
151
|
+
if (!subscription.payment_details?.stripe?.subscription_id) {
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
try {
|
|
156
|
+
const method = await PaymentMethod.findByPk(subscription.default_payment_method_id);
|
|
157
|
+
if (method && method.type === 'stripe') {
|
|
158
|
+
const client = await method.getStripeClient();
|
|
159
|
+
await client.subscriptions.update(subscription.payment_details.stripe.subscription_id, {
|
|
160
|
+
cancel_at_period_end: false,
|
|
161
|
+
cancel_at: null,
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
logger.info('Removed cancellation settings in Stripe after payment success', {
|
|
165
|
+
subscription: subscription.id,
|
|
166
|
+
stripeSubscription: subscription.payment_details.stripe.subscription_id,
|
|
167
|
+
paymentIntent: paymentIntentId,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
} catch (err) {
|
|
171
|
+
logger.error('Failed to update Stripe subscription after payment success', {
|
|
172
|
+
error: err,
|
|
173
|
+
subscription: subscription.id,
|
|
174
|
+
paymentIntent: paymentIntentId,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export async function handleSubscriptionOnPaymentFailure(
|
|
180
|
+
subscription: Subscription,
|
|
181
|
+
eventType: string,
|
|
182
|
+
client: Stripe
|
|
183
|
+
) {
|
|
184
|
+
if (!subscription || !subscription.isActive()) {
|
|
185
|
+
logger.warn('Subscription is not active or not found', { subscription: subscription.id });
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const now = dayjs().unix();
|
|
190
|
+
const { interval } = subscription.pending_invoice_item_interval;
|
|
191
|
+
const dueUnit = getDueUnit(interval);
|
|
192
|
+
const cancelUpdates: { [key: string]: any } = {};
|
|
193
|
+
const daysUntilCancel = getDaysUntilCancel(subscription);
|
|
194
|
+
const cancelSubscription = shouldCancelSubscription(subscription);
|
|
195
|
+
|
|
196
|
+
if (daysUntilCancel > 0) {
|
|
197
|
+
cancelUpdates.cancel_at = subscription.current_period_start + daysUntilCancel * dueUnit;
|
|
198
|
+
} else {
|
|
199
|
+
cancelUpdates.cancel_at_period_end = true;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (cancelSubscription) {
|
|
203
|
+
await subscription.update({
|
|
204
|
+
status: 'canceled',
|
|
205
|
+
canceled_at: now,
|
|
206
|
+
cancelation_details: {
|
|
207
|
+
comment: 'exceed_current_period',
|
|
208
|
+
feedback: 'other',
|
|
209
|
+
reason: 'payment_failed',
|
|
210
|
+
},
|
|
211
|
+
});
|
|
212
|
+
logger.warn(`[${eventType}] Subscription moved to canceled after payment failed`, {
|
|
213
|
+
subscription: subscription.id,
|
|
214
|
+
});
|
|
215
|
+
} else {
|
|
216
|
+
// check grace period
|
|
217
|
+
const daysUntilDue = getDaysUntilDue(subscription);
|
|
218
|
+
if (typeof daysUntilDue === 'number') {
|
|
219
|
+
const gracePeriodStart = subscription.current_period_start;
|
|
220
|
+
const graceDuration = daysUntilDue ? daysUntilDue * dueUnit : 0;
|
|
221
|
+
|
|
222
|
+
if (gracePeriodStart + graceDuration <= now) {
|
|
223
|
+
// past grace period, set to past_due
|
|
224
|
+
await subscription.update({
|
|
225
|
+
status: 'past_due',
|
|
226
|
+
...cancelUpdates,
|
|
227
|
+
cancelation_details: {
|
|
228
|
+
comment: 'past_due',
|
|
229
|
+
feedback: 'other',
|
|
230
|
+
reason: 'payment_failed',
|
|
231
|
+
},
|
|
232
|
+
});
|
|
233
|
+
logger.warn(`[${eventType}] Subscription moved to past_due after payment failed`, {
|
|
234
|
+
subscription: subscription.id,
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// sync to stripe
|
|
241
|
+
if (subscription.payment_details?.stripe?.subscription_id && client) {
|
|
242
|
+
try {
|
|
243
|
+
const method = await PaymentMethod.findByPk(subscription.default_payment_method_id);
|
|
244
|
+
if (method && method.type === 'stripe') {
|
|
245
|
+
const stripeUpdates: any = {
|
|
246
|
+
cancellation_details: {
|
|
247
|
+
comment: 'past_due',
|
|
248
|
+
feedback: 'other',
|
|
249
|
+
reason: 'payment_failed',
|
|
250
|
+
},
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
if (cancelUpdates.cancel_at) {
|
|
254
|
+
stripeUpdates.cancel_at = cancelUpdates.cancel_at;
|
|
255
|
+
} else if (cancelUpdates.cancel_at_period_end) {
|
|
256
|
+
stripeUpdates.cancel_at_period_end = true;
|
|
257
|
+
}
|
|
258
|
+
if (cancelSubscription) {
|
|
259
|
+
stripeUpdates.cancel_at = now;
|
|
260
|
+
stripeUpdates.cancel_at_period_end = false;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
await client.subscriptions.update(subscription.payment_details.stripe.subscription_id, stripeUpdates);
|
|
264
|
+
logger.info(`[${eventType}] Updated subscription in Stripe after payment failed`, {
|
|
265
|
+
subscription: subscription.id,
|
|
266
|
+
stripeSubscription: subscription.payment_details.stripe.subscription_id,
|
|
267
|
+
updates: stripeUpdates,
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
} catch (err) {
|
|
271
|
+
logger.error(`[${eventType}] Failed to update subscription in Stripe`, {
|
|
272
|
+
error: err,
|
|
273
|
+
subscription: subscription.id,
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
@@ -1354,126 +1354,136 @@ export async function slashOverdraftProtectionStake(subscription: Subscription,
|
|
|
1354
1354
|
|
|
1355
1355
|
let slashAmount = new BN(0);
|
|
1356
1356
|
const slashInvoices: Invoice[] = [];
|
|
1357
|
-
const
|
|
1357
|
+
for (const invoice of invoices) {
|
|
1358
1358
|
if (!invoice.payment_intent_id) {
|
|
1359
1359
|
await emitAsync(
|
|
1360
1360
|
'invoice.queued',
|
|
1361
1361
|
invoice.id,
|
|
1362
1362
|
{ invoiceId: invoice.id, retryOnError: false, justCreate: true },
|
|
1363
|
-
{
|
|
1364
|
-
sync: true,
|
|
1365
|
-
}
|
|
1363
|
+
{ sync: true }
|
|
1366
1364
|
);
|
|
1367
1365
|
await invoice.reload();
|
|
1368
1366
|
}
|
|
1367
|
+
|
|
1369
1368
|
const paymentIntent = await PaymentIntent.findByPk(invoice.payment_intent_id);
|
|
1370
1369
|
if (paymentIntent) {
|
|
1371
|
-
|
|
1370
|
+
const isAlreadyPaid = paymentIntent.status === 'succeeded' && invoice.status === 'paid';
|
|
1371
|
+
|
|
1372
|
+
if (!isAlreadyPaid) {
|
|
1373
|
+
const tmp = slashAmount.add(new BN(invoice.amount_remaining));
|
|
1374
|
+
if (tmp.lte(new BN(remainingStake))) {
|
|
1375
|
+
slashAmount = tmp;
|
|
1376
|
+
slashInvoices.push(invoice);
|
|
1377
|
+
} else {
|
|
1378
|
+
break;
|
|
1379
|
+
}
|
|
1380
|
+
} else {
|
|
1372
1381
|
logger.info('PaymentIntent and Invoice already updated', {
|
|
1373
1382
|
subscription: subscription.id,
|
|
1374
1383
|
paymentIntent: paymentIntent.id,
|
|
1375
1384
|
invoice: invoice.id,
|
|
1376
1385
|
});
|
|
1377
|
-
return;
|
|
1378
1386
|
}
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
const slashedInvoiceIds: string[] = [];
|
|
1391
|
+
const processInvoice = async (invoice: Invoice): Promise<void> => {
|
|
1392
|
+
const paymentIntent = await PaymentIntent.findByPk(invoice.payment_intent_id);
|
|
1393
|
+
if (!paymentIntent || (paymentIntent.status === 'succeeded' && invoice.status === 'paid')) return;
|
|
1394
|
+
|
|
1395
|
+
try {
|
|
1396
|
+
const stakeEnough = await checkRemainingStake(
|
|
1397
|
+
paymentMethod,
|
|
1398
|
+
paymentCurrency,
|
|
1399
|
+
address,
|
|
1400
|
+
invoice.amount_remaining
|
|
1401
|
+
);
|
|
1402
|
+
if (!stakeEnough.enough) {
|
|
1403
|
+
throw new Error(`Stake slashing aborted because no enough staking for invoice: ${invoice.id}`);
|
|
1404
|
+
}
|
|
1405
|
+
// do the slash
|
|
1406
|
+
const signed = await client.signSlashStakeTx({
|
|
1407
|
+
tx: {
|
|
1408
|
+
itx: {
|
|
1388
1409
|
address,
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
}
|
|
1394
|
-
// do the slash
|
|
1395
|
-
const signed = await client.signSlashStakeTx({
|
|
1396
|
-
tx: {
|
|
1397
|
-
itx: {
|
|
1398
|
-
address,
|
|
1399
|
-
outputs: [
|
|
1400
|
-
{
|
|
1401
|
-
owner: wallet.address,
|
|
1402
|
-
tokens: [{ address: paymentCurrency.contract, value: invoice.amount_remaining }],
|
|
1403
|
-
},
|
|
1404
|
-
],
|
|
1405
|
-
message: 'overdraft_exceeded',
|
|
1406
|
-
data: {
|
|
1407
|
-
typeUrl: 'json',
|
|
1408
|
-
// @ts-ignore
|
|
1409
|
-
value: {
|
|
1410
|
-
appId: wallet.address,
|
|
1411
|
-
reason: 'overdraft_exceeded',
|
|
1412
|
-
subscriptionId: subscription.id,
|
|
1413
|
-
invoiceId: invoice.id,
|
|
1414
|
-
paymentIntentId: paymentIntent.id,
|
|
1415
|
-
},
|
|
1416
|
-
},
|
|
1410
|
+
outputs: [
|
|
1411
|
+
{
|
|
1412
|
+
owner: wallet.address,
|
|
1413
|
+
tokens: [{ address: paymentCurrency.contract, value: invoice.amount_remaining }],
|
|
1417
1414
|
},
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
capture_method: 'manual',
|
|
1430
|
-
last_payment_error: null,
|
|
1431
|
-
payment_details: {
|
|
1432
|
-
arcblock: {
|
|
1433
|
-
tx_hash: txHash,
|
|
1434
|
-
payer:
|
|
1435
|
-
subscription.overdraft_protection?.payment_details?.arcblock?.payer ||
|
|
1436
|
-
getSubscriptionPaymentAddress(subscription, 'arcblock'),
|
|
1437
|
-
type: 'slash',
|
|
1415
|
+
],
|
|
1416
|
+
message: 'overdraft_exceeded',
|
|
1417
|
+
data: {
|
|
1418
|
+
typeUrl: 'json',
|
|
1419
|
+
// @ts-ignore
|
|
1420
|
+
value: {
|
|
1421
|
+
appId: wallet.address,
|
|
1422
|
+
reason: 'overdraft_exceeded',
|
|
1423
|
+
subscriptionId: subscription.id,
|
|
1424
|
+
invoiceId: invoice.id,
|
|
1425
|
+
paymentIntentId: paymentIntent.id,
|
|
1438
1426
|
},
|
|
1439
1427
|
},
|
|
1440
|
-
}
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1428
|
+
},
|
|
1429
|
+
},
|
|
1430
|
+
wallet,
|
|
1431
|
+
});
|
|
1432
|
+
// @ts-ignore
|
|
1433
|
+
const { buffer } = await client.encodeSlashStakeTx({ tx: signed });
|
|
1434
|
+
// @ts-ignore
|
|
1435
|
+
const txHash = await client.sendSlashStakeTx({ tx: signed, wallet }, getGasPayerExtra(buffer));
|
|
1436
|
+
|
|
1437
|
+
await paymentIntent.update({
|
|
1438
|
+
status: 'succeeded',
|
|
1439
|
+
amount_received: invoice.amount_remaining,
|
|
1440
|
+
capture_method: 'manual',
|
|
1441
|
+
last_payment_error: null,
|
|
1442
|
+
payment_details: {
|
|
1443
|
+
arcblock: {
|
|
1444
|
+
tx_hash: txHash,
|
|
1445
|
+
payer:
|
|
1446
|
+
subscription.overdraft_protection?.payment_details?.arcblock?.payer ||
|
|
1447
|
+
getSubscriptionPaymentAddress(subscription, 'arcblock'),
|
|
1448
|
+
type: 'slash',
|
|
1449
|
+
},
|
|
1450
|
+
},
|
|
1451
|
+
});
|
|
1452
|
+
logger.info('PaymentIntent updated after stake slash', {
|
|
1453
|
+
subscription: subscription.id,
|
|
1454
|
+
paymentIntent: paymentIntent.id,
|
|
1455
|
+
status: 'succeeded',
|
|
1456
|
+
});
|
|
1457
|
+
|
|
1458
|
+
await invoice.update({
|
|
1459
|
+
status: 'paid',
|
|
1460
|
+
paid: true,
|
|
1461
|
+
amount_paid: invoice.amount_remaining,
|
|
1462
|
+
amount_remaining: '0',
|
|
1463
|
+
attempted: true,
|
|
1464
|
+
attempt_count: invoice.attempt_count + 1,
|
|
1465
|
+
status_transitions: { ...invoice.status_transitions, paid_at: dayjs().unix() },
|
|
1466
|
+
});
|
|
1467
|
+
logger.info('Invoice updated after stake slash', {
|
|
1468
|
+
subscription: subscription.id,
|
|
1469
|
+
invoice: invoice.id,
|
|
1470
|
+
status: 'paid',
|
|
1471
|
+
});
|
|
1472
|
+
slashedInvoiceIds.push(invoice.id);
|
|
1473
|
+
} catch (updateError) {
|
|
1474
|
+
logger.error('stake slash failed', {
|
|
1475
|
+
subscription: subscription.id,
|
|
1476
|
+
invoice: invoice.id,
|
|
1477
|
+
error: updateError,
|
|
1478
|
+
});
|
|
1479
|
+
throw updateError;
|
|
1470
1480
|
}
|
|
1471
|
-
}
|
|
1481
|
+
};
|
|
1472
1482
|
|
|
1473
|
-
await Promise.all(
|
|
1474
|
-
logger.info(`${
|
|
1483
|
+
await Promise.all(slashInvoices.map(processInvoice));
|
|
1484
|
+
logger.info(`${slashedInvoiceIds.length} invoices updated after stake slash`, {
|
|
1475
1485
|
subscription: subscription.id,
|
|
1476
|
-
invoices:
|
|
1486
|
+
invoices: slashedInvoiceIds,
|
|
1477
1487
|
slashAmount: slashAmount.toString(),
|
|
1478
1488
|
});
|
|
1479
1489
|
} catch (error) {
|
|
@@ -41,6 +41,7 @@ import { ensureOverdraftProtectionPrice } from '../libs/overdraft-protection';
|
|
|
41
41
|
import createQueue from '../libs/queue';
|
|
42
42
|
import { CHARGE_SUPPORTED_CHAIN_TYPES, EVM_CHAIN_TYPES } from '../libs/constants';
|
|
43
43
|
import { getCheckoutSessionSubscriptionIds, getSubscriptionCreateSetup } from '../libs/session';
|
|
44
|
+
import { syncStripeSubscriptionAfterRecovery } from '../integrations/stripe/handlers/subscription';
|
|
44
45
|
|
|
45
46
|
type PaymentJob = {
|
|
46
47
|
paymentIntentId: string;
|
|
@@ -246,6 +247,7 @@ export const handlePaymentSucceed = async (
|
|
|
246
247
|
cancelation_details: null,
|
|
247
248
|
});
|
|
248
249
|
logger.info(`Subscription ${subscription.id} recovered on payment done ${paymentIntent.id}: cancel rest`);
|
|
250
|
+
await syncStripeSubscriptionAfterRecovery(subscription, paymentIntent.id);
|
|
249
251
|
return;
|
|
250
252
|
}
|
|
251
253
|
const subscriptionItems = await SubscriptionItem.findAll({ where: { subscription_id: subscription.id } });
|
|
@@ -267,11 +269,13 @@ export const handlePaymentSucceed = async (
|
|
|
267
269
|
logger.info(
|
|
268
270
|
`Subscription ${subscription.id} recovered on payment done ${paymentIntent.id}: cancel and billing cycle reset`
|
|
269
271
|
);
|
|
272
|
+
await syncStripeSubscriptionAfterRecovery(subscription, paymentIntent.id);
|
|
270
273
|
} else if (subscription.cancel_at_period_end) {
|
|
271
274
|
// reset cancel_at_period_end if we are recovering from payment failed
|
|
272
275
|
// @ts-ignore
|
|
273
276
|
await subscription.update({ status: 'active', cancel_at_period_end: false, cancelation_details: null });
|
|
274
277
|
logger.info(`Subscription ${subscription.id} recovered on payment done ${paymentIntent.id}: cancel reset`);
|
|
278
|
+
await syncStripeSubscriptionAfterRecovery(subscription, paymentIntent.id);
|
|
275
279
|
} else {
|
|
276
280
|
await subscription.update({ status: 'active' });
|
|
277
281
|
logger.info(`Subscription ${subscription.id} recovered on payment done ${paymentIntent.id}`);
|
|
@@ -346,7 +346,9 @@ const SubscriptionDataSchema = Joi.object({
|
|
|
346
346
|
color: Joi.string().valid('primary', 'secondary', 'success', 'error', 'warning').optional(),
|
|
347
347
|
variant: Joi.string().valid('text', 'contained', 'outlined').optional(),
|
|
348
348
|
text: Joi.object().required(),
|
|
349
|
-
link: Joi.
|
|
349
|
+
link: Joi.alternatives()
|
|
350
|
+
.try(Joi.string().uri(), Joi.string().pattern(/^\/[^\s]*$/))
|
|
351
|
+
.required(),
|
|
350
352
|
type: Joi.string().valid('notification', 'custom').optional(),
|
|
351
353
|
triggerEvents: Joi.array().items(Joi.string()).optional(),
|
|
352
354
|
})
|
|
@@ -691,7 +691,9 @@ const updateSchema = Joi.object<{
|
|
|
691
691
|
color: Joi.string().valid('primary', 'secondary', 'success', 'error', 'warning').optional(),
|
|
692
692
|
variant: Joi.string().valid('text', 'contained', 'outlined').optional(),
|
|
693
693
|
text: Joi.object().required(),
|
|
694
|
-
link: Joi.
|
|
694
|
+
link: Joi.alternatives()
|
|
695
|
+
.try(Joi.string().uri(), Joi.string().pattern(/^\/[^\s]*$/))
|
|
696
|
+
.required(),
|
|
695
697
|
type: Joi.string().valid('notification', 'custom').optional(),
|
|
696
698
|
triggerEvents: Joi.array().items(Joi.string()).optional(),
|
|
697
699
|
})
|
|
@@ -1742,30 +1744,37 @@ router.get('/:id/cycle-amount', authPortal, async (req, res) => {
|
|
|
1742
1744
|
if (!currency) {
|
|
1743
1745
|
return res.status(404).json({ error: 'Currency not found' });
|
|
1744
1746
|
}
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1747
|
+
try {
|
|
1748
|
+
// get upcoming invoice
|
|
1749
|
+
const result = await getUpcomingInvoiceAmount(subscription.id);
|
|
1750
|
+
// get past invoices
|
|
1751
|
+
const pastMaxAmount = await getPastInvoicesAmount(subscription.id, 'max');
|
|
1749
1752
|
|
|
1750
|
-
|
|
1751
|
-
|
|
1753
|
+
// return max amount
|
|
1754
|
+
const nextAmount = new BN(result.amount === '0' ? result.minExpectedAmount : result.amount).toString();
|
|
1752
1755
|
|
|
1753
|
-
|
|
1756
|
+
const maxAmount = new BN(pastMaxAmount.amount).lte(new BN(nextAmount)) ? nextAmount : pastMaxAmount.amount;
|
|
1754
1757
|
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1758
|
+
if (req.query?.overdraftProtection) {
|
|
1759
|
+
const { price } = await ensureOverdraftProtectionPrice(subscription.livemode);
|
|
1760
|
+
const invoicePrice = (price?.currency_options || []).find(
|
|
1761
|
+
(x: any) => x.currency_id === subscription?.currency_id
|
|
1762
|
+
);
|
|
1763
|
+
const gas = invoicePrice?.unit_amount;
|
|
1764
|
+
return res.json({
|
|
1765
|
+
amount: new BN(maxAmount).add(new BN(gas)).toString(),
|
|
1766
|
+
gas,
|
|
1767
|
+
currency,
|
|
1768
|
+
});
|
|
1769
|
+
}
|
|
1759
1770
|
return res.json({
|
|
1760
|
-
amount:
|
|
1761
|
-
gas,
|
|
1771
|
+
amount: maxAmount,
|
|
1762
1772
|
currency,
|
|
1763
1773
|
});
|
|
1774
|
+
} catch (err) {
|
|
1775
|
+
logger.error(err);
|
|
1776
|
+
return res.status(400).json({ error: err.message });
|
|
1764
1777
|
}
|
|
1765
|
-
return res.json({
|
|
1766
|
-
amount: maxAmount,
|
|
1767
|
-
currency,
|
|
1768
|
-
});
|
|
1769
1778
|
});
|
|
1770
1779
|
|
|
1771
1780
|
// slash stake
|
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.40",
|
|
4
4
|
"scripts": {
|
|
5
5
|
"dev": "blocklet dev --open",
|
|
6
6
|
"eject": "vite eject",
|
|
@@ -45,30 +45,30 @@
|
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
47
|
"@abtnode/cron": "^1.16.42",
|
|
48
|
-
"@arcblock/did": "^1.20.
|
|
48
|
+
"@arcblock/did": "^1.20.6",
|
|
49
49
|
"@arcblock/did-auth-storage-nedb": "^1.7.1",
|
|
50
|
-
"@arcblock/did-connect": "^2.13.
|
|
51
|
-
"@arcblock/did-util": "^1.20.
|
|
52
|
-
"@arcblock/jwt": "^1.20.
|
|
53
|
-
"@arcblock/ux": "^2.13.
|
|
54
|
-
"@arcblock/validator": "^1.20.
|
|
55
|
-
"@blocklet/did-space-js": "^1.0.
|
|
50
|
+
"@arcblock/did-connect": "^2.13.24",
|
|
51
|
+
"@arcblock/did-util": "^1.20.6",
|
|
52
|
+
"@arcblock/jwt": "^1.20.6",
|
|
53
|
+
"@arcblock/ux": "^2.13.24",
|
|
54
|
+
"@arcblock/validator": "^1.20.6",
|
|
55
|
+
"@blocklet/did-space-js": "^1.0.51",
|
|
56
56
|
"@blocklet/js-sdk": "^1.16.42",
|
|
57
57
|
"@blocklet/logger": "^1.16.42",
|
|
58
|
-
"@blocklet/payment-react": "1.18.
|
|
58
|
+
"@blocklet/payment-react": "1.18.40",
|
|
59
59
|
"@blocklet/sdk": "^1.16.42",
|
|
60
|
-
"@blocklet/ui-react": "^2.13.
|
|
61
|
-
"@blocklet/uploader": "^0.1.
|
|
62
|
-
"@blocklet/xss": "^0.1.
|
|
60
|
+
"@blocklet/ui-react": "^2.13.24",
|
|
61
|
+
"@blocklet/uploader": "^0.1.89",
|
|
62
|
+
"@blocklet/xss": "^0.1.34",
|
|
63
63
|
"@mui/icons-material": "^5.16.6",
|
|
64
64
|
"@mui/lab": "^5.0.0-alpha.173",
|
|
65
65
|
"@mui/material": "^5.16.6",
|
|
66
66
|
"@mui/system": "^5.16.6",
|
|
67
|
-
"@ocap/asset": "^1.20.
|
|
68
|
-
"@ocap/client": "^1.20.
|
|
69
|
-
"@ocap/mcrypto": "^1.20.
|
|
70
|
-
"@ocap/util": "^1.20.
|
|
71
|
-
"@ocap/wallet": "^1.20.
|
|
67
|
+
"@ocap/asset": "^1.20.6",
|
|
68
|
+
"@ocap/client": "^1.20.6",
|
|
69
|
+
"@ocap/mcrypto": "^1.20.6",
|
|
70
|
+
"@ocap/util": "^1.20.6",
|
|
71
|
+
"@ocap/wallet": "^1.20.6",
|
|
72
72
|
"@stripe/react-stripe-js": "^2.7.3",
|
|
73
73
|
"@stripe/stripe-js": "^2.4.0",
|
|
74
74
|
"ahooks": "^3.8.0",
|
|
@@ -123,7 +123,7 @@
|
|
|
123
123
|
"devDependencies": {
|
|
124
124
|
"@abtnode/types": "^1.16.42",
|
|
125
125
|
"@arcblock/eslint-config-ts": "^0.3.3",
|
|
126
|
-
"@blocklet/payment-types": "1.18.
|
|
126
|
+
"@blocklet/payment-types": "1.18.40",
|
|
127
127
|
"@types/cookie-parser": "^1.4.7",
|
|
128
128
|
"@types/cors": "^2.8.17",
|
|
129
129
|
"@types/debug": "^4.1.12",
|
|
@@ -169,5 +169,5 @@
|
|
|
169
169
|
"parser": "typescript"
|
|
170
170
|
}
|
|
171
171
|
},
|
|
172
|
-
"gitHead": "
|
|
172
|
+
"gitHead": "53abc664b88898a7cb9afa494f4ace333b1afe9c"
|
|
173
173
|
}
|