payment-kit 1.15.33 → 1.15.35
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/setup-intent.ts +3 -1
- package/api/src/integrations/stripe/handlers/subscription.ts +2 -8
- package/api/src/integrations/stripe/resource.ts +0 -11
- package/api/src/libs/invoice.ts +202 -1
- package/api/src/libs/notification/template/subscription-canceled.ts +15 -2
- package/api/src/libs/notification/template/subscription-renew-failed.ts +1 -1
- package/api/src/libs/notification/template/subscription-renewed.ts +1 -1
- package/api/src/libs/notification/template/subscription-trial-will-end.ts +9 -5
- package/api/src/libs/notification/template/subscription-will-canceled.ts +9 -5
- package/api/src/libs/notification/template/subscription-will-renew.ts +10 -12
- package/api/src/libs/payment.ts +3 -2
- package/api/src/libs/refund.ts +4 -0
- package/api/src/libs/subscription.ts +58 -14
- package/api/src/queues/invoice.ts +1 -0
- package/api/src/queues/payment.ts +3 -1
- package/api/src/queues/refund.ts +9 -8
- package/api/src/queues/subscription.ts +111 -40
- package/api/src/routes/checkout-sessions.ts +22 -6
- package/api/src/routes/connect/change-payment.ts +51 -34
- package/api/src/routes/connect/change-plan.ts +25 -3
- package/api/src/routes/connect/recharge.ts +28 -3
- package/api/src/routes/connect/setup.ts +27 -6
- package/api/src/routes/connect/shared.ts +223 -1
- package/api/src/routes/connect/subscribe.ts +25 -3
- package/api/src/routes/customers.ts +2 -2
- package/api/src/routes/invoices.ts +27 -105
- package/api/src/routes/payment-links.ts +3 -0
- package/api/src/routes/refunds.ts +22 -1
- package/api/src/routes/subscriptions.ts +112 -21
- package/api/src/routes/webhook-attempts.ts +14 -1
- package/api/src/store/models/invoice.ts +3 -1
- package/blocklet.yml +1 -1
- package/package.json +4 -4
- package/src/app.tsx +3 -1
- package/src/components/invoice/list.tsx +83 -31
- package/src/components/invoice/recharge.tsx +244 -0
- package/src/components/payment-intent/actions.tsx +2 -1
- package/src/components/payment-link/actions.tsx +6 -6
- package/src/components/payment-link/item.tsx +53 -18
- package/src/components/pricing-table/actions.tsx +14 -3
- package/src/components/pricing-table/payment-settings.tsx +1 -1
- package/src/components/refund/actions.tsx +43 -1
- package/src/components/refund/list.tsx +1 -1
- package/src/components/subscription/actions/cancel.tsx +10 -7
- package/src/components/subscription/metrics.tsx +1 -1
- package/src/components/subscription/portal/actions.tsx +22 -1
- package/src/components/subscription/portal/list.tsx +1 -0
- package/src/components/webhook/attempts.tsx +19 -121
- package/src/components/webhook/request-info.tsx +139 -0
- package/src/locales/en.tsx +4 -0
- package/src/locales/zh.tsx +8 -0
- package/src/pages/admin/billing/invoices/detail.tsx +15 -0
- package/src/pages/admin/billing/invoices/index.tsx +1 -1
- package/src/pages/admin/billing/subscriptions/detail.tsx +12 -4
- package/src/pages/admin/customers/customers/detail.tsx +1 -0
- package/src/pages/admin/payments/refunds/detail.tsx +2 -2
- package/src/pages/admin/products/links/create.tsx +4 -1
- package/src/pages/customer/index.tsx +1 -1
- package/src/pages/customer/invoice/detail.tsx +34 -14
- package/src/pages/customer/recharge.tsx +45 -35
- package/src/pages/customer/subscription/change-plan.tsx +8 -1
- package/src/pages/customer/subscription/detail.tsx +12 -22
- package/src/pages/customer/subscription/embed.tsx +3 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type Stripe from 'stripe';
|
|
2
2
|
|
|
3
3
|
import logger from '../../../libs/logger';
|
|
4
|
-
import { CheckoutSession, SetupIntent, Subscription, TEventExpanded } from '../../../store/models';
|
|
4
|
+
import { CheckoutSession, Lock, SetupIntent, Subscription, TEventExpanded } from '../../../store/models';
|
|
5
5
|
|
|
6
6
|
async function handleSubscriptionOnSetupSucceeded(event: TEventExpanded, stripeIntentId: string) {
|
|
7
7
|
const subscription = await Subscription.findOne({
|
|
@@ -69,6 +69,8 @@ async function handleSetupIntentOnSetupSucceeded(event: TEventExpanded, stripeIn
|
|
|
69
69
|
payment_method_options: {},
|
|
70
70
|
},
|
|
71
71
|
});
|
|
72
|
+
// lock the subscription to prevent concurrent change plan
|
|
73
|
+
await Lock.acquire(`${subscription.id}-change-plan`, subscription.current_period_end);
|
|
72
74
|
logger.info('subscription payment changed to stripe on stripe setup intent succeeded', {
|
|
73
75
|
subscriptionId: subscription.id,
|
|
74
76
|
setupIntentId: setupIntent.id,
|
|
@@ -70,19 +70,13 @@ export async function handleSubscriptionEvent(event: TEventExpanded, _: Stripe)
|
|
|
70
70
|
return;
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
const fields = [
|
|
74
|
-
'cancel_at',
|
|
75
|
-
'cancel_at_period_end',
|
|
76
|
-
'canceled_at',
|
|
77
|
-
'current_period_start',
|
|
78
|
-
'current_period_end',
|
|
79
|
-
'trial_end',
|
|
80
|
-
];
|
|
73
|
+
const fields = ['cancel_at', 'cancel_at_period_end', 'canceled_at', 'current_period_start', 'current_period_end'];
|
|
81
74
|
if (subscription.payment_settings?.payment_method_types?.includes('stripe')) {
|
|
82
75
|
fields.push('pause_collection');
|
|
83
76
|
}
|
|
84
77
|
if (subscription.status === 'trialing' && event.data.object.status === 'active') {
|
|
85
78
|
fields.push('status');
|
|
79
|
+
fields.push('trial_end');
|
|
86
80
|
}
|
|
87
81
|
|
|
88
82
|
await finalizeStripeSubscriptionUpdate({
|
|
@@ -289,18 +289,7 @@ export async function ensureStripeSubscription(
|
|
|
289
289
|
}
|
|
290
290
|
})
|
|
291
291
|
);
|
|
292
|
-
|
|
293
|
-
await internal.update({
|
|
294
|
-
payment_details: {
|
|
295
|
-
stripe: {
|
|
296
|
-
customer_id: customer.id,
|
|
297
|
-
subscription_id: stripeSubscription.id,
|
|
298
|
-
setup_intent_id: stripeSubscription.pending_setup_intent?.id,
|
|
299
|
-
},
|
|
300
|
-
},
|
|
301
|
-
});
|
|
302
292
|
}
|
|
303
|
-
|
|
304
293
|
return stripeSubscription;
|
|
305
294
|
}
|
|
306
295
|
|
package/api/src/libs/invoice.ts
CHANGED
|
@@ -3,20 +3,25 @@ import type { LiteralUnion } from 'type-fest';
|
|
|
3
3
|
import { withQuery } from 'ufo';
|
|
4
4
|
|
|
5
5
|
import { fromUnitToToken } from '@ocap/util';
|
|
6
|
+
import { Op } from 'sequelize';
|
|
7
|
+
import { cloneDeep, pick } from 'lodash';
|
|
6
8
|
import {
|
|
9
|
+
Customer,
|
|
7
10
|
Invoice,
|
|
8
11
|
InvoiceItem,
|
|
9
12
|
PaymentCurrency,
|
|
10
13
|
PaymentMethod,
|
|
11
14
|
Price,
|
|
12
15
|
Product,
|
|
16
|
+
Refund,
|
|
17
|
+
SetupIntent,
|
|
13
18
|
Subscription,
|
|
14
19
|
SubscriptionItem,
|
|
15
20
|
UsageRecord,
|
|
16
21
|
} from '../store/models';
|
|
17
22
|
import { getConnectQueryParam } from './util';
|
|
18
23
|
import { expandLineItems } from './session';
|
|
19
|
-
import { getSubscriptionCycleAmount, getSubscriptionCycleSetup } from './subscription';
|
|
24
|
+
import { getSubscriptionCycleAmount, getSubscriptionCycleSetup, getSubscriptionStakeAmountSetup } from './subscription';
|
|
20
25
|
|
|
21
26
|
export function getCustomerInvoicePageUrl({
|
|
22
27
|
invoiceId,
|
|
@@ -144,3 +149,199 @@ export async function getInvoiceShouldPayTotal(invoice: Invoice) {
|
|
|
144
149
|
return invoice.total;
|
|
145
150
|
}
|
|
146
151
|
}
|
|
152
|
+
|
|
153
|
+
export async function getReturnStakeInvoices(subscription: Subscription) {
|
|
154
|
+
const method = await PaymentMethod.findOne({ where: { type: 'arcblock', livemode: subscription.livemode } });
|
|
155
|
+
const returnStakeInvoice: Invoice[] = [];
|
|
156
|
+
const refunds = await Refund.findAll({
|
|
157
|
+
where: { subscription_id: subscription.id, status: 'succeeded', type: 'stake_return' },
|
|
158
|
+
include: [
|
|
159
|
+
{ model: Invoice, as: 'invoice' },
|
|
160
|
+
{ model: Customer, as: 'customer' },
|
|
161
|
+
{ model: PaymentCurrency, as: 'paymentCurrency' },
|
|
162
|
+
],
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
await Promise.all(
|
|
166
|
+
refunds.map(async (r: any) => {
|
|
167
|
+
const invoice =
|
|
168
|
+
r.invoice ||
|
|
169
|
+
(await Invoice.findOne({
|
|
170
|
+
where: {
|
|
171
|
+
subscription_id: subscription.id,
|
|
172
|
+
currency_id: r.currency_id,
|
|
173
|
+
},
|
|
174
|
+
order: [['created_at', 'ASC']],
|
|
175
|
+
}));
|
|
176
|
+
returnStakeInvoice.push({
|
|
177
|
+
id: r.id,
|
|
178
|
+
status: 'paid',
|
|
179
|
+
billing_reason: 'return_stake',
|
|
180
|
+
description: 'Return Subscription staking',
|
|
181
|
+
total: r.amount,
|
|
182
|
+
amount_due: '0',
|
|
183
|
+
amount_paid: r.amount,
|
|
184
|
+
amount_remaining: '0',
|
|
185
|
+
subscription_id: subscription.id,
|
|
186
|
+
paid: true,
|
|
187
|
+
...pick(invoice, ['number', 'auto_advance', 'period_start', 'period_end']),
|
|
188
|
+
...pick(r, ['created_at', 'updated_at', 'currency_id', 'customer_id']),
|
|
189
|
+
// @ts-ignore
|
|
190
|
+
paymentCurrency: r.paymentCurrency,
|
|
191
|
+
paymentMethod: method,
|
|
192
|
+
customer: r.customer,
|
|
193
|
+
metadata: {
|
|
194
|
+
payment_details: {
|
|
195
|
+
arcblock: {
|
|
196
|
+
tx_hash: r?.payment_details?.arcblock?.tx_hash,
|
|
197
|
+
payer: r?.payment_details?.arcblock?.payer,
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
})
|
|
203
|
+
);
|
|
204
|
+
return returnStakeInvoice;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export async function getSetupInvoice(
|
|
208
|
+
subscription: Subscription,
|
|
209
|
+
paymentMethod: PaymentMethod,
|
|
210
|
+
currencyId: string,
|
|
211
|
+
stakingProps: { tx_hash: string; payer: string; address: string }
|
|
212
|
+
) {
|
|
213
|
+
const currency = await PaymentCurrency.findByPk(currencyId);
|
|
214
|
+
if (!currency) {
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
if (!stakingProps?.tx_hash) {
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
if (paymentMethod.type !== 'arcblock') {
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
const firstInvoice = await Invoice.findOne({
|
|
224
|
+
where: { subscription_id: subscription.id, currency_id: currency.id },
|
|
225
|
+
order: [['created_at', 'ASC']],
|
|
226
|
+
include: [{ model: Customer, as: 'customer' }],
|
|
227
|
+
});
|
|
228
|
+
if (!firstInvoice) {
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
const stakeAmountResult = await getSubscriptionStakeAmountSetup(subscription, paymentMethod, stakingProps.tx_hash);
|
|
232
|
+
// @ts-ignore
|
|
233
|
+
const stakeAmount = stakeAmountResult?.[currency?.contract] || '0';
|
|
234
|
+
return {
|
|
235
|
+
id: stakingProps.address as string,
|
|
236
|
+
status: 'paid',
|
|
237
|
+
description: 'Subscription staking',
|
|
238
|
+
billing_reason: 'stake',
|
|
239
|
+
total: stakeAmount,
|
|
240
|
+
amount_due: '0',
|
|
241
|
+
amount_paid: stakeAmount,
|
|
242
|
+
amount_remaining: '0',
|
|
243
|
+
...pick(firstInvoice, [
|
|
244
|
+
'number',
|
|
245
|
+
'paid',
|
|
246
|
+
'auto_advance',
|
|
247
|
+
'currency_id',
|
|
248
|
+
'customer_id',
|
|
249
|
+
'subscription_id',
|
|
250
|
+
'period_start',
|
|
251
|
+
'period_end',
|
|
252
|
+
'created_at',
|
|
253
|
+
'updated_at',
|
|
254
|
+
]),
|
|
255
|
+
// @ts-ignore
|
|
256
|
+
paymentCurrency: currency,
|
|
257
|
+
paymentMethod,
|
|
258
|
+
// @ts-ignore
|
|
259
|
+
customer: firstInvoice?.customer,
|
|
260
|
+
metadata: {
|
|
261
|
+
payment_details: {
|
|
262
|
+
arcblock: {
|
|
263
|
+
tx_hash: stakingProps?.tx_hash,
|
|
264
|
+
payer: stakingProps?.payer,
|
|
265
|
+
},
|
|
266
|
+
},
|
|
267
|
+
},
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export async function getStakingInvoices(subscription: Subscription): Promise<Invoice[]> {
|
|
272
|
+
const method = await PaymentMethod.findOne({ where: { type: 'arcblock', livemode: subscription.livemode } });
|
|
273
|
+
if (!method) {
|
|
274
|
+
return [];
|
|
275
|
+
}
|
|
276
|
+
const stakeInvoices = await Invoice.findAll({
|
|
277
|
+
where: { subscription_id: subscription.id, billing_reason: 'stake', status: 'paid' },
|
|
278
|
+
include: [
|
|
279
|
+
{ model: Customer, as: 'customer' },
|
|
280
|
+
{ model: PaymentCurrency, as: 'paymentCurrency' },
|
|
281
|
+
{ model: PaymentMethod, as: 'paymentMethod' },
|
|
282
|
+
],
|
|
283
|
+
});
|
|
284
|
+
const invoices = cloneDeep(stakeInvoices);
|
|
285
|
+
const setups = await SetupIntent.findAll({
|
|
286
|
+
where: {
|
|
287
|
+
customer_id: subscription.customer_id,
|
|
288
|
+
payment_method_id: method?.id,
|
|
289
|
+
status: 'succeeded',
|
|
290
|
+
[Op.and]: [
|
|
291
|
+
{ metadata: { [Op.not]: null } },
|
|
292
|
+
{ 'metadata.subscription_id': subscription.id },
|
|
293
|
+
{ setup_details: { [Op.not]: null } },
|
|
294
|
+
{ 'setup_details.arcblock': { [Op.not]: null } },
|
|
295
|
+
{ 'setup_details.arcblock.staking': { [Op.not]: null } },
|
|
296
|
+
] as any,
|
|
297
|
+
},
|
|
298
|
+
});
|
|
299
|
+
if (setups.length === 0) {
|
|
300
|
+
// No payment change, use the first invoice
|
|
301
|
+
if (stakeInvoices.length > 0) {
|
|
302
|
+
return stakeInvoices;
|
|
303
|
+
}
|
|
304
|
+
const newInvoice = await getSetupInvoice(subscription, method, subscription.currency_id, {
|
|
305
|
+
tx_hash: subscription.payment_details?.arcblock?.staking?.tx_hash!,
|
|
306
|
+
payer: subscription.payment_details?.arcblock?.payer!,
|
|
307
|
+
address: subscription.payment_details?.arcblock?.staking?.address!,
|
|
308
|
+
});
|
|
309
|
+
// @ts-ignore
|
|
310
|
+
return [newInvoice].filter(Boolean);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
await Promise.all(
|
|
314
|
+
setups.map(async (setup: SetupIntent, index: number) => {
|
|
315
|
+
if (
|
|
316
|
+
index === 0 &&
|
|
317
|
+
setup.metadata?.from_currency &&
|
|
318
|
+
!stakeInvoices.find((x) => x.currency_id === setup?.metadata?.from_currency)
|
|
319
|
+
) {
|
|
320
|
+
const paymentDetails =
|
|
321
|
+
setup?.metadata?.from_payment_details?.arcblock || subscription.payment_details?.arcblock;
|
|
322
|
+
const fromMethod = await PaymentMethod.findByPk(setup?.metadata?.from_payment_method_id);
|
|
323
|
+
const newInvoice = await getSetupInvoice(subscription, fromMethod!, setup.metadata?.from_currency, {
|
|
324
|
+
tx_hash: paymentDetails?.staking?.tx_hash!,
|
|
325
|
+
payer: paymentDetails?.payer!,
|
|
326
|
+
address: paymentDetails?.staking?.address!,
|
|
327
|
+
});
|
|
328
|
+
if (newInvoice) {
|
|
329
|
+
// @ts-ignore
|
|
330
|
+
invoices.push(newInvoice);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
if (setup.metadata?.to_currency && !stakeInvoices.find((x) => x.currency_id === setup?.metadata?.to_currency)) {
|
|
334
|
+
const newInvoice = await getSetupInvoice(subscription, method, setup.metadata?.to_currency, {
|
|
335
|
+
tx_hash: setup.setup_details?.arcblock?.staking?.tx_hash!,
|
|
336
|
+
payer: setup.setup_details?.arcblock?.payer!,
|
|
337
|
+
address: setup.setup_details?.arcblock?.staking?.address!,
|
|
338
|
+
});
|
|
339
|
+
if (newInvoice) {
|
|
340
|
+
// @ts-ignore
|
|
341
|
+
invoices.push(newInvoice);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
})
|
|
345
|
+
);
|
|
346
|
+
return invoices;
|
|
347
|
+
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { fromUnitToToken } from '@ocap/util';
|
|
4
4
|
import prettyMsI18n from 'pretty-ms-i18n';
|
|
5
5
|
|
|
6
|
+
import { Op } from 'sequelize';
|
|
6
7
|
import { getUserLocale } from '../../../integrations/blocklet/notification';
|
|
7
8
|
import { translate } from '../../../locales';
|
|
8
9
|
import { Customer, PaymentMethod, Refund, Subscription } from '../../../store/models';
|
|
@@ -56,7 +57,15 @@ export class SubscriptionCanceledEmailTemplate implements BaseEmailTemplate<Subs
|
|
|
56
57
|
throw new Error(`Customer(${subscription.customer_id}) not found`);
|
|
57
58
|
}
|
|
58
59
|
|
|
59
|
-
const invoice =
|
|
60
|
+
const invoice = await Invoice.findOne({
|
|
61
|
+
where: {
|
|
62
|
+
id: subscription.latest_invoice_id,
|
|
63
|
+
},
|
|
64
|
+
include: [{ model: PaymentCurrency, as: 'paymentCurrency' }],
|
|
65
|
+
});
|
|
66
|
+
if (!invoice) {
|
|
67
|
+
throw new Error(`Invoice(${subscription.latest_invoice_id}) not found`);
|
|
68
|
+
}
|
|
60
69
|
const paymentCurrency = (await PaymentCurrency.findOne({
|
|
61
70
|
where: {
|
|
62
71
|
id: subscription.currency_id,
|
|
@@ -68,7 +77,8 @@ export class SubscriptionCanceledEmailTemplate implements BaseEmailTemplate<Subs
|
|
|
68
77
|
const productName = await getMainProductName(subscription.id);
|
|
69
78
|
const at: string = formatTime(subscription.canceled_at * 1000);
|
|
70
79
|
|
|
71
|
-
|
|
80
|
+
// @ts-ignore
|
|
81
|
+
const paymentInfo: string = `${fromUnitToToken(invoice.total, invoice?.paymentCurrency?.decimal)} ${invoice?.paymentCurrency?.symbol}`;
|
|
72
82
|
const currentPeriodStart: string = formatTime(invoice.period_start * 1000);
|
|
73
83
|
const currentPeriodEnd: string = formatTime(invoice.period_end * 1000);
|
|
74
84
|
const duration: string = prettyMsI18n(
|
|
@@ -98,6 +108,9 @@ export class SubscriptionCanceledEmailTemplate implements BaseEmailTemplate<Subs
|
|
|
98
108
|
where: {
|
|
99
109
|
subscription_id: subscription.id,
|
|
100
110
|
type: 'refund',
|
|
111
|
+
status: {
|
|
112
|
+
[Op.not]: 'canceled',
|
|
113
|
+
},
|
|
101
114
|
},
|
|
102
115
|
});
|
|
103
116
|
const conditions = [
|
|
@@ -91,7 +91,7 @@ export class SubscriptionRenewFailedEmailTemplate
|
|
|
91
91
|
const paymentIntent = await PaymentIntent.findByPk(invoice.payment_intent_id);
|
|
92
92
|
const paymentCurrency = (await PaymentCurrency.findOne({
|
|
93
93
|
where: {
|
|
94
|
-
id:
|
|
94
|
+
id: invoice.currency_id,
|
|
95
95
|
},
|
|
96
96
|
})) as PaymentCurrency;
|
|
97
97
|
|
|
@@ -82,7 +82,7 @@ export class SubscriptionRenewedEmailTemplate implements BaseEmailTemplate<Subsc
|
|
|
82
82
|
const paymentIntent = await PaymentIntent.findByPk(invoice.payment_intent_id);
|
|
83
83
|
const paymentCurrency = (await PaymentCurrency.findOne({
|
|
84
84
|
where: {
|
|
85
|
-
id:
|
|
85
|
+
id: invoice.currency_id,
|
|
86
86
|
},
|
|
87
87
|
})) as PaymentCurrency;
|
|
88
88
|
|
|
@@ -4,7 +4,7 @@ import { fromUnitToToken } from '@ocap/util';
|
|
|
4
4
|
import type { ManipulateType } from 'dayjs';
|
|
5
5
|
import prettyMsI18n from 'pretty-ms-i18n';
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import { getTokenByAddress } from '../../../integrations/arcblock/stake';
|
|
8
8
|
import { getUserLocale } from '../../../integrations/blocklet/notification';
|
|
9
9
|
import { translate } from '../../../locales';
|
|
10
10
|
import { Customer, PaymentMethod, Subscription, PaymentCurrency } from '../../../store/models';
|
|
@@ -76,6 +76,10 @@ export class SubscriptionTrialWilEndEmailTemplate
|
|
|
76
76
|
|
|
77
77
|
const paymentMethod: PaymentMethod | null = await PaymentMethod.findByPk(paymentCurrency.payment_method_id);
|
|
78
78
|
|
|
79
|
+
if (!paymentMethod) {
|
|
80
|
+
throw new Error(`Payment method not found: ${paymentCurrency.payment_method_id}`);
|
|
81
|
+
}
|
|
82
|
+
|
|
79
83
|
const userDid = customer.did;
|
|
80
84
|
const locale = await getUserLocale(userDid);
|
|
81
85
|
const productName = await getMainProductName(subscription.id);
|
|
@@ -86,14 +90,14 @@ export class SubscriptionTrialWilEndEmailTemplate
|
|
|
86
90
|
const at: string = formatTime((subscription.trial_end as number) * 1000);
|
|
87
91
|
|
|
88
92
|
const willEndDuration: string = getSimplifyDuration((subscription.trial_end! - dayjs().unix()) * 1000, locale);
|
|
89
|
-
// const paymentDetail: PaymentDetail = await getPaymentDetail(userDid, invoice);
|
|
90
93
|
|
|
91
94
|
const paymentAmount = await getPaymentAmountForCycleSubscription(subscription, paymentCurrency);
|
|
92
95
|
const paymentDetail = { price: paymentAmount, balance: 0, symbol: paymentCurrency.symbol };
|
|
96
|
+
// @ts-ignore
|
|
97
|
+
const paymentAddress = subscription.payment_details?.[paymentMethod.type]?.payer ?? undefined;
|
|
98
|
+
const balance = await getTokenByAddress(paymentAddress, paymentMethod, paymentCurrency);
|
|
99
|
+
paymentDetail.balance = +fromUnitToToken(balance, paymentCurrency.decimal);
|
|
93
100
|
|
|
94
|
-
const token = await getTokenSummaryByDid(userDid, customer.livemode);
|
|
95
|
-
|
|
96
|
-
paymentDetail.balance = +fromUnitToToken(token?.[paymentCurrency.id] || '0', paymentCurrency.decimal);
|
|
97
101
|
const paymentInfo: string = `${paymentAmount} ${paymentCurrency.symbol}`;
|
|
98
102
|
const currentPeriodStart: string = formatTime((subscription.trial_start as number) * 1000);
|
|
99
103
|
const currentPeriodEnd: string = formatTime((subscription.trial_end as number) * 1000);
|
|
@@ -78,19 +78,23 @@ export class SubscriptionWillCanceledEmailTemplate
|
|
|
78
78
|
throw new Error(`Customer not found: ${subscription.customer_id}`);
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
-
const invoice =
|
|
82
|
-
const paymentCurrency = (await PaymentCurrency.findOne({
|
|
81
|
+
const invoice = await Invoice.findOne({
|
|
83
82
|
where: {
|
|
84
|
-
id: subscription.
|
|
83
|
+
id: subscription.latest_invoice_id,
|
|
85
84
|
},
|
|
86
|
-
|
|
85
|
+
include: [{ model: PaymentCurrency, as: 'paymentCurrency' }],
|
|
86
|
+
});
|
|
87
|
+
if (!invoice) {
|
|
88
|
+
throw new Error(`Invoice(${subscription.latest_invoice_id}) not found`);
|
|
89
|
+
}
|
|
87
90
|
|
|
88
91
|
const userDid = customer.did;
|
|
89
92
|
const locale = await getUserLocale(userDid);
|
|
90
93
|
const productName = await getMainProductName(subscription.id);
|
|
91
94
|
const at: string = formatTime(cancelAt * 1000);
|
|
92
95
|
const willCancelDuration: string = getSimplifyDuration((cancelAt - now) * 1000, locale);
|
|
93
|
-
|
|
96
|
+
// @ts-ignore
|
|
97
|
+
const paymentInfo: string = `${fromUnitToToken(+invoice.total, invoice?.paymentCurrency?.decimal)} ${invoice?.paymentCurrency?.symbol}`;
|
|
94
98
|
|
|
95
99
|
let body: string = translate('notification.subscriptWillCanceled.body', locale, {
|
|
96
100
|
productName,
|
|
@@ -6,7 +6,7 @@ import type { LiteralUnion } from 'type-fest';
|
|
|
6
6
|
|
|
7
7
|
import { fromUnitToToken } from '@ocap/util';
|
|
8
8
|
import dayjs from '../../dayjs';
|
|
9
|
-
import {
|
|
9
|
+
import { getTokenByAddress } from '../../../integrations/arcblock/stake';
|
|
10
10
|
import { getUserLocale } from '../../../integrations/blocklet/notification';
|
|
11
11
|
import { translate } from '../../../locales';
|
|
12
12
|
import {
|
|
@@ -93,19 +93,19 @@ export class SubscriptionWillRenewEmailTemplate
|
|
|
93
93
|
const productName = await getMainProductName(subscription.id);
|
|
94
94
|
const at: string = formatTime(invoice.period_end * 1000);
|
|
95
95
|
const willRenewDuration = getSimplifyDuration((invoice.period_end - dayjs().unix()) * 1000, locale);
|
|
96
|
-
// const upcomingInvoiceAmount = await getUpcomingInvoiceAmount(subscription.id);
|
|
97
|
-
// const amount: string = fromUnitToToken(+upcomingInvoiceAmount.amount, upcomingInvoiceAmount.currency?.decimal);
|
|
98
|
-
// const paymentDetail: PaymentDetail = await getPaymentDetail(userDid, invoice, amount);
|
|
99
|
-
// paymentDetail.price = +amount;
|
|
100
96
|
|
|
101
97
|
const paymentAmount = await getPaymentAmountForCycleSubscription(subscription, paymentCurrency);
|
|
102
98
|
const paymentDetail = { price: paymentAmount, balance: 0, symbol: paymentCurrency.symbol, balanceFormatted: '0' };
|
|
103
99
|
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
100
|
+
const paymentMethod = await PaymentMethod.findByPk(paymentCurrency.payment_method_id);
|
|
101
|
+
if (!paymentMethod) {
|
|
102
|
+
throw new Error(`Payment method not found: ${paymentCurrency.payment_method_id}`);
|
|
103
|
+
}
|
|
104
|
+
// @ts-ignore
|
|
105
|
+
const paymentAddress = subscription.payment_details?.[paymentMethod.type]?.payer ?? undefined;
|
|
106
|
+
const balance = await getTokenByAddress(paymentAddress, paymentMethod, paymentCurrency);
|
|
107
|
+
paymentDetail.balanceFormatted = fromUnitToToken(balance, paymentCurrency.decimal);
|
|
108
|
+
paymentDetail.balance = +paymentDetail.balanceFormatted;
|
|
109
109
|
|
|
110
110
|
const { isPrePaid, interval } = await this.getPaymentCategory({
|
|
111
111
|
subscriptionId: subscription.id,
|
|
@@ -132,8 +132,6 @@ export class SubscriptionWillRenewEmailTemplate
|
|
|
132
132
|
locale,
|
|
133
133
|
userDid,
|
|
134
134
|
});
|
|
135
|
-
const paymentMethod: PaymentMethod | null = await PaymentMethod.findByPk(paymentCurrency.payment_method_id);
|
|
136
|
-
|
|
137
135
|
const addFundsLink: string = getCustomerRechargeLink({
|
|
138
136
|
locale,
|
|
139
137
|
userDid,
|
package/api/src/libs/payment.ts
CHANGED
|
@@ -211,13 +211,14 @@ export async function getPaymentDetail(
|
|
|
211
211
|
if (!paymentMethod) {
|
|
212
212
|
return defaultResult;
|
|
213
213
|
}
|
|
214
|
-
|
|
214
|
+
const paymentSettings = invoice?.payment_settings;
|
|
215
215
|
if (['arcblock', 'ethereum'].includes(paymentMethod.type)) {
|
|
216
216
|
// balance enough token for payment?
|
|
217
|
+
const payer = paymentSettings?.payment_method_options.arcblock?.payer as string;
|
|
217
218
|
const result = await isDelegationSufficientForPayment({
|
|
218
219
|
paymentMethod,
|
|
219
220
|
paymentCurrency,
|
|
220
|
-
userDid,
|
|
221
|
+
userDid: payer || userDid,
|
|
221
222
|
amount: inputAmount,
|
|
222
223
|
});
|
|
223
224
|
|
package/api/src/libs/refund.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { BN } from '@ocap/util';
|
|
2
2
|
import { Op, type WhereOptions } from 'sequelize';
|
|
3
3
|
import { PaymentIntent, Refund } from '../store/models';
|
|
4
|
+
import logger from './logger';
|
|
4
5
|
|
|
5
6
|
export async function getRefundAmountSetup({
|
|
6
7
|
currencyId,
|
|
@@ -45,6 +46,9 @@ export async function getRefundAmountSetup({
|
|
|
45
46
|
include: [],
|
|
46
47
|
});
|
|
47
48
|
if (count === 0) {
|
|
49
|
+
logger.info('No refund found for payment intent', {
|
|
50
|
+
paymentIntentId,
|
|
51
|
+
});
|
|
48
52
|
return {
|
|
49
53
|
amount: paymentIntent.amount_received,
|
|
50
54
|
totalAmount: paymentIntent.amount_received,
|
|
@@ -313,6 +313,8 @@ export async function createProration(
|
|
|
313
313
|
prorations: [],
|
|
314
314
|
newCredit: '0',
|
|
315
315
|
appliedCredit: '0',
|
|
316
|
+
remaining: '0',
|
|
317
|
+
remainingUnused: '0',
|
|
316
318
|
};
|
|
317
319
|
}
|
|
318
320
|
|
|
@@ -321,7 +323,7 @@ export async function createProration(
|
|
|
321
323
|
const prorations = await Promise.all(
|
|
322
324
|
prorationItems.map((x: TLineItemExpanded & { [key: string]: any }) => {
|
|
323
325
|
const price = getSubscriptionItemPrice(x);
|
|
324
|
-
const unitAmount = getPriceUintAmountByCurrency(price,
|
|
326
|
+
const unitAmount = getPriceUintAmountByCurrency(price, lastInvoice.currency_id);
|
|
325
327
|
const amount = new BN(unitAmount)
|
|
326
328
|
.mul(new BN(x.quantity))
|
|
327
329
|
.mul(new BN(prorationRate))
|
|
@@ -363,6 +365,25 @@ export async function createProration(
|
|
|
363
365
|
|
|
364
366
|
// 5. adjust invoice total && update customer token balance
|
|
365
367
|
const total = setup.amount.setup;
|
|
368
|
+
|
|
369
|
+
// 6. calculate remaining amount
|
|
370
|
+
const refunds = await Refund.findAll({
|
|
371
|
+
where: {
|
|
372
|
+
status: { [Op.not]: 'canceled' },
|
|
373
|
+
subscription_id: subscription.id,
|
|
374
|
+
currency_id: lastInvoice?.currency_id,
|
|
375
|
+
invoice_id: lastInvoice?.id,
|
|
376
|
+
},
|
|
377
|
+
});
|
|
378
|
+
const refundAmount = refunds.reduce((acc, x) => acc.add(new BN(x.amount || '0')), new BN(0));
|
|
379
|
+
|
|
380
|
+
// 7. calculate remaining amount, default refund recurring amount
|
|
381
|
+
const calcRemaining = (amount: BN, subtract: BN) =>
|
|
382
|
+
amount.sub(subtract).lt(new BN(0)) ? '0' : amount.sub(subtract).toString();
|
|
383
|
+
|
|
384
|
+
const remaining = calcRemaining(new BN(total), refundAmount);
|
|
385
|
+
const remainingUnused = calcRemaining(unused, refundAmount);
|
|
386
|
+
|
|
366
387
|
let due = setup.amount.setup;
|
|
367
388
|
let newCredit = '0';
|
|
368
389
|
let appliedCredit = '0';
|
|
@@ -384,6 +405,8 @@ export async function createProration(
|
|
|
384
405
|
prorationEnd,
|
|
385
406
|
prorationRate,
|
|
386
407
|
unused: unused.toString(),
|
|
408
|
+
remaining,
|
|
409
|
+
remainingUnused,
|
|
387
410
|
total,
|
|
388
411
|
due,
|
|
389
412
|
newCredit,
|
|
@@ -394,6 +417,8 @@ export async function createProration(
|
|
|
394
417
|
lastInvoice,
|
|
395
418
|
total,
|
|
396
419
|
due,
|
|
420
|
+
remaining,
|
|
421
|
+
remainingUnused,
|
|
397
422
|
used: new BN(lastInvoice.amount_due).sub(unused).toString(),
|
|
398
423
|
unused: unused.toString(),
|
|
399
424
|
prorations,
|
|
@@ -402,10 +427,10 @@ export async function createProration(
|
|
|
402
427
|
};
|
|
403
428
|
}
|
|
404
429
|
|
|
405
|
-
export async function getSubscriptionRefundSetup(subscription: Subscription, anchor: number) {
|
|
430
|
+
export async function getSubscriptionRefundSetup(subscription: Subscription, anchor: number, currencyId?: string) {
|
|
406
431
|
const items = await SubscriptionItem.findAll({ where: { subscription_id: subscription.id } });
|
|
407
432
|
const expanded = await Price.expand(items.map((x) => x.toJSON()));
|
|
408
|
-
const setup = getSubscriptionCreateSetup(expanded, subscription.currency_id, 0);
|
|
433
|
+
const setup = getSubscriptionCreateSetup(expanded, currencyId || subscription.currency_id, 0);
|
|
409
434
|
return createProration(subscription, setup, anchor);
|
|
410
435
|
}
|
|
411
436
|
|
|
@@ -452,7 +477,7 @@ export async function getUpcomingInvoiceAmount(subscriptionId: string) {
|
|
|
452
477
|
throw new Error('Subscription not found');
|
|
453
478
|
}
|
|
454
479
|
|
|
455
|
-
if (subscription.isActive() === false) {
|
|
480
|
+
if (subscription.isActive() === false && subscription.status !== 'past_due') {
|
|
456
481
|
throw new Error(`Subscription not active for ${subscriptionId}, so usage check is skipped`);
|
|
457
482
|
}
|
|
458
483
|
|
|
@@ -726,10 +751,17 @@ export async function getRemainingStakes(subscriptionIds: string[], subscription
|
|
|
726
751
|
export async function getSubscriptionStakeSlashSetup(
|
|
727
752
|
subscription: Subscription,
|
|
728
753
|
address: string,
|
|
729
|
-
paymentMethod: PaymentMethod
|
|
754
|
+
paymentMethod: PaymentMethod,
|
|
755
|
+
paymentCurrencyId?: string
|
|
730
756
|
) {
|
|
731
757
|
const lastInvoice = await Invoice.findByPk(subscription.latest_invoice_id);
|
|
732
|
-
const result = await getSubscriptionRemainingStakeSetup(
|
|
758
|
+
const result = await getSubscriptionRemainingStakeSetup(
|
|
759
|
+
subscription,
|
|
760
|
+
address,
|
|
761
|
+
paymentMethod,
|
|
762
|
+
'slash',
|
|
763
|
+
paymentCurrencyId
|
|
764
|
+
);
|
|
733
765
|
return {
|
|
734
766
|
...result,
|
|
735
767
|
lastInvoice,
|
|
@@ -739,10 +771,17 @@ export async function getSubscriptionStakeSlashSetup(
|
|
|
739
771
|
export async function getSubscriptionStakeReturnSetup(
|
|
740
772
|
subscription: Subscription,
|
|
741
773
|
address: string,
|
|
742
|
-
paymentMethod: PaymentMethod
|
|
774
|
+
paymentMethod: PaymentMethod,
|
|
775
|
+
paymentCurrencyId?: string
|
|
743
776
|
) {
|
|
744
777
|
const lastInvoice = await Invoice.findByPk(subscription.latest_invoice_id);
|
|
745
|
-
const result = await getSubscriptionRemainingStakeSetup(
|
|
778
|
+
const result = await getSubscriptionRemainingStakeSetup(
|
|
779
|
+
subscription,
|
|
780
|
+
address,
|
|
781
|
+
paymentMethod,
|
|
782
|
+
'return',
|
|
783
|
+
paymentCurrencyId
|
|
784
|
+
);
|
|
746
785
|
return {
|
|
747
786
|
...result,
|
|
748
787
|
lastInvoice,
|
|
@@ -753,9 +792,10 @@ export async function getSubscriptionRemainingStakeSetup(
|
|
|
753
792
|
subscription: Subscription,
|
|
754
793
|
address: string,
|
|
755
794
|
paymentMethod: PaymentMethod,
|
|
756
|
-
action: 'return' | 'slash' = 'return'
|
|
795
|
+
action: 'return' | 'slash' = 'return',
|
|
796
|
+
paymentCurrencyId?: string
|
|
757
797
|
) {
|
|
758
|
-
const currency = await PaymentCurrency.findByPk(subscription.currency_id);
|
|
798
|
+
const currency = await PaymentCurrency.findByPk(paymentCurrencyId || subscription.currency_id);
|
|
759
799
|
if (!currency) {
|
|
760
800
|
return {
|
|
761
801
|
total: '0',
|
|
@@ -782,12 +822,12 @@ export async function getSubscriptionRemainingStakeSetup(
|
|
|
782
822
|
}
|
|
783
823
|
const [summary] = await Invoice.getUncollectibleAmount({
|
|
784
824
|
subscriptionId: subscription.id,
|
|
785
|
-
currencyId:
|
|
825
|
+
currencyId: currency.id,
|
|
786
826
|
customerId: subscription.customer_id,
|
|
787
827
|
});
|
|
788
828
|
const subscriptionInitStakes = JSON.parse(state.data?.value || '{}');
|
|
789
829
|
const initStake = subscriptionInitStakes[subscription.id];
|
|
790
|
-
const uncollectibleAmountBN = new BN(summary?.[
|
|
830
|
+
const uncollectibleAmountBN = new BN(summary?.[currency.id] || '0');
|
|
791
831
|
if (state.nonce) {
|
|
792
832
|
const returnStake = total.sub(uncollectibleAmountBN);
|
|
793
833
|
return {
|
|
@@ -909,11 +949,15 @@ export async function getSubscriptionStakeCancellation(
|
|
|
909
949
|
return cancellation;
|
|
910
950
|
}
|
|
911
951
|
|
|
912
|
-
export async function getSubscriptionStakeAmountSetup(
|
|
952
|
+
export async function getSubscriptionStakeAmountSetup(
|
|
953
|
+
subscription: Subscription,
|
|
954
|
+
paymentMethod: PaymentMethod,
|
|
955
|
+
stakingTxHash?: string
|
|
956
|
+
) {
|
|
913
957
|
if (paymentMethod.type !== 'arcblock') {
|
|
914
958
|
return null;
|
|
915
959
|
}
|
|
916
|
-
const txHash = subscription?.payment_details?.arcblock?.staking?.tx_hash;
|
|
960
|
+
const txHash = stakingTxHash || subscription?.payment_details?.arcblock?.staking?.tx_hash;
|
|
917
961
|
if (!txHash) {
|
|
918
962
|
return null;
|
|
919
963
|
}
|