payment-kit 1.15.33 → 1.15.34

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/api/src/integrations/stripe/handlers/setup-intent.ts +3 -1
  2. package/api/src/integrations/stripe/handlers/subscription.ts +2 -8
  3. package/api/src/integrations/stripe/resource.ts +0 -11
  4. package/api/src/libs/invoice.ts +202 -1
  5. package/api/src/libs/notification/template/subscription-canceled.ts +11 -2
  6. package/api/src/libs/notification/template/subscription-renew-failed.ts +1 -1
  7. package/api/src/libs/notification/template/subscription-renewed.ts +1 -1
  8. package/api/src/libs/notification/template/subscription-trial-will-end.ts +9 -5
  9. package/api/src/libs/notification/template/subscription-will-canceled.ts +9 -5
  10. package/api/src/libs/notification/template/subscription-will-renew.ts +10 -12
  11. package/api/src/libs/payment.ts +3 -2
  12. package/api/src/libs/subscription.ts +33 -14
  13. package/api/src/queues/invoice.ts +1 -0
  14. package/api/src/queues/payment.ts +3 -1
  15. package/api/src/queues/refund.ts +9 -8
  16. package/api/src/queues/subscription.ts +109 -38
  17. package/api/src/routes/checkout-sessions.ts +20 -4
  18. package/api/src/routes/connect/change-payment.ts +51 -34
  19. package/api/src/routes/connect/change-plan.ts +25 -3
  20. package/api/src/routes/connect/setup.ts +27 -6
  21. package/api/src/routes/connect/shared.ts +135 -1
  22. package/api/src/routes/connect/subscribe.ts +25 -3
  23. package/api/src/routes/invoices.ts +23 -105
  24. package/api/src/routes/subscriptions.ts +66 -17
  25. package/api/src/store/models/invoice.ts +2 -1
  26. package/blocklet.yml +1 -1
  27. package/package.json +4 -4
  28. package/src/components/invoice/list.tsx +47 -24
  29. package/src/components/pricing-table/payment-settings.tsx +1 -1
  30. package/src/components/subscription/actions/cancel.tsx +10 -7
  31. package/src/components/subscription/metrics.tsx +1 -1
  32. package/src/pages/admin/billing/invoices/detail.tsx +15 -0
  33. package/src/pages/admin/billing/invoices/index.tsx +1 -1
  34. package/src/pages/admin/billing/subscriptions/detail.tsx +12 -4
  35. package/src/pages/admin/customers/customers/detail.tsx +1 -0
  36. package/src/pages/customer/index.tsx +1 -1
  37. package/src/pages/customer/invoice/detail.tsx +28 -14
  38. package/src/pages/customer/subscription/change-plan.tsx +8 -1
  39. package/src/pages/customer/subscription/detail.tsx +4 -4
  40. 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
 
@@ -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
+ }
@@ -56,7 +56,15 @@ export class SubscriptionCanceledEmailTemplate implements BaseEmailTemplate<Subs
56
56
  throw new Error(`Customer(${subscription.customer_id}) not found`);
57
57
  }
58
58
 
59
- const invoice = (await Invoice.findByPk(subscription.latest_invoice_id)) as Invoice;
59
+ const invoice = await Invoice.findOne({
60
+ where: {
61
+ id: subscription.latest_invoice_id,
62
+ },
63
+ include: [{ model: PaymentCurrency, as: 'paymentCurrency' }],
64
+ });
65
+ if (!invoice) {
66
+ throw new Error(`Invoice(${subscription.latest_invoice_id}) not found`);
67
+ }
60
68
  const paymentCurrency = (await PaymentCurrency.findOne({
61
69
  where: {
62
70
  id: subscription.currency_id,
@@ -68,7 +76,8 @@ export class SubscriptionCanceledEmailTemplate implements BaseEmailTemplate<Subs
68
76
  const productName = await getMainProductName(subscription.id);
69
77
  const at: string = formatTime(subscription.canceled_at * 1000);
70
78
 
71
- const paymentInfo: string = `${fromUnitToToken(invoice.total, paymentCurrency.decimal)} ${paymentCurrency.symbol}`;
79
+ // @ts-ignore
80
+ const paymentInfo: string = `${fromUnitToToken(invoice.total, invoice?.paymentCurrency?.decimal)} ${invoice?.paymentCurrency?.symbol}`;
72
81
  const currentPeriodStart: string = formatTime(invoice.period_start * 1000);
73
82
  const currentPeriodEnd: string = formatTime(invoice.period_end * 1000);
74
83
  const duration: string = prettyMsI18n(
@@ -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: subscription.currency_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: subscription.currency_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 { getTokenSummaryByDid } from '../../../integrations/arcblock/stake';
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 = (await Invoice.findByPk(subscription.latest_invoice_id)) as Invoice;
82
- const paymentCurrency = (await PaymentCurrency.findOne({
81
+ const invoice = await Invoice.findOne({
83
82
  where: {
84
- id: subscription.currency_id,
83
+ id: subscription.latest_invoice_id,
85
84
  },
86
- })) as PaymentCurrency;
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
- const paymentInfo: string = `${fromUnitToToken(+invoice.total, paymentCurrency.decimal)} ${paymentCurrency.symbol}`;
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 { getTokenSummaryByDid } from '../../../integrations/arcblock/stake';
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 token = await getTokenSummaryByDid(userDid, customer.livemode);
105
-
106
- const balance = fromUnitToToken(token?.[paymentCurrency.id] || '0', paymentCurrency.decimal);
107
- paymentDetail.balanceFormatted = balance;
108
- paymentDetail.balance = +balance;
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,
@@ -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
 
@@ -321,7 +321,7 @@ export async function createProration(
321
321
  const prorations = await Promise.all(
322
322
  prorationItems.map((x: TLineItemExpanded & { [key: string]: any }) => {
323
323
  const price = getSubscriptionItemPrice(x);
324
- const unitAmount = getPriceUintAmountByCurrency(price, subscription.currency_id);
324
+ const unitAmount = getPriceUintAmountByCurrency(price, lastInvoice.currency_id);
325
325
  const amount = new BN(unitAmount)
326
326
  .mul(new BN(x.quantity))
327
327
  .mul(new BN(prorationRate))
@@ -402,10 +402,10 @@ export async function createProration(
402
402
  };
403
403
  }
404
404
 
405
- export async function getSubscriptionRefundSetup(subscription: Subscription, anchor: number) {
405
+ export async function getSubscriptionRefundSetup(subscription: Subscription, anchor: number, currencyId?: string) {
406
406
  const items = await SubscriptionItem.findAll({ where: { subscription_id: subscription.id } });
407
407
  const expanded = await Price.expand(items.map((x) => x.toJSON()));
408
- const setup = getSubscriptionCreateSetup(expanded, subscription.currency_id, 0);
408
+ const setup = getSubscriptionCreateSetup(expanded, currencyId || subscription.currency_id, 0);
409
409
  return createProration(subscription, setup, anchor);
410
410
  }
411
411
 
@@ -452,7 +452,7 @@ export async function getUpcomingInvoiceAmount(subscriptionId: string) {
452
452
  throw new Error('Subscription not found');
453
453
  }
454
454
 
455
- if (subscription.isActive() === false) {
455
+ if (subscription.isActive() === false && subscription.status !== 'past_due') {
456
456
  throw new Error(`Subscription not active for ${subscriptionId}, so usage check is skipped`);
457
457
  }
458
458
 
@@ -726,10 +726,17 @@ export async function getRemainingStakes(subscriptionIds: string[], subscription
726
726
  export async function getSubscriptionStakeSlashSetup(
727
727
  subscription: Subscription,
728
728
  address: string,
729
- paymentMethod: PaymentMethod
729
+ paymentMethod: PaymentMethod,
730
+ paymentCurrencyId?: string
730
731
  ) {
731
732
  const lastInvoice = await Invoice.findByPk(subscription.latest_invoice_id);
732
- const result = await getSubscriptionRemainingStakeSetup(subscription, address, paymentMethod, 'slash');
733
+ const result = await getSubscriptionRemainingStakeSetup(
734
+ subscription,
735
+ address,
736
+ paymentMethod,
737
+ 'slash',
738
+ paymentCurrencyId
739
+ );
733
740
  return {
734
741
  ...result,
735
742
  lastInvoice,
@@ -739,10 +746,17 @@ export async function getSubscriptionStakeSlashSetup(
739
746
  export async function getSubscriptionStakeReturnSetup(
740
747
  subscription: Subscription,
741
748
  address: string,
742
- paymentMethod: PaymentMethod
749
+ paymentMethod: PaymentMethod,
750
+ paymentCurrencyId?: string
743
751
  ) {
744
752
  const lastInvoice = await Invoice.findByPk(subscription.latest_invoice_id);
745
- const result = await getSubscriptionRemainingStakeSetup(subscription, address, paymentMethod, 'return');
753
+ const result = await getSubscriptionRemainingStakeSetup(
754
+ subscription,
755
+ address,
756
+ paymentMethod,
757
+ 'return',
758
+ paymentCurrencyId
759
+ );
746
760
  return {
747
761
  ...result,
748
762
  lastInvoice,
@@ -753,9 +767,10 @@ export async function getSubscriptionRemainingStakeSetup(
753
767
  subscription: Subscription,
754
768
  address: string,
755
769
  paymentMethod: PaymentMethod,
756
- action: 'return' | 'slash' = 'return'
770
+ action: 'return' | 'slash' = 'return',
771
+ paymentCurrencyId?: string
757
772
  ) {
758
- const currency = await PaymentCurrency.findByPk(subscription.currency_id);
773
+ const currency = await PaymentCurrency.findByPk(paymentCurrencyId || subscription.currency_id);
759
774
  if (!currency) {
760
775
  return {
761
776
  total: '0',
@@ -782,12 +797,12 @@ export async function getSubscriptionRemainingStakeSetup(
782
797
  }
783
798
  const [summary] = await Invoice.getUncollectibleAmount({
784
799
  subscriptionId: subscription.id,
785
- currencyId: subscription.currency_id,
800
+ currencyId: currency.id,
786
801
  customerId: subscription.customer_id,
787
802
  });
788
803
  const subscriptionInitStakes = JSON.parse(state.data?.value || '{}');
789
804
  const initStake = subscriptionInitStakes[subscription.id];
790
- const uncollectibleAmountBN = new BN(summary?.[subscription.currency_id] || '0');
805
+ const uncollectibleAmountBN = new BN(summary?.[currency.id] || '0');
791
806
  if (state.nonce) {
792
807
  const returnStake = total.sub(uncollectibleAmountBN);
793
808
  return {
@@ -909,11 +924,15 @@ export async function getSubscriptionStakeCancellation(
909
924
  return cancellation;
910
925
  }
911
926
 
912
- export async function getSubscriptionStakeAmountSetup(subscription: Subscription, paymentMethod: PaymentMethod) {
927
+ export async function getSubscriptionStakeAmountSetup(
928
+ subscription: Subscription,
929
+ paymentMethod: PaymentMethod,
930
+ stakingTxHash?: string
931
+ ) {
913
932
  if (paymentMethod.type !== 'arcblock') {
914
933
  return null;
915
934
  }
916
- const txHash = subscription?.payment_details?.arcblock?.staking?.tx_hash;
935
+ const txHash = stakingTxHash || subscription?.payment_details?.arcblock?.staking?.tx_hash;
917
936
  if (!txHash) {
918
937
  return null;
919
938
  }
@@ -220,6 +220,7 @@ export const startInvoiceQueue = async () => {
220
220
  status: 'open',
221
221
  collection_method: 'charge_automatically',
222
222
  amount_remaining: { [Op.gt]: '0' },
223
+ billing_reason: { [Op.ne]: 'stake' },
223
224
  },
224
225
  });
225
226
 
@@ -587,11 +587,13 @@ export const handlePayment = async (job: PaymentJob) => {
587
587
  }
588
588
  const client = paymentMethod.getOcapClient();
589
589
 
590
+ const payer = paymentSettings?.payment_method_options.arcblock?.payer as string;
591
+
590
592
  // check balance before capture with transaction
591
593
  result = await isDelegationSufficientForPayment({
592
594
  paymentMethod,
593
595
  paymentCurrency,
594
- userDid: customer.did,
596
+ userDid: payer || customer.did,
595
597
  amount: paymentIntent.amount,
596
598
  });
597
599
  if (result.sufficient === false) {
@@ -126,10 +126,14 @@ const handleRefundJob = async (
126
126
  // try refund transfer and reschedule on error
127
127
  logger.info('Refund transfer attempt', { id: refund.id, attempt: refund.attempt_count });
128
128
  let result;
129
+ const paymentIntent = await PaymentIntent.findByPk(refund.payment_intent_id);
130
+ if (!paymentIntent) {
131
+ throw new Error('PaymentIntent not found');
132
+ }
133
+
129
134
  try {
130
135
  if (paymentMethod.type === 'arcblock') {
131
136
  const client = paymentMethod.getOcapClient();
132
-
133
137
  // check balance before transfer with transaction
134
138
  result = await isBalanceSufficientForRefund({ paymentMethod, paymentCurrency, amount: refund.amount });
135
139
  if (result.sufficient === false) {
@@ -137,11 +141,12 @@ const handleRefundJob = async (
137
141
  throw new CustomError(result.reason, 'app balance not sufficient for this refund');
138
142
  }
139
143
 
144
+ const payer = paymentIntent?.payment_details?.arcblock?.payer;
140
145
  // do the transfer
141
146
  const signed = await client.signTransferV2Tx({
142
147
  tx: {
143
148
  itx: {
144
- to: customer.did,
149
+ to: payer || customer.did,
145
150
  value: '0',
146
151
  assets: [],
147
152
  tokens: [{ address: paymentCurrency.contract, value: refund.amount }],
@@ -192,7 +197,6 @@ const handleRefundJob = async (
192
197
 
193
198
  // do the capture
194
199
  const client = paymentMethod.getEvmClient();
195
- const paymentIntent = await PaymentIntent.findByPk(refund.payment_intent_id);
196
200
  const payer = paymentIntent!.payment_details?.ethereum?.payer as string;
197
201
  const receipt = await sendErc20ToUser(client, paymentCurrency.contract, payer, refund.amount);
198
202
  logger.info('refund transfer done', { id: refund.id, txHash: receipt.hash });
@@ -216,10 +220,6 @@ const handleRefundJob = async (
216
220
  if (!refund.payment_intent_id) {
217
221
  throw new Error('payment intent id not found');
218
222
  }
219
- const paymentIntent = await PaymentIntent.findByPk(refund.payment_intent_id);
220
- if (!paymentIntent) {
221
- throw new Error('PaymentIntent not found');
222
- }
223
223
  const stripePaymentIntentId = paymentIntent?.payment_details?.stripe?.payment_intent_id;
224
224
  if (!stripePaymentIntentId) {
225
225
  throw new Error('paymentIntent should have stripe payment intent id');
@@ -315,7 +315,8 @@ const handleStakeReturnJob = async (
315
315
  }
316
316
  const client = paymentMethod.getOcapClient();
317
317
  const subscription = await Subscription.findByPk(refund.subscription_id);
318
- const address = await getSubscriptionStakeAddress(subscription!, customer.did);
318
+ const address =
319
+ arcblockDetail?.staking?.address || (await getSubscriptionStakeAddress(subscription!, customer.did));
319
320
  const stakeEnough = await checkRemainingStake(paymentMethod, paymentCurrency, address, refund.amount);
320
321
  if (!stakeEnough.enough) {
321
322
  logger.warn('Stake return aborted because stake is not enough ', {