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
@@ -1021,7 +1021,8 @@ export async function executeOcapTransactions(
1021
1021
  claims: any[],
1022
1022
  paymentMethod: PaymentMethod,
1023
1023
  request: Request,
1024
- subscriptionId?: string
1024
+ subscriptionId?: string,
1025
+ paymentCurrencyContract?: string
1025
1026
  ) {
1026
1027
  const client = paymentMethod.getOcapClient();
1027
1028
  const delegation = claims.find((x) => x.type === 'signature' && x.meta?.purpose === 'delegation');
@@ -1031,6 +1032,9 @@ export async function executeOcapTransactions(
1031
1032
  [staking, 'Stake'],
1032
1033
  ];
1033
1034
 
1035
+ const stakingAmount =
1036
+ staking?.requirement?.tokens?.find((x: any) => x.address === paymentCurrencyContract)?.value || '0';
1037
+
1034
1038
  const [delegationTxHash, stakingTxHash] = await Promise.all(
1035
1039
  transactions.map(async ([claim, type]) => {
1036
1040
  if (!claim) {
@@ -1065,5 +1069,135 @@ export async function executeOcapTransactions(
1065
1069
  tx_hash: stakingTxHash,
1066
1070
  address: await getCustomerStakeAddress(userDid, nonce),
1067
1071
  },
1072
+ stakingAmount,
1068
1073
  };
1069
1074
  }
1075
+
1076
+ export async function ensureStakeInvoice(
1077
+ invoiceProps: { total: string; description?: string; checkout_session_id?: string; currency_id: string; metadata?: any; payment_settings?: any },
1078
+ subscription: Subscription,
1079
+ paymentMethod: PaymentMethod,
1080
+ customer: Customer
1081
+ ) {
1082
+ if (paymentMethod.type !== 'arcblock') {
1083
+ return;
1084
+ }
1085
+ try {
1086
+ const stakingInvoice = await Invoice.create({
1087
+ livemode: subscription.livemode,
1088
+ number: await customer.getInvoiceNumber(),
1089
+ description: invoiceProps?.description || 'Stake for subscription',
1090
+ statement_descriptor: '',
1091
+ period_start: dayjs().unix(),
1092
+ period_end: subscription.current_period_end,
1093
+
1094
+ auto_advance: false,
1095
+ paid: true,
1096
+ paid_out_of_band: false,
1097
+
1098
+ status: 'paid',
1099
+ collection_method: 'charge_automatically',
1100
+ billing_reason: 'stake',
1101
+
1102
+ currency_id: invoiceProps.currency_id,
1103
+ customer_id: customer.id,
1104
+ payment_intent_id: '',
1105
+ subscription_id: subscription?.id,
1106
+ checkout_session_id: invoiceProps?.checkout_session_id || '',
1107
+
1108
+ total: invoiceProps.total || '0',
1109
+ subtotal: invoiceProps.total || '0',
1110
+ tax: '0',
1111
+ subtotal_excluding_tax: invoiceProps.total || '0',
1112
+
1113
+ amount_due: '0',
1114
+ amount_paid: invoiceProps.total || '0',
1115
+ amount_remaining: '0',
1116
+ amount_shipping: '0',
1117
+
1118
+ starting_balance: '0',
1119
+ ending_balance: '0',
1120
+ starting_token_balance: {},
1121
+ ending_token_balance: {},
1122
+
1123
+ attempt_count: 0,
1124
+ attempted: false,
1125
+ // next_payment_attempt: undefined,
1126
+
1127
+ custom_fields: [],
1128
+ customer_address: customer.address,
1129
+ customer_email: customer.email,
1130
+ customer_name: customer.name,
1131
+ customer_phone: customer.phone,
1132
+
1133
+ discounts: [],
1134
+ total_discount_amounts: [],
1135
+
1136
+ due_date: undefined,
1137
+ effective_at: dayjs().unix(),
1138
+ status_transitions: {
1139
+ finalized_at: dayjs().unix(),
1140
+ },
1141
+
1142
+ payment_settings: invoiceProps?.payment_settings || subscription?.payment_settings,
1143
+ default_payment_method_id: paymentMethod.id,
1144
+
1145
+ account_country: '',
1146
+ account_name: '',
1147
+ metadata: invoiceProps.metadata || {},
1148
+ });
1149
+ logger.info('create staking invoice success', {
1150
+ stakingInvoice,
1151
+ subscriptionId: subscription?.id,
1152
+ paymentMethod: paymentMethod.id,
1153
+ customerId: customer.id,
1154
+ });
1155
+ } catch (error) {
1156
+ logger.error('ensureStake: create invoice failed', { error, subscriptionId: subscription?.id, paymentMethod: paymentMethod.id, customerId: customer.id });
1157
+ }
1158
+ }
1159
+
1160
+
1161
+ export async function updateStripeSubscriptionAfterChangePayment(setupIntent: SetupIntent, subscription: Subscription) {
1162
+ const { from_method: fromMethodId, to_method: toMethodId } = setupIntent.metadata || {};
1163
+ const fromMethod = await PaymentMethod.findByPk(fromMethodId);
1164
+ if (fromMethod?.type === 'stripe') {
1165
+ // pause Stripe
1166
+ const client = fromMethod?.getStripeClient();
1167
+ const stripeSubscriptionId = subscription.payment_details?.stripe?.subscription_id as string;
1168
+ if (client && stripeSubscriptionId) {
1169
+ const stripeSubscription = await client.subscriptions.retrieve(stripeSubscriptionId);
1170
+ if (stripeSubscription) {
1171
+ const result = await client.subscriptions.update(stripeSubscriptionId, {
1172
+ pause_collection: {
1173
+ behavior: 'void',
1174
+ },
1175
+ });
1176
+ logger.info('stripe subscription paused on payment change', {
1177
+ subscription: subscription.id,
1178
+ stripeSubscription: stripeSubscriptionId,
1179
+ result,
1180
+ });
1181
+ }
1182
+ }
1183
+ } else {
1184
+ const toMethod = await PaymentMethod.findByPk(toMethodId);
1185
+ if (toMethod?.type === 'stripe') {
1186
+ // resume stripe
1187
+ const client = toMethod?.getStripeClient();
1188
+ const stripeSubscriptionId = subscription.payment_details?.stripe?.subscription_id as string;
1189
+ if (client && stripeSubscriptionId) {
1190
+ const stripeSubscription = await client.subscriptions.retrieve(stripeSubscriptionId);
1191
+ if (stripeSubscription.status === 'paused') {
1192
+ await client.subscriptions.resume(stripeSubscriptionId, {
1193
+ proration_behavior: 'none',
1194
+ });
1195
+ logger.info('stripe subscription resumed on payment change', {
1196
+ subscription: subscription.id,
1197
+ stripeSubscription: stripeSubscriptionId,
1198
+ });
1199
+ }
1200
+ }
1201
+ }
1202
+ }
1203
+ }
@@ -12,6 +12,7 @@ import type { Invoice, TLineItemExpanded } from '../../store/models';
12
12
  import {
13
13
  ensureInvoiceForCheckout,
14
14
  ensurePaymentIntent,
15
+ ensureStakeInvoice,
15
16
  executeOcapTransactions,
16
17
  getAuthPrincipalClaim,
17
18
  getDelegationTxClaim,
@@ -122,7 +123,7 @@ export default {
122
123
  onAuth: async (args: CallbackArgs) => {
123
124
  const { request, userDid, userPk, claims, extraParams } = args;
124
125
  const { checkoutSessionId, connectedDid } = extraParams;
125
- const { checkoutSession, customer, paymentMethod, subscription } = await ensurePaymentIntent(
126
+ const { checkoutSession, customer, paymentMethod, subscription, paymentCurrency } = await ensurePaymentIntent(
126
127
  checkoutSessionId,
127
128
  connectedDid || userDid
128
129
  );
@@ -159,13 +160,34 @@ export default {
159
160
  await prepareTxExecution();
160
161
  const { invoice } = await ensureInvoiceForCheckout({ checkoutSession, customer, subscription });
161
162
 
162
- const paymentDetails = await executeOcapTransactions(
163
+ const { stakingAmount, ...paymentDetails } = await executeOcapTransactions(
163
164
  userDid,
164
165
  userPk,
165
166
  claims,
166
167
  paymentMethod,
167
168
  request,
168
- subscription?.id
169
+ subscription?.id,
170
+ paymentCurrency?.contract
171
+ );
172
+ await ensureStakeInvoice(
173
+ {
174
+ total: stakingAmount,
175
+ description: 'Stake for subscription',
176
+ checkout_session_id: checkoutSessionId,
177
+ currency_id: paymentCurrency.id,
178
+ metadata: {
179
+ payment_details: {
180
+ arcblock: {
181
+ tx_hash: paymentDetails?.staking?.tx_hash,
182
+ payer: paymentDetails?.payer,
183
+ address: paymentDetails?.staking?.address,
184
+ },
185
+ },
186
+ },
187
+ },
188
+ subscription,
189
+ paymentMethod,
190
+ customer
169
191
  );
170
192
  await afterTxExecution(invoice!, paymentDetails);
171
193
 
@@ -10,7 +10,6 @@ import { createListParamSchema, getWhereFromKvQuery, MetadataSchema } from '../l
10
10
  import { authenticate } from '../libs/security';
11
11
  import { expandLineItems } from '../libs/session';
12
12
  import { formatMetadata } from '../libs/util';
13
- import { Refund, SetupIntent } from '../store/models';
14
13
  import { Customer } from '../store/models/customer';
15
14
  import { Invoice } from '../store/models/invoice';
16
15
  import { InvoiceItem } from '../store/models/invoice-item';
@@ -20,7 +19,7 @@ import { PaymentMethod } from '../store/models/payment-method';
20
19
  import { Price } from '../store/models/price';
21
20
  import { Product } from '../store/models/product';
22
21
  import { Subscription } from '../store/models/subscription';
23
- import { getSubscriptionStakeAmountSetup } from '../libs/subscription';
22
+ import { getReturnStakeInvoices, getStakingInvoices } from '../libs/invoice';
24
23
 
25
24
  const router = Router();
26
25
  const authAdmin = authenticate<Subscription>({ component: true, roles: ['owner', 'admin'] });
@@ -43,6 +42,7 @@ const schema = createListParamSchema<{
43
42
  currency_id?: string;
44
43
  ignore_zero?: boolean;
45
44
  include_staking?: boolean;
45
+ include_return_staking?: boolean;
46
46
  include_recovered_from?: boolean;
47
47
  }>({
48
48
  status: Joi.string().empty(''),
@@ -52,17 +52,16 @@ const schema = createListParamSchema<{
52
52
  currency_id: Joi.string().empty(''),
53
53
  ignore_zero: Joi.boolean().empty(false),
54
54
  include_staking: Joi.boolean().empty(false),
55
+ include_return_staking: Joi.boolean().empty(false),
55
56
  include_recovered_from: Joi.boolean().empty(false),
56
57
  });
57
58
  router.get('/', authMine, async (req, res) => {
58
59
  // eslint-disable-next-line @typescript-eslint/naming-convention
59
- const { page, pageSize, livemode, status, ignore_zero, include_staking, ...query } = await schema.validateAsync(
60
- req.query,
61
- {
60
+ const { page, pageSize, livemode, status, ignore_zero, include_staking, include_return_staking, ...query } =
61
+ await schema.validateAsync(req.query, {
62
62
  stripUnknown: false,
63
63
  allowUnknown: true,
64
- }
65
- );
64
+ });
66
65
  const where = getWhereFromKvQuery(query.q);
67
66
 
68
67
  if (status) {
@@ -108,7 +107,9 @@ router.get('/', authMine, async (req, res) => {
108
107
  // @ts-ignore
109
108
  where[key] = query[key];
110
109
  });
111
-
110
+ if (!!(include_staking && query.subscription_id) || !include_staking) {
111
+ where.billing_reason = { [Op.ne]: 'stake' };
112
+ }
112
113
  try {
113
114
  const { rows: list, count } = await Invoice.findAndCountAll({
114
115
  where,
@@ -117,7 +118,7 @@ router.get('/', authMine, async (req, res) => {
117
118
  limit: pageSize,
118
119
  include: [
119
120
  { model: PaymentCurrency, as: 'paymentCurrency' },
120
- // { model: PaymentMethod, as: 'paymentMethod' },
121
+ { model: PaymentMethod, as: 'paymentMethod' },
121
122
  // { model: PaymentIntent, as: 'paymentIntent' },
122
123
  // { model: Subscription, as: 'subscription' },
123
124
  { model: Customer, as: 'customer' },
@@ -126,113 +127,30 @@ router.get('/', authMine, async (req, res) => {
126
127
 
127
128
  // push staking info as first invoice if we are on the last page
128
129
  let subscription;
130
+ let invoices = list;
129
131
  if (query.subscription_id && include_staking && page === Math.ceil((count || 1) / pageSize)) {
130
132
  try {
131
133
  subscription = await Subscription.findByPk(query.subscription_id);
132
134
  if (subscription?.payment_details?.arcblock?.staking?.tx_hash) {
133
- const method = await PaymentMethod.findOne({ where: { type: 'arcblock', livemode: subscription.livemode } });
134
- const setup = await SetupIntent.findOne({
135
- where: {
136
- customer_id: subscription.customer_id,
137
- payment_method_id: method?.id,
138
- metadata: { subscription_id: subscription.id },
139
- },
140
- order: [['created_at', 'ASC']],
141
- });
142
- const currencyId = setup?.currency_id || subscription.currency_id;
143
- const currency = await PaymentCurrency.findByPk(currencyId);
144
- if (method && currency) {
145
- const { address } = subscription.payment_details.arcblock.staking;
146
- const firstInvoice = await Invoice.findOne({
147
- where: { subscription_id: subscription.id, currency_id: currencyId },
148
- order: [['created_at', 'ASC']],
149
- include: [{ model: PaymentCurrency, as: 'paymentCurrency' }],
150
- });
151
- if (firstInvoice) {
152
- const customer = await Customer.findByPk(firstInvoice.customer_id);
153
- const stakeAmountResult = await getSubscriptionStakeAmountSetup(subscription, method);
154
- // @ts-ignore
155
- const stakeAmount = stakeAmountResult?.[currency?.contract] || '0';
156
-
157
- list.push({
158
- id: address as string,
159
- status: 'paid',
160
- description: 'Subscription staking',
161
- billing_reason: 'subscription_create',
162
- total: stakeAmount,
163
- amount_due: '0',
164
- amount_paid: stakeAmount,
165
- amount_remaining: '0',
166
- ...pick(firstInvoice, [
167
- 'number',
168
- 'paid',
169
- 'auto_advance',
170
- 'currency_id',
171
- 'customer_id',
172
- 'subscription_id',
173
- 'period_start',
174
- 'period_end',
175
- 'created_at',
176
- 'updated_at',
177
- ]),
178
- // @ts-ignore
179
- paymentCurrency: currency,
180
- paymentMethod: method,
181
- customer,
182
- metadata: {
183
- payment_details: {
184
- arcblock: {
185
- tx_hash: subscription.payment_details.arcblock.staking.tx_hash,
186
- payer: subscription.payment_details.arcblock.payer,
187
- },
188
- },
189
- },
190
- });
191
- const stakeRefundRecord = await Refund.findOne({
192
- where: { subscription_id: subscription.id, status: 'succeeded', type: 'stake_return' },
193
- });
194
- if (stakeRefundRecord) {
195
- list.unshift({
196
- id: address as string,
197
- status: 'paid',
198
- description: 'Return Subscription staking',
199
- billing_reason: 'subscription_create',
200
- total: stakeRefundRecord.amount,
201
- amount_due: '0',
202
- amount_paid: stakeRefundRecord.amount,
203
- amount_remaining: '0',
204
- created_at: stakeRefundRecord.created_at,
205
- updated_at: stakeRefundRecord.updated_at,
206
- currency_id: stakeRefundRecord.currency_id,
207
- customer_id: stakeRefundRecord.customer_id,
208
- subscription_id: subscription.id,
209
- period_start: subscription.current_period_start,
210
- period_end: subscription.current_period_end,
211
- paid: true,
212
- ...pick(firstInvoice, ['number', 'auto_advance']),
213
- // @ts-ignore
214
- paymentCurrency: currency,
215
- paymentMethod: method,
216
- customer,
217
- metadata: {
218
- payment_details: {
219
- arcblock: {
220
- tx_hash: stakeRefundRecord?.payment_details?.arcblock?.tx_hash,
221
- payer: stakeRefundRecord?.payment_details?.arcblock?.payer,
222
- },
223
- },
224
- },
225
- });
226
- }
227
- }
135
+ const stakingInvoices = await getStakingInvoices(subscription);
136
+ let returnStakeInvoices: any[] = [];
137
+ if (include_return_staking) {
138
+ returnStakeInvoices = await getReturnStakeInvoices(subscription);
228
139
  }
140
+ invoices = [...(stakingInvoices || []), ...(returnStakeInvoices || []), ...list]
141
+ .filter(Boolean)
142
+ .sort((a, b) =>
143
+ query.o === 'asc'
144
+ ? new Date(a.created_at).getTime() - new Date(b.created_at).getTime()
145
+ : new Date(b.created_at).getTime() - new Date(a.created_at).getTime()
146
+ );
229
147
  }
230
148
  } catch (err) {
231
149
  console.error('Failed to include staking record in invoice list', err);
232
150
  }
233
151
  }
234
152
 
235
- res.json({ count, list, subscription, paging: { page, pageSize } });
153
+ res.json({ count, list: invoices, subscription, paging: { page, pageSize } });
236
154
  } catch (err) {
237
155
  console.error(err);
238
156
  res.json({ count: 0, list: [], paging: { page, pageSize } });
@@ -8,7 +8,12 @@ import uniq from 'lodash/uniq';
8
8
 
9
9
  import { literal, OrderItem } from 'sequelize';
10
10
  import { createEvent } from '../libs/audit';
11
- import { ensureStripeCustomer, ensureStripePrice, ensureStripeSubscription } from '../integrations/stripe/resource';
11
+ import {
12
+ ensureStripeCustomer,
13
+ ensureStripePaymentCustomer,
14
+ ensureStripePrice,
15
+ ensureStripeSubscription,
16
+ } from '../integrations/stripe/resource';
12
17
  import { createListParamSchema, getWhereFromKvQuery, getWhereFromQuery, MetadataSchema } from '../libs/api';
13
18
  import dayjs from '../libs/dayjs';
14
19
  import logger from '../libs/logger';
@@ -258,7 +263,8 @@ router.put('/:id/cancel', authPortal, async (req, res) => {
258
263
  return res.status(400).json({ error: `comment invalid: ${commentError.message}` });
259
264
  }
260
265
 
261
- const slashStake = req.body?.requestByAdmin && req.body?.staking === 'slash';
266
+ const requestByAdmin = ['owner', 'admin'].includes(req.user?.role as string);
267
+ const slashStake = requestByAdmin && req.body?.staking === 'slash';
262
268
 
263
269
  if (slashStake) {
264
270
  const { error: slashReasonError } = SlashStakeSchema.validate(req.body?.slashReason);
@@ -292,7 +298,6 @@ router.put('/:id/cancel', authPortal, async (req, res) => {
292
298
  }
293
299
 
294
300
  let canReturnStake = false;
295
- const requestByAdmin = ['owner', 'admin'].includes(req.user?.role as string);
296
301
  if ((requestByAdmin && staking === 'proration') || req.body?.cancel_from === 'customer') {
297
302
  canReturnStake = true;
298
303
  }
@@ -680,11 +685,11 @@ const updateSchema = Joi.object<{
680
685
  .items(
681
686
  Joi.object({
682
687
  name: Joi.string().optional(),
683
- color: Joi.string().allow('primary', 'secondary', 'success', 'error', 'warning').optional(),
684
- variant: Joi.string().allow('text', 'contained', 'outlined').optional(),
688
+ color: Joi.string().valid('primary', 'secondary', 'success', 'error', 'warning').optional(),
689
+ variant: Joi.string().valid('text', 'contained', 'outlined').optional(),
685
690
  text: Joi.object().required(),
686
691
  link: Joi.string().uri().required(),
687
- type: Joi.string().allow('notification', 'custom').optional(),
692
+ type: Joi.string().valid('notification', 'custom').optional(),
688
693
  triggerEvents: Joi.array().items(Joi.string()).optional(),
689
694
  })
690
695
  )
@@ -1263,11 +1268,6 @@ router.get('/:id/proration', authPortal, async (req, res) => {
1263
1268
  return res.status(400).json({ error: 'Subscription is not active' });
1264
1269
  }
1265
1270
 
1266
- const paymentMethod = await PaymentMethod.findByPk(subscription.default_payment_method_id);
1267
- if (paymentMethod?.type === 'stripe') {
1268
- return res.status(400).json({ error: 'Not supported for subscriptions paid with stripe' });
1269
- }
1270
-
1271
1271
  const invoice = await Invoice.findOne({
1272
1272
  where: {
1273
1273
  subscription_id: subscription.id,
@@ -1278,16 +1278,23 @@ router.get('/:id/proration', authPortal, async (req, res) => {
1278
1278
  });
1279
1279
 
1280
1280
  const anchor = req.query.time ? dayjs(req.query.time as any).unix() : dayjs().unix();
1281
- const result = await getSubscriptionRefundSetup(subscription, anchor);
1281
+ const result = await getSubscriptionRefundSetup(subscription, anchor, invoice?.currency_id);
1282
1282
  if (result.total === '0') {
1283
1283
  return res.json(null);
1284
1284
  }
1285
+ const paymentCurrency = await PaymentCurrency.findByPk(result.lastInvoice?.currency_id);
1286
+ const paymentMethod = await PaymentMethod.findByPk(paymentCurrency?.payment_method_id);
1287
+ if (paymentMethod?.type === 'stripe') {
1288
+ return res.status(400).json({ error: 'Not supported for subscriptions paid with stripe' });
1289
+ }
1290
+
1285
1291
  return res.json({
1286
1292
  total: result.total,
1287
1293
  latest: invoice?.total,
1288
1294
  unused: result.unused,
1289
1295
  used: result.used,
1290
1296
  prorations: result.prorations,
1297
+ paymentCurrency,
1291
1298
  });
1292
1299
  } catch (err) {
1293
1300
  console.error(err);
@@ -1418,6 +1425,8 @@ router.post('/:id/change-payment', authPortal, async (req, res) => {
1418
1425
  to_currency: paymentCurrency.id,
1419
1426
  from_method: subscription.default_payment_method_id,
1420
1427
  to_method: paymentMethod.id,
1428
+ // @ts-ignore
1429
+ from_payment_details: subscription.payment_details?.[previousPaymentMethod?.type],
1421
1430
  },
1422
1431
  });
1423
1432
 
@@ -1444,9 +1453,32 @@ router.post('/:id/change-payment', authPortal, async (req, res) => {
1444
1453
  const client = paymentMethod.getStripeClient();
1445
1454
  let exist;
1446
1455
  if (subscription.payment_details?.stripe?.subscription_id) {
1447
- exist = await client.subscriptions.retrieve(subscription.payment_details.stripe.subscription_id);
1456
+ exist = await client.subscriptions.retrieve(subscription.payment_details.stripe.subscription_id, {
1457
+ expand: ['latest_invoice.payment_intent', 'pending_setup_intent'],
1458
+ });
1448
1459
  }
1449
1460
  if (exist) {
1461
+ if (!exist.default_payment_method) {
1462
+ const settings = PaymentMethod.decryptSettings(paymentMethod.settings);
1463
+ stripeContext = {
1464
+ type: 'subscription',
1465
+ id: exist.id,
1466
+ client_secret:
1467
+ // @ts-ignore
1468
+ exist.latest_invoice?.payment_intent?.client_secret || exist.pending_setup_intent?.client_secret,
1469
+ // @ts-ignore
1470
+ intent_type: exist.latest_invoice?.payment_intent ? 'payment_intent' : 'setup_intent',
1471
+ publishable_key: settings.stripe?.publishable_key,
1472
+ status: exist.status,
1473
+ };
1474
+ return res.json({
1475
+ setupIntent,
1476
+ stripeContext,
1477
+ subscription,
1478
+ customer,
1479
+ delegation,
1480
+ });
1481
+ }
1450
1482
  if (exist.status === 'paused') {
1451
1483
  await client.subscriptions.resume(exist.id, {
1452
1484
  proration_behavior: 'none',
@@ -1457,6 +1489,7 @@ router.post('/:id/change-payment', authPortal, async (req, res) => {
1457
1489
  proration_behavior: 'none',
1458
1490
  pause_collection: '',
1459
1491
  });
1492
+ await Lock.acquire(`${subscription.id}-change-plan`, subscription.current_period_end);
1460
1493
  logger.info('stripe subscription updated on subscription payment change', {
1461
1494
  subscription: subscription.id,
1462
1495
  intent: setupIntent.id,
@@ -1480,6 +1513,7 @@ router.post('/:id/change-payment', authPortal, async (req, res) => {
1480
1513
  const settings = PaymentMethod.decryptSettings(paymentMethod.settings);
1481
1514
 
1482
1515
  // changing from crypto to stripe: create/resume stripe subscription, pause crypto subscription
1516
+ const stripeCustomer = await ensureStripePaymentCustomer(subscription, paymentMethod);
1483
1517
  const stripeSubscription = await ensureStripeSubscription(
1484
1518
  subscription,
1485
1519
  paymentMethod,
@@ -1488,7 +1522,18 @@ router.post('/:id/change-payment', authPortal, async (req, res) => {
1488
1522
  0,
1489
1523
  subscription.current_period_end
1490
1524
  );
1491
-
1525
+ if (stripeSubscription) {
1526
+ await subscription.update({
1527
+ payment_details: {
1528
+ ...subscription.payment_details,
1529
+ stripe: {
1530
+ customer_id: stripeCustomer.id,
1531
+ subscription_id: stripeSubscription.id,
1532
+ setup_intent_id: stripeSubscription.pending_setup_intent?.id,
1533
+ },
1534
+ },
1535
+ });
1536
+ }
1492
1537
  stripeContext = {
1493
1538
  type: 'subscription',
1494
1539
  id: stripeSubscription.id,
@@ -1500,7 +1545,6 @@ router.post('/:id/change-payment', authPortal, async (req, res) => {
1500
1545
  publishable_key: settings.stripe?.publishable_key,
1501
1546
  status: stripeSubscription.status,
1502
1547
  };
1503
-
1504
1548
  await setupIntent.update({
1505
1549
  setup_details: {
1506
1550
  stripe: {
@@ -1519,6 +1563,10 @@ router.post('/:id/change-payment', authPortal, async (req, res) => {
1519
1563
  userDid: customer!.did,
1520
1564
  amount: getFastCheckoutAmount(lineItems, 'subscription', paymentCurrency.id, false),
1521
1565
  });
1566
+ if (paymentMethod.type === 'arcblock' && delegation.sufficient) {
1567
+ delegation.sufficient = false;
1568
+ delegation.reason = 'NO_ENOUGH_TOKEN';
1569
+ }
1522
1570
  if (delegation.sufficient) {
1523
1571
  await setupIntent.update({
1524
1572
  status: 'succeeded',
@@ -1541,7 +1589,7 @@ router.post('/:id/change-payment', authPortal, async (req, res) => {
1541
1589
 
1542
1590
  // NOTE: this should only happen when local subscription is updated
1543
1591
  // changing from stripe to crypto: pause stripe subscription
1544
- if (previousPaymentMethod!.type === 'stripe') {
1592
+ if (previousPaymentMethod!.type === 'stripe' && setupIntent.status === 'succeeded') {
1545
1593
  const client = await previousPaymentMethod?.getStripeClient();
1546
1594
  const stripeSubscriptionId = subscription.payment_details?.stripe?.subscription_id as string;
1547
1595
  const result = await client?.subscriptions.update(stripeSubscriptionId, {
@@ -1569,7 +1617,6 @@ router.post('/:id/change-payment', authPortal, async (req, res) => {
1569
1617
  return res.status(500).json({ code: err.code, error: err.message });
1570
1618
  }
1571
1619
  });
1572
-
1573
1620
  // FIXME: this should be removed in future
1574
1621
  // Clean up subscriptions that have invalid invoices and payments
1575
1622
  router.delete('/cleanup', auth, async (req, res) => {
@@ -1767,4 +1814,6 @@ router.get('/:id/payer-token', authMine, async (req, res) => {
1767
1814
  const token = await getTokenByAddress(paymentAddress, paymentMethod, paymentCurrency);
1768
1815
  return res.json({ token, paymentAddress });
1769
1816
  });
1817
+
1818
+ router.get('/:id/change-plan');
1770
1819
  export default router;
@@ -59,7 +59,8 @@ export class Invoice extends Model<InferAttributes<Invoice>, InferCreationAttrib
59
59
  | 'subscription'
60
60
  | 'manual'
61
61
  | 'upcoming'
62
- | 'slash_stake',
62
+ | 'slash_stake'
63
+ | 'stake',
63
64
  string
64
65
  >;
65
66
 
package/blocklet.yml CHANGED
@@ -14,7 +14,7 @@ repository:
14
14
  type: git
15
15
  url: git+https://github.com/blocklet/payment-kit.git
16
16
  specVersion: 1.2.8
17
- version: 1.15.33
17
+ version: 1.15.34
18
18
  logo: logo.png
19
19
  files:
20
20
  - dist
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "payment-kit",
3
- "version": "1.15.33",
3
+ "version": "1.15.34",
4
4
  "scripts": {
5
5
  "dev": "blocklet dev --open",
6
6
  "eject": "vite eject",
@@ -53,7 +53,7 @@
53
53
  "@arcblock/validator": "^1.18.139",
54
54
  "@blocklet/js-sdk": "1.16.33-beta-20241031-073543-49b1ff9b",
55
55
  "@blocklet/logger": "1.16.33-beta-20241031-073543-49b1ff9b",
56
- "@blocklet/payment-react": "1.15.33",
56
+ "@blocklet/payment-react": "1.15.34",
57
57
  "@blocklet/sdk": "1.16.33-beta-20241031-073543-49b1ff9b",
58
58
  "@blocklet/ui-react": "^2.10.65",
59
59
  "@blocklet/uploader": "^0.1.51",
@@ -120,7 +120,7 @@
120
120
  "devDependencies": {
121
121
  "@abtnode/types": "1.16.33-beta-20241031-073543-49b1ff9b",
122
122
  "@arcblock/eslint-config-ts": "^0.3.3",
123
- "@blocklet/payment-types": "1.15.33",
123
+ "@blocklet/payment-types": "1.15.34",
124
124
  "@types/cookie-parser": "^1.4.7",
125
125
  "@types/cors": "^2.8.17",
126
126
  "@types/debug": "^4.1.12",
@@ -165,5 +165,5 @@
165
165
  "parser": "typescript"
166
166
  }
167
167
  },
168
- "gitHead": "88e0986e379d9ed673435ea96305f16b4a54097e"
168
+ "gitHead": "95c0d3ddd12382f694014c05015ca46c19424e3e"
169
169
  }