payment-kit 1.19.7 → 1.19.9

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/index.ts CHANGED
@@ -10,6 +10,7 @@ import express, { ErrorRequestHandler } from 'express';
10
10
  // eslint-disable-next-line import/no-extraneous-dependencies
11
11
  import { xss } from '@blocklet/xss';
12
12
  import { csrf } from '@blocklet/sdk/lib/middlewares';
13
+ import { CustomError, formatError, getStatusFromError } from '@blocklet/error';
13
14
 
14
15
  import crons from './crons/index';
15
16
  import { ensureStakedForGas } from './integrations/arcblock/stake';
@@ -94,18 +95,22 @@ if (isProduction) {
94
95
  const staticDir = path.resolve(process.env.BLOCKLET_APP_DIR!, 'dist');
95
96
  app.use(express.static(staticDir, { maxAge: '30d', index: false }));
96
97
  app.use(fallback('index.html', { root: staticDir }));
97
-
98
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
99
- app.use(<ErrorRequestHandler>((err, req, res, _next) => {
100
- logger.error(err);
101
- if (req.accepts('json')) {
102
- res.status(500).send({ error: err.message });
103
- } else {
104
- res.status(500).send('Something broke!');
105
- }
106
- }));
107
98
  }
108
99
 
100
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
101
+ app.use(<ErrorRequestHandler>((err, req, res, _next) => {
102
+ logger.error(err);
103
+ if (err instanceof CustomError) {
104
+ res.status(getStatusFromError(err)).json({ error: formatError(err) });
105
+ return;
106
+ }
107
+ if (req.accepts('json')) {
108
+ res.status(500).send({ error: err.message });
109
+ } else {
110
+ res.status(500).send('Something broke!');
111
+ }
112
+ }));
113
+
109
114
  const port = parseInt(process.env.BLOCKLET_PORT!, 10);
110
115
 
111
116
  export const server = app.listen(port, (err?: any) => {
@@ -1,7 +1,7 @@
1
1
  import { Notification as BlockletNotification } from '@blocklet/sdk';
2
2
 
3
3
  import type { BaseEmailTemplate, BaseEmailTemplateType } from './template/base';
4
- import { CheckoutSession, Invoice, Subscription } from '../../store/models';
4
+ import { CheckoutSession, CreditGrant, Invoice, Meter, MeterEvent, Subscription } from '../../store/models';
5
5
  import { getNotificationSettings, shouldSendSystemNotification } from '../setting';
6
6
  import logger from '../logger';
7
7
  import { events } from '../event';
@@ -86,6 +86,33 @@ export class Notification {
86
86
  }
87
87
  }
88
88
 
89
+ if (options.creditGrantId) {
90
+ const creditGrant = await CreditGrant.findByPk(options.creditGrantId);
91
+ if (creditGrant) {
92
+ const meter = await Meter.findOne({
93
+ where: {
94
+ currency_id: creditGrant.currency_id,
95
+ status: 'active',
96
+ },
97
+ });
98
+ if (meter) {
99
+ return { meter, id: meter.id };
100
+ }
101
+ }
102
+ }
103
+ if (options.meterEventName) {
104
+ const meterEvent = await MeterEvent.findOne({
105
+ where: {
106
+ event_name: options.meterEventName,
107
+ },
108
+ });
109
+ if (meterEvent) {
110
+ const meter = await Meter.getMeterByEventName(meterEvent.event_name);
111
+ if (meter) {
112
+ return { meter, id: meter.id };
113
+ }
114
+ }
115
+ }
89
116
  return null;
90
117
  }
91
118
  }
@@ -59,6 +59,7 @@ export async function checkTokenBalance(args: {
59
59
  paymentCurrency: TPaymentCurrency;
60
60
  userDid: string;
61
61
  amount: string;
62
+ skipUserCheck?: boolean;
62
63
  }): Promise<SufficientForPaymentResult> {
63
64
  const { paymentMethod, paymentCurrency, userDid, amount } = args;
64
65
  const tokenAddress = paymentCurrency.contract as string;
@@ -66,8 +67,20 @@ export async function checkTokenBalance(args: {
66
67
 
67
68
  if (paymentMethod.type === 'arcblock') {
68
69
  // get user wallet did
69
- const { user } = await blocklet.getUser(userDid, { enableConnectedAccount: true });
70
- const delegator = getWalletDid(user);
70
+ let delegator = userDid;
71
+ if (!args.skipUserCheck) {
72
+ const { user } = await blocklet.getUser(userDid, { enableConnectedAccount: true });
73
+ if (!user) {
74
+ return {
75
+ sufficient: false,
76
+ reason: 'NO_CUSTOMER',
77
+ requestedAmount: totalAmount.toString(),
78
+ };
79
+ }
80
+ if (user) {
81
+ delegator = getWalletDid(user);
82
+ }
83
+ }
71
84
  if (!delegator) {
72
85
  return {
73
86
  sufficient: false,
@@ -226,10 +239,13 @@ export async function isDelegationSufficientForPayment(args: {
226
239
 
227
240
  // Regular token handling for non-credit currencies
228
241
  // user have bond wallet did?
242
+ let delegator = userDid;
229
243
  const { user } = await blocklet.getUser(userDid, { enableConnectedAccount: true });
230
- const delegator = getWalletDid(user);
231
- if (!delegator) {
232
- return { sufficient: false, reason: 'NO_DID_WALLET' };
244
+ if (user) {
245
+ delegator = getWalletDid(user);
246
+ if (!delegator) {
247
+ return { sufficient: false, reason: 'NO_DID_WALLET' };
248
+ }
233
249
  }
234
250
 
235
251
  const client = paymentMethod.getOcapClient();
@@ -1,5 +1,5 @@
1
1
  import { Op } from 'sequelize';
2
- import { CheckoutSession, Invoice, Setting, Subscription } from '../store/models';
2
+ import { CheckoutSession, Invoice, Meter, Setting, Subscription } from '../store/models';
3
3
  import type { EventType, NotificationSetting } from '../store/models/types';
4
4
  import logger from './logger';
5
5
 
@@ -121,15 +121,29 @@ export async function getNotificationSettingFromInvoice(invoice: Invoice): Promi
121
121
  return null;
122
122
  }
123
123
 
124
+ export async function getNotificationSettingFromMeter(meter: Meter): Promise<NotificationSetting | null> {
125
+ if (!meter) {
126
+ return null;
127
+ }
128
+ const settingId = getSettingIdFromObject(meter);
129
+ if (settingId) {
130
+ const settings = await getSettingForNotification(settingId);
131
+ return settings || null;
132
+ }
133
+ return null;
134
+ }
135
+
124
136
  export type NotificationSettingsProps = {
125
137
  subscription?: Subscription;
126
138
  invoice?: Invoice;
127
139
  checkoutSession?: CheckoutSession;
140
+ meter?: Meter;
128
141
  };
129
142
  export async function getNotificationSettings({
130
143
  subscription,
131
144
  invoice,
132
145
  checkoutSession,
146
+ meter,
133
147
  }: NotificationSettingsProps): Promise<NotificationSetting | null> {
134
148
  if (subscription) {
135
149
  const settings = await getNotificationSettingFromSubscription(subscription);
@@ -146,6 +160,11 @@ export async function getNotificationSettings({
146
160
  return settings || null;
147
161
  }
148
162
 
163
+ if (meter) {
164
+ const settings = await getNotificationSettingFromMeter(meter);
165
+ return settings || null;
166
+ }
167
+
149
168
  return null;
150
169
  }
151
170
 
@@ -229,7 +229,7 @@ export default flat({
229
229
  },
230
230
 
231
231
  creditGrantGranted: {
232
- title: '恭喜!您的额度授予已激活',
232
+ title: '恭喜!您的额度已激活',
233
233
  body: '您已获得 {grantedAmount} 的额度授予,激活时间为 {at},有效期至 {expiresAt}。祝您使用愉快!',
234
234
  bodyNoExpire: '您已获得 {grantedAmount} 的额度授予,激活时间为 {at}。祝您使用愉快!',
235
235
  grantedCredit: '授予额度',
@@ -238,8 +238,8 @@ export default flat({
238
238
  },
239
239
 
240
240
  creditGrantLowBalance: {
241
- title: '额度授予余额不足提醒',
242
- body: '您的额度授予余额已低于 10%,当前剩余额度为 {availableAmount}。请及时充值或联系管理员以避免服务受限。',
241
+ title: '额度余额不足提醒',
242
+ body: '您的额度已低于 10%,当前剩余额度为 {availableAmount}。请及时充值或联系管理员以避免服务受限。',
243
243
  totalGrantedCredit: '总授予额度',
244
244
  },
245
245
  },
@@ -13,6 +13,7 @@ import sortBy from 'lodash/sortBy';
13
13
  import uniq from 'lodash/uniq';
14
14
  import type { WhereOptions } from 'sequelize';
15
15
 
16
+ import { CustomError, formatError, getStatusFromError } from '@blocklet/error';
16
17
  import { MetadataSchema } from '../libs/api';
17
18
  import { checkPassportForPaymentLink } from '../integrations/blocklet/passport';
18
19
  import dayjs from '../libs/dayjs';
@@ -418,7 +419,7 @@ export const formatCheckoutSession = async (payload: any, throwOnEmptyItems = tr
418
419
 
419
420
  // TODO: need to support stake subscription
420
421
  if (raw.enable_subscription_grouping === true && !raw.subscription_data?.no_stake) {
421
- throw new Error('Subscription grouping is only supported for stake-free subscriptions');
422
+ throw new CustomError(400, 'Subscription grouping is only supported for stake-free subscriptions');
422
423
  }
423
424
 
424
425
  if (payload.include_free_trial && raw.subscription_data) {
@@ -428,7 +429,7 @@ export const formatCheckoutSession = async (payload: any, throwOnEmptyItems = tr
428
429
  if (raw.subscription_data?.service_actions) {
429
430
  const { error } = SubscriptionDataSchema.validate(raw.subscription_data);
430
431
  if (error) {
431
- throw new Error('Invalid service actions for checkout session');
432
+ throw new CustomError(400, 'Invalid service actions for checkout session');
432
433
  }
433
434
  }
434
435
 
@@ -438,7 +439,7 @@ export const formatCheckoutSession = async (payload: any, throwOnEmptyItems = tr
438
439
 
439
440
  if (raw.nft_mint_settings?.enabled) {
440
441
  if (!raw.nft_mint_settings?.factory) {
441
- throw new Error('factory is required when nft mint is enabled');
442
+ throw new CustomError(400, 'factory is required when nft mint is enabled');
442
443
  }
443
444
  }
444
445
 
@@ -457,20 +458,20 @@ export const formatCheckoutSession = async (payload: any, throwOnEmptyItems = tr
457
458
 
458
459
  const items = await Price.expand(raw.line_items as any[]);
459
460
  if (items.some((x) => !x.price)) {
460
- throw new Error('Invalid line items for checkout session, some price may have been deleted');
461
+ throw new CustomError(400, 'Invalid line items for checkout session, some price may have been deleted');
461
462
  }
462
463
  if (items.some((x) => !x.price.active)) {
463
- throw new Error('Invalid line items for checkout session, some price may have been archived');
464
+ throw new CustomError(400, 'Invalid line items for checkout session, some price may have been archived');
464
465
  }
465
466
  const enableSubscriptionGrouping = payload.enable_subscription_grouping;
466
467
  for (let i = 0; i < items.length; i++) {
467
468
  const result = isLineItemAligned(items, i);
468
469
  if (result.currency === false) {
469
- throw new Error('line_items should have same currency');
470
+ throw new CustomError(400, 'line_items should have same currency');
470
471
  }
471
472
  // if subscription grouping is not enabled, we need to check the recurring
472
473
  if (result.recurring === false && !enableSubscriptionGrouping) {
473
- throw new Error('line_items should have same recurring');
474
+ throw new CustomError(400, 'line_items should have same recurring');
474
475
  }
475
476
  }
476
477
 
@@ -790,6 +791,10 @@ export async function startCheckoutSessionFromPaymentLink(id: string, req: Reque
790
791
  raw.created_via = 'portal';
791
792
  raw.submit_type = link.submit_type;
792
793
  raw.currency_id = link.currency_id || req.currency.id;
794
+ if (!raw.currency_id) {
795
+ res.status(400).json({ error: 'Currency not found in payment link' });
796
+ return;
797
+ }
793
798
  raw.payment_link_id = link.id;
794
799
 
795
800
  // Inherit multi-subscription settings from payment link
@@ -927,7 +932,11 @@ export async function startCheckoutSessionFromPaymentLink(id: string, req: Reque
927
932
  });
928
933
  } catch (err) {
929
934
  logger.error(err);
930
- res.status(500).json({ error: err.message });
935
+ if (err instanceof CustomError) {
936
+ res.status(getStatusFromError(err)).json({ error: formatError(err) });
937
+ } else {
938
+ res.status(500).json({ error: err.message });
939
+ }
931
940
  }
932
941
  }
933
942
 
@@ -2043,7 +2052,13 @@ router.put('/:id/amount', ensureCheckoutSessionOpen, async (req, res) => {
2043
2052
  // validate amount on donation settings
2044
2053
  if (checkoutSession.payment_link_id) {
2045
2054
  const link = await PaymentLink.findByPk(checkoutSession.payment_link_id);
2055
+ if (!checkoutSession.currency_id) {
2056
+ return res.status(400).json({ error: 'Currency not found in checkout session' });
2057
+ }
2046
2058
  const currency = await PaymentCurrency.findByPk(checkoutSession.currency_id);
2059
+ if (!currency) {
2060
+ return res.status(404).json({ error: 'Currency not found' });
2061
+ }
2047
2062
  if (link?.donation_settings?.amount && currency) {
2048
2063
  const input = Number(fromUnitToToken(amount, currency.decimal));
2049
2064
  const { minimum, maximum, presets, custom } = link.donation_settings.amount;
@@ -2088,10 +2103,13 @@ router.put('/:id/amount', ensureCheckoutSessionOpen, async (req, res) => {
2088
2103
  // recalculate amount
2089
2104
  await checkoutSession.update(await getCheckoutSessionAmounts(checkoutSession));
2090
2105
 
2091
- res.json({ ...checkoutSession.toJSON(), line_items: await Price.expand(newItems) });
2106
+ return res.json({ ...checkoutSession.toJSON(), line_items: await Price.expand(newItems) });
2092
2107
  } catch (err) {
2093
2108
  logger.error(err);
2094
- res.status(500).json({ error: err.message });
2109
+ if (err instanceof CustomError) {
2110
+ return res.status(getStatusFromError(err)).json({ error: formatError(err) });
2111
+ }
2112
+ return res.status(500).json({ error: err.message });
2095
2113
  }
2096
2114
  });
2097
2115
 
@@ -32,11 +32,9 @@ export default {
32
32
  },
33
33
  onConnect: async (args: CallbackArgs) => {
34
34
  const { userDid, userPk, extraParams } = args;
35
- const { checkoutSessionId, connectedDid, sessionUserDid } = extraParams;
36
- const { paymentMethod, paymentCurrency, checkoutSession, subscription, customer } = await ensureSetupIntent(
37
- checkoutSessionId,
38
- connectedDid || sessionUserDid || userDid
39
- );
35
+ const { checkoutSessionId } = extraParams;
36
+ const { paymentMethod, paymentCurrency, checkoutSession, subscription, customer } =
37
+ await ensureSetupIntent(checkoutSessionId);
40
38
  if (!subscription) {
41
39
  throw new Error('Subscription for checkoutSession not found');
42
40
  }
@@ -121,9 +119,9 @@ export default {
121
119
  },
122
120
  onAuth: async (args: CallbackArgs) => {
123
121
  const { request, userDid, userPk, claims, extraParams, updateSession, step } = args;
124
- const { checkoutSessionId, connectedDid, sessionUserDid } = extraParams;
122
+ const { checkoutSessionId } = extraParams;
125
123
  const { setupIntent, checkoutSession, paymentMethod, subscription, invoice, paymentCurrency, customer } =
126
- await ensureSetupIntent(checkoutSessionId, connectedDid || sessionUserDid || userDid);
124
+ await ensureSetupIntent(checkoutSessionId);
127
125
 
128
126
  if (!subscription) {
129
127
  throw new Error('Subscription for checkoutSession not found');
@@ -13,14 +13,14 @@ import { encodeApproveItx } from '../../integrations/ethereum/token';
13
13
  import { blocklet, ethWallet, wallet } from '../../libs/auth';
14
14
  import logger from '../../libs/logger';
15
15
  import { getGasPayerExtra, getTokenLimitsForDelegation } from '../../libs/payment';
16
- import {
17
- getCheckoutAmount,
18
- getCheckoutSessionSubscriptionIds,
19
- getFastCheckoutAmount,
20
- getStatementDescriptor,
21
- getSubscriptionCreateSetup,
16
+ import {
17
+ getCheckoutAmount,
18
+ getCheckoutSessionSubscriptionIds,
19
+ getFastCheckoutAmount,
20
+ getStatementDescriptor,
21
+ getSubscriptionCreateSetup,
22
22
  isDonationCheckoutSession,
23
- getSubscriptionLineItems
23
+ getSubscriptionLineItems,
24
24
  } from '../../libs/session';
25
25
  import {
26
26
  expandSubscriptionItems,
@@ -68,7 +68,11 @@ export async function ensureCheckoutSession(checkoutSessionId: string) {
68
68
  return checkoutSession;
69
69
  }
70
70
 
71
- export async function ensurePaymentIntent(checkoutSessionId: string, userDid?: string, skipCustomer?: boolean): Promise<Result> {
71
+ export async function ensurePaymentIntent(
72
+ checkoutSessionId: string,
73
+ userDid?: string,
74
+ skipCustomer?: boolean
75
+ ): Promise<Result> {
72
76
  const checkoutSession = await ensureCheckoutSession(checkoutSessionId);
73
77
 
74
78
  let paymentCurrencyId;
@@ -100,11 +104,8 @@ export async function ensurePaymentIntent(checkoutSessionId: string, userDid?: s
100
104
  let primarySubscription: Subscription | null = null;
101
105
  if (subscriptionIds.length > 0) {
102
106
  // @ts-ignore
103
- subscriptions = await Promise.all(
104
- subscriptionIds.filter(Boolean)
105
- .map(id => Subscription.findByPk(id))
106
- );
107
-
107
+ subscriptions = await Promise.all(subscriptionIds.filter(Boolean).map((id) => Subscription.findByPk(id)));
108
+
108
109
  subscriptions = subscriptions.filter(Boolean);
109
110
  for (const subscription of subscriptions) {
110
111
  if (subscription && subscription.status !== 'incomplete') {
@@ -119,81 +120,69 @@ export async function ensurePaymentIntent(checkoutSessionId: string, userDid?: s
119
120
  }
120
121
  }
121
122
  let customer = null;
122
-
123
+
123
124
  if (!skipCustomer) {
124
- // 检查是否为打赏场景
125
- const isDonation = isDonationCheckoutSession(checkoutSession);
126
-
127
- // if donation, create customer if not exists
128
- if (isDonation && !checkoutSession.customer_id && userDid) {
129
- customer = await Customer.findByPkOrDid(userDid);
130
- if (!customer) {
131
- const { user } = await blocklet.getUser(userDid);
132
- if (user) {
133
- customer = await Customer.create({
134
- did: userDid,
135
- email: user.email,
136
- name: user.fullName || userDid,
137
- description: user.remark,
138
- metadata: { fromDonation: true },
139
- livemode: checkoutSession.livemode,
140
- phone: user.phone,
141
- address: Customer.formatAddressFromUser(user),
142
- delinquent: false,
143
- balance: '0',
144
- next_invoice_sequence: 1,
145
- invoice_prefix: Customer.getInvoicePrefix(),
146
- });
147
- logger.info('Customer created for donation', { userDid, customerId: customer.id });
148
- } else {
149
- customer = await Customer.create({
150
- did: userDid,
151
- email: '',
152
- name: 'anonymous',
153
- description: 'Anonymous customer',
154
- metadata: { fromDonation: true, anonymous: true },
155
- livemode: checkoutSession.livemode,
156
- phone:'',
157
- delinquent: false,
158
- balance: '0',
159
- next_invoice_sequence: 1,
160
- invoice_prefix: Customer.getInvoicePrefix(),
161
- });
125
+ // 检查是否为打赏场景
126
+ const isDonation = isDonationCheckoutSession(checkoutSession);
127
+
128
+ // if donation, create customer if not exists
129
+ if (isDonation && !checkoutSession.customer_id && userDid) {
130
+ customer = await Customer.findByPkOrDid(userDid);
131
+ if (!customer) {
132
+ const { user } = await blocklet.getUser(userDid);
133
+ if (user) {
134
+ customer = await Customer.create({
135
+ did: userDid,
136
+ email: user.email,
137
+ name: user.fullName || userDid,
138
+ description: user.remark,
139
+ metadata: { fromDonation: true },
140
+ livemode: checkoutSession.livemode,
141
+ phone: user.phone,
142
+ address: Customer.formatAddressFromUser(user),
143
+ delinquent: false,
144
+ balance: '0',
145
+ next_invoice_sequence: 1,
146
+ invoice_prefix: Customer.getInvoicePrefix(),
147
+ });
148
+ logger.info('Customer created for donation', { userDid, customerId: customer.id });
149
+ } else {
150
+ customer = await Customer.create({
151
+ did: userDid,
152
+ email: '',
153
+ name: 'anonymous',
154
+ description: 'Anonymous customer',
155
+ metadata: { fromDonation: true, anonymous: true },
156
+ livemode: checkoutSession.livemode,
157
+ phone: '',
158
+ delinquent: false,
159
+ balance: '0',
160
+ next_invoice_sequence: 1,
161
+ invoice_prefix: Customer.getInvoicePrefix(),
162
+ });
163
+ }
162
164
  }
163
- }
164
-
165
- if (customer) {
166
- await checkoutSession.update({ customer_id: customer.id, customer_did: customer.did });
167
- if (paymentIntent) {
168
- await paymentIntent.update({ customer_id: customer.id });
165
+
166
+ if (customer) {
167
+ await checkoutSession.update({ customer_id: customer.id, customer_did: customer.did });
168
+ if (paymentIntent) {
169
+ await paymentIntent.update({ customer_id: customer.id });
170
+ }
171
+ logger.info('Customer associated with donation', {
172
+ userDid,
173
+ customerId: customer.id,
174
+ checkoutSessionId: checkoutSession.id,
175
+ paymentIntentId: paymentIntent?.id,
176
+ });
169
177
  }
170
- logger.info('Customer associated with donation', {
171
- userDid,
172
- customerId: customer.id,
173
- checkoutSessionId: checkoutSession.id,
174
- paymentIntentId: paymentIntent?.id
175
- });
176
- }
177
- } else {
178
- // 非打赏场景或已有客户ID的情况
179
- customer = await Customer.findByPk(checkoutSession.customer_id);
180
- }
181
- if (!customer) {
182
- throw new Error('Customer not found');
183
- }
184
-
185
- if (userDid && !isDonation) {
186
- const { user } = await blocklet.getUser(userDid, { enableConnectedAccount: true });
187
- if (!user) {
188
- throw new Error('Seems you have not connected to this app before');
178
+ } else {
179
+ // 非打赏场景或已有客户ID的情况
180
+ customer = await Customer.findByPk(checkoutSession.customer_id);
189
181
  }
190
-
191
- if (customer.did !== user.did) {
192
- throw new Error('This is not your payment intent');
182
+ if (!customer) {
183
+ throw new Error('Customer not found');
193
184
  }
194
185
  }
195
- }
196
-
197
186
 
198
187
  const [paymentMethod, paymentCurrency] = await Promise.all([
199
188
  PaymentMethod.findByPk(paymentMethodId),
@@ -223,7 +212,7 @@ export async function ensurePaymentIntent(checkoutSessionId: string, userDid?: s
223
212
  };
224
213
  }
225
214
 
226
- export async function ensureSetupIntent(checkoutSessionId: string, userDid?: string) {
215
+ export async function ensureSetupIntent(checkoutSessionId: string) {
227
216
  const checkoutSession = await ensureCheckoutSession(checkoutSessionId);
228
217
 
229
218
  if (!checkoutSession.setup_intent_id) {
@@ -256,16 +245,6 @@ export async function ensureSetupIntent(checkoutSessionId: string, userDid?: str
256
245
  if (!customer) {
257
246
  throw new Error('Customer not found for checkoutSession');
258
247
  }
259
- if (userDid) {
260
- const { user } = await blocklet.getUser(userDid, { enableConnectedAccount: true });
261
- if (!user) {
262
- throw new Error('Seems you have not connected to this app before');
263
- }
264
-
265
- if (customer.did !== user.did) {
266
- throw new Error('This is not your setupIntent');
267
- }
268
- }
269
248
 
270
249
  const [paymentMethod, paymentCurrency] = await Promise.all([
271
250
  PaymentMethod.findByPk(subscription.default_payment_method_id),
@@ -344,8 +323,13 @@ export async function ensureSetupIntent(checkoutSessionId: string, userDid?: str
344
323
  export async function ensureSubscriptionDelegation(subscriptionId: string) {
345
324
  const subscription = (await Subscription.findOne({
346
325
  where: { id: subscriptionId },
347
- include: [{ model: PaymentCurrency, as: 'paymentCurrency' }, { model: PaymentMethod, as: 'paymentMethod' }],
348
- })) as (Subscription & { paymentCurrency: PaymentCurrency; paymentMethod: PaymentMethod; items?: TLineItemExpanded[] }) | null;
326
+ include: [
327
+ { model: PaymentCurrency, as: 'paymentCurrency' },
328
+ { model: PaymentMethod, as: 'paymentMethod' },
329
+ ],
330
+ })) as
331
+ | (Subscription & { paymentCurrency: PaymentCurrency; paymentMethod: PaymentMethod; items?: TLineItemExpanded[] })
332
+ | null;
349
333
  if (!subscription) {
350
334
  throw new Error('Subscription not found');
351
335
  }
@@ -380,7 +364,6 @@ export async function ensureInvoiceForCheckout({
380
364
  subscriptions,
381
365
  lineItems,
382
366
  }: Args): Promise<{ invoice: Invoice | null; items: InvoiceItem[] }> {
383
-
384
367
  const isGroupInvoice = subscriptions && subscriptions.length > 1;
385
368
  // invoices are optional when checkout session is in payment mode
386
369
  if (checkoutSession.mode === 'payment' && !checkoutSession.invoice_creation?.enabled) {
@@ -426,9 +409,7 @@ export async function ensureInvoiceForCheckout({
426
409
  client.invoices
427
410
  .voidInvoice(invoice.metadata.stripe_id)
428
411
  .then(() => {
429
- logger.info(
430
- `Invoice marked void on stripe for checkout session ${checkoutSession.id}: ${existingInvoice}`
431
- );
412
+ logger.info(`Invoice marked void on stripe for checkout session ${checkoutSession.id}: ${existingInvoice}`);
432
413
  })
433
414
  .catch((err) => {
434
415
  logger.error(
@@ -441,16 +422,16 @@ export async function ensureInvoiceForCheckout({
441
422
  }
442
423
 
443
424
  const currency = await PaymentCurrency.findByPk(checkoutSession.currency_id);
444
-
425
+
445
426
  const metadata = {
446
427
  ...(checkoutSession.invoice_creation?.invoice_data?.metadata || {}),
447
428
  };
448
-
429
+
449
430
  if (isGroupInvoice) {
450
- metadata.subscription_ids = subscriptions.map(sub => sub.id);
431
+ metadata.subscription_ids = subscriptions.map((sub) => sub.id);
451
432
  metadata.is_group_invoice = true;
452
433
  }
453
-
434
+
454
435
  const trialInDays = Number(checkoutSession.subscription_data?.trial_period_days || 0);
455
436
  const trialEnd = Number(checkoutSession.subscription_data?.trial_end || 0);
456
437
  const now = dayjs().unix();
@@ -507,14 +488,13 @@ export async function ensureInvoiceForCheckout({
507
488
  return { invoice, items };
508
489
  }
509
490
 
510
-
511
491
  export async function ensureInvoicesForSubscriptions({
512
492
  checkoutSession,
513
493
  customer,
514
494
  subscriptions,
515
495
  invoiceProps,
516
- }: Omit<Args, 'subscription'> & { subscriptions: Subscription[]; invoiceProps?: Partial<TInvoice> }): Promise<{
517
- invoices: Invoice[];
496
+ }: Omit<Args, 'subscription'> & { subscriptions: Subscription[]; invoiceProps?: Partial<TInvoice> }): Promise<{
497
+ invoices: Invoice[];
518
498
  }> {
519
499
  if (!subscriptions?.length) {
520
500
  logger.warn('No subscriptions provided for invoice creation');
@@ -523,15 +503,16 @@ export async function ensureInvoicesForSubscriptions({
523
503
 
524
504
  const lineItems = await Price.expand(checkoutSession.line_items, { product: true });
525
505
 
526
- const primarySubscription = (subscriptions.find((x) => x.metadata.is_primary_subscription) || subscriptions[0]) as Subscription;
506
+ const primarySubscription = (subscriptions.find((x) => x.metadata.is_primary_subscription) ||
507
+ subscriptions[0]) as Subscription;
527
508
  const invoices = await Promise.all(
528
509
  subscriptions.map(async (subscription) => {
529
510
  const subItems = await getSubscriptionLineItems(subscription, lineItems, primarySubscription);
530
- const { invoice } = await ensureInvoiceForCheckout({
531
- checkoutSession,
532
- customer,
533
- subscription,
534
- subscriptions,
511
+ const { invoice } = await ensureInvoiceForCheckout({
512
+ checkoutSession,
513
+ customer,
514
+ subscription,
515
+ subscriptions,
535
516
  lineItems: subItems,
536
517
  props: invoiceProps,
537
518
  });
@@ -540,10 +521,10 @@ export async function ensureInvoicesForSubscriptions({
540
521
  );
541
522
 
542
523
  const createdInvoices = invoices.filter(Boolean);
543
-
524
+
544
525
  logger.info(`Created ${createdInvoices.length} invoices for subscriptions`, {
545
526
  checkoutSessionId: checkoutSession.id,
546
- invoiceIds: createdInvoices.map(inv => inv?.id)
527
+ invoiceIds: createdInvoices.map((inv) => inv?.id),
547
528
  });
548
529
 
549
530
  return { invoices: createdInvoices as Invoice[] };
@@ -625,7 +606,7 @@ export async function ensureSubscriptionRecharge(subscriptionId: string) {
625
606
  paymentMethod: paymentMethod as PaymentMethod,
626
607
  receiverAddress,
627
608
  subscription,
628
- customer
609
+ customer,
629
610
  };
630
611
  }
631
612
 
@@ -715,7 +696,7 @@ export async function getDelegationTxClaim({
715
696
  billingThreshold,
716
697
  paymentMethod,
717
698
  paymentCurrency,
718
- requiredStake
699
+ requiredStake,
719
700
  });
720
701
  if (mode === 'delegation') {
721
702
  tokenRequirements = [];
@@ -788,7 +769,11 @@ export async function getDelegationTxClaim({
788
769
  throw new Error(`getDelegationTxClaim: Payment method ${paymentMethod.type} not supported`);
789
770
  }
790
771
 
791
- export function getStakeAmount(subscription: Subscription, paymentCurrency: PaymentCurrency, items: TLineItemExpanded[]) {
772
+ export function getStakeAmount(
773
+ subscription: Subscription,
774
+ paymentCurrency: PaymentCurrency,
775
+ items: TLineItemExpanded[]
776
+ ) {
792
777
  const billingThreshold = Number(subscription.billing_thresholds?.amount_gte || 0);
793
778
  const minStakeAmount = Number(subscription.billing_thresholds?.stake_gte || 0);
794
779
  const threshold = fromTokenToUnit(Math.max(billingThreshold, minStakeAmount), paymentCurrency.decimal);
@@ -816,29 +801,29 @@ export async function getStakeTxClaim({
816
801
  }) {
817
802
  let billingThreshold = Number(subscription.billing_thresholds?.amount_gte || 0);
818
803
  let minStakeAmount = Number(subscription.billing_thresholds?.stake_gte || 0);
819
-
804
+
820
805
  const hasGrouping = subscriptions && subscriptions.length > 1;
821
806
  if (hasGrouping) {
822
- const primarySubscription = subscriptions[0] as Subscription;
807
+ const primarySubscription = subscriptions[0] as Subscription;
823
808
  // use the settings of the primary subscription, not the scattered staking
824
809
  billingThreshold = Number(primarySubscription.billing_thresholds?.amount_gte || 0);
825
810
  minStakeAmount = Number(primarySubscription.billing_thresholds?.stake_gte || 0);
826
-
811
+
827
812
  logger.info('Using primary subscription for staking', {
828
813
  primarySubscriptionId: primarySubscription.id,
829
814
  billingThreshold,
830
815
  minStakeAmount,
831
- allSubscriptionIds: subscriptions.map(s => s.id)
816
+ allSubscriptionIds: subscriptions.map((s) => s.id),
832
817
  });
833
818
  }
834
-
819
+
835
820
  const threshold = fromTokenToUnit(Math.max(billingThreshold, minStakeAmount), paymentCurrency.decimal);
836
821
  const staking = getSubscriptionStakeSetup(items, paymentCurrency.id, threshold.toString());
837
822
  const amount = staking.licensed.add(staking.metered).toString();
838
-
823
+
839
824
  logger.info('getStakeTxClaim', {
840
825
  subscriptionId: subscription.id,
841
- allSubscriptions: subscriptions?.map(s => s.id) || [],
826
+ allSubscriptions: subscriptions?.map((s) => s.id) || [],
842
827
  billingThreshold,
843
828
  minStakeAmount,
844
829
  threshold: threshold.toString(),
@@ -849,11 +834,9 @@ export async function getStakeTxClaim({
849
834
  if (paymentMethod.type === 'arcblock') {
850
835
  // create staking data
851
836
  const client = paymentMethod.getOcapClient();
852
-
853
- const stakeId = hasGrouping
854
- ? `stake-group-${subscription.id}`
855
- : subscription.id;
856
-
837
+
838
+ const stakeId = hasGrouping ? `stake-group-${subscription.id}` : subscription.id;
839
+
857
840
  const address = await getCustomerStakeAddress(userDid, stakeId);
858
841
  const { state } = await client.getStakeState({ address });
859
842
  const data = {
@@ -862,10 +845,12 @@ export async function getStakeTxClaim({
862
845
  {
863
846
  appId: wallet.address,
864
847
  subscriptionId: subscription.id,
865
- ...(hasGrouping ? {
866
- subscriptionGroup: true,
867
- subscriptionIds: subscriptions.map(s => s.id)
868
- } : {})
848
+ ...(hasGrouping
849
+ ? {
850
+ subscriptionGroup: true,
851
+ subscriptionIds: subscriptions.map((s) => s.id),
852
+ }
853
+ : {}),
869
854
  },
870
855
  JSON.parse(state?.data?.value || '{}')
871
856
  ),
@@ -902,11 +887,13 @@ export async function getStakeTxClaim({
902
887
  meta: {
903
888
  purpose: 'staking',
904
889
  address,
905
- ...(hasGrouping ? {
906
- subscriptionGroup: true,
907
- primarySubscriptionId: subscription.id,
908
- allSubscriptionIds: subscriptions.map(s => s.id)
909
- } : {})
890
+ ...(hasGrouping
891
+ ? {
892
+ subscriptionGroup: true,
893
+ primarySubscriptionId: subscription.id,
894
+ allSubscriptionIds: subscriptions.map((s) => s.id),
895
+ }
896
+ : {}),
910
897
  },
911
898
  chainInfo: {
912
899
  host: paymentMethod.settings?.arcblock?.api_host as string,
@@ -1015,7 +1002,7 @@ export async function getTokenRequirements({
1015
1002
  paymentCurrency,
1016
1003
  trialing = false,
1017
1004
  billingThreshold = 0,
1018
- requiredStake
1005
+ requiredStake,
1019
1006
  }: TokenRequirementArgs) {
1020
1007
  const tokenRequirements = [];
1021
1008
  let amount = getFastCheckoutAmount(items, mode, paymentCurrency.id, !!trialing);
@@ -1197,7 +1184,7 @@ export async function ensureReStakeContext(subscriptionId: string) {
1197
1184
  };
1198
1185
  }
1199
1186
  export async function ensureSubscriptionForCollectBatch(
1200
- subscriptionId?: string,
1187
+ subscriptionId?: string,
1201
1188
  currencyId?: string,
1202
1189
  customerId?: string
1203
1190
  ) {
@@ -1258,13 +1245,13 @@ export async function ensureSubscriptionForOverdraftProtection(subscriptionId: s
1258
1245
  if (!subscription) {
1259
1246
  throw new Error(`Subscription ${subscriptionId} not found when prepare SubGuard`);
1260
1247
  }
1261
- // @ts-ignore
1248
+ // @ts-ignore
1262
1249
  subscription.items = await expandSubscriptionItems(subscription.id);
1263
1250
  const paymentCurrency = await PaymentCurrency.findByPk(subscription.currency_id);
1264
1251
  if (!paymentCurrency) {
1265
1252
  throw new Error(`PaymentCurrency ${subscription.currency_id} not found when prepare SubGuard`);
1266
1253
  }
1267
-
1254
+
1268
1255
  const paymentMethod = await PaymentMethod.findByPk(paymentCurrency.payment_method_id);
1269
1256
 
1270
1257
  if (!paymentMethod) {
@@ -1288,7 +1275,6 @@ export async function ensureSubscriptionForOverdraftProtection(subscriptionId: s
1288
1275
  };
1289
1276
  }
1290
1277
 
1291
-
1292
1278
  async function executeSingleTransaction(
1293
1279
  client: any,
1294
1280
  claim: any,
@@ -1323,13 +1309,12 @@ export async function executeOcapTransactions(
1323
1309
  ) {
1324
1310
  const client = paymentMethod.getOcapClient();
1325
1311
  logger.info('start executeOcapTransactions', { userDid, claims });
1326
-
1327
- const delegation = claims.find(x => x.type === 'signature' && x.meta?.purpose === 'delegation');
1328
- const staking = claims.find(x => x.type === 'prepareTx' && x.meta?.purpose === 'staking');
1329
-
1330
- const stakingAmount = staking?.requirement?.tokens?.find(
1331
- (x: any) => x?.address === paymentCurrencyContract
1332
- )?.value || '0';
1312
+
1313
+ const delegation = claims.find((x) => x.type === 'signature' && x.meta?.purpose === 'delegation');
1314
+ const staking = claims.find((x) => x.type === 'prepareTx' && x.meta?.purpose === 'staking');
1315
+
1316
+ const stakingAmount =
1317
+ staking?.requirement?.tokens?.find((x: any) => x?.address === paymentCurrencyContract)?.value || '0';
1333
1318
 
1334
1319
  try {
1335
1320
  const getHeaders = (index: number): Record<string, string> => {
@@ -1341,17 +1326,17 @@ export async function executeOcapTransactions(
1341
1326
  const req = requestSource[headerIndex];
1342
1327
  return req ? client.pickGasPayerHeaders(req) : {};
1343
1328
  }
1344
-
1329
+
1345
1330
  if (requestSource && typeof requestSource === 'object') {
1346
1331
  return client.pickGasPayerHeaders(requestSource);
1347
1332
  }
1348
-
1333
+
1349
1334
  return {};
1350
1335
  };
1351
1336
 
1352
1337
  const transactions = [
1353
1338
  { claim: delegation, type: 'Delegate' },
1354
- { claim: staking, type: 'Stake' }
1339
+ { claim: staking, type: 'Stake' },
1355
1340
  ];
1356
1341
 
1357
1342
  const txHashes = [];
@@ -1377,7 +1362,7 @@ export async function executeOcapTransactions(
1377
1362
  type: 'delegate',
1378
1363
  staking: {
1379
1364
  tx_hash: stakingTxHash,
1380
- address: await getCustomerStakeAddress(userDid, nonce || subscriptionId || ''),
1365
+ address: await getCustomerStakeAddress(userDid, nonce || subscriptionId || ''),
1381
1366
  },
1382
1367
  stakingAmount,
1383
1368
  };
@@ -1413,7 +1398,7 @@ export async function updateStripeSubscriptionAfterChangePayment(setupIntent: Se
1413
1398
  const toMethod = await PaymentMethod.findByPk(toMethodId);
1414
1399
  if (toMethod?.type === 'stripe') {
1415
1400
  // resume stripe
1416
- const client = toMethod?.getStripeClient();
1401
+ const client = toMethod?.getStripeClient();
1417
1402
  const stripeSubscriptionId = subscription.payment_details?.stripe?.subscription_id as string;
1418
1403
  if (client && stripeSubscriptionId) {
1419
1404
  const stripeSubscription = await client.subscriptions.retrieve(stripeSubscriptionId);
@@ -1443,7 +1428,7 @@ export async function returnStakeForCanceledSubscription(subscriptionId: string)
1443
1428
  if (subscription.status !== 'canceled') {
1444
1429
  throw new Error(`Subscription ${subscriptionId} is not canceled`);
1445
1430
  }
1446
-
1431
+
1447
1432
  if (!subscription.payment_details?.arcblock?.staking?.tx_hash) {
1448
1433
  throw new Error(`No staking transaction found in subscription ${subscriptionId}`);
1449
1434
  }
@@ -1455,4 +1440,4 @@ export async function returnStakeForCanceledSubscription(subscriptionId: string)
1455
1440
  } catch (err) {
1456
1441
  logger.error('returnStakeForCanceledSubscription failed', { error: err, subscriptionId });
1457
1442
  }
1458
- }
1443
+ }
@@ -233,6 +233,7 @@ export default {
233
233
  paymentCurrency,
234
234
  userDid,
235
235
  amount: fastCheckoutAmount,
236
+ skipUserCheck: true,
236
237
  });
237
238
 
238
239
  if (tokenBalanceCheck.sufficient === false) {
@@ -151,6 +151,7 @@ router.put('/:id', auth, async (req, res) => {
151
151
 
152
152
  const updateData: any = {
153
153
  ...pick(req.body, ['name', 'description', 'status']),
154
+ unit: req.body.unit || meter.unit,
154
155
  updated_by: req.user?.did,
155
156
  };
156
157
 
@@ -306,6 +306,8 @@ const updateCurrencySchema = Joi.object({
306
306
  name: Joi.string().empty('').max(32).optional(),
307
307
  description: Joi.string().empty('').max(255).optional(),
308
308
  logo: Joi.string().empty('').optional(),
309
+ metadata: Joi.object().optional(),
310
+ symbol: Joi.string().empty('').optional(),
309
311
  }).unknown(true);
310
312
  router.put('/:id', auth, async (req, res) => {
311
313
  const { id } = req.params;
@@ -329,11 +331,18 @@ router.put('/:id', auth, async (req, res) => {
329
331
  return res.status(400).json({ error: 'Payment method not found' });
330
332
  }
331
333
 
332
- const updatedCurrency = await currency.update({
334
+ const updates: Partial<TPaymentCurrency> = {
333
335
  name: raw.name || currency.name,
334
336
  description: raw.description || currency.description,
335
337
  logo: raw.logo || method.logo,
336
- });
338
+ metadata: raw.metadata || currency.metadata,
339
+ };
340
+
341
+ if (currency.type === 'credit') {
342
+ updates.symbol = raw.symbol || currency.symbol;
343
+ }
344
+
345
+ const updatedCurrency = await currency.update(updates);
337
346
  return res.json(updatedCurrency);
338
347
  });
339
348
 
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.19.7
17
+ version: 1.19.9
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.19.7",
3
+ "version": "1.19.9",
4
4
  "scripts": {
5
5
  "dev": "blocklet dev --open",
6
6
  "lint": "tsc --noEmit && eslint src api/src --ext .mjs,.js,.jsx,.ts,.tsx",
@@ -46,17 +46,18 @@
46
46
  "@abtnode/cron": "^1.16.46",
47
47
  "@arcblock/did": "^1.21.0",
48
48
  "@arcblock/did-auth-storage-nedb": "^1.7.1",
49
- "@arcblock/did-connect": "^3.0.32",
49
+ "@arcblock/did-connect": "^3.0.36",
50
50
  "@arcblock/did-util": "^1.21.0",
51
51
  "@arcblock/jwt": "^1.21.0",
52
- "@arcblock/ux": "^3.0.32",
52
+ "@arcblock/ux": "^3.0.36",
53
53
  "@arcblock/validator": "^1.21.0",
54
- "@blocklet/did-space-js": "^1.1.8",
54
+ "@blocklet/did-space-js": "^1.1.10",
55
+ "@blocklet/error": "^0.2.5",
55
56
  "@blocklet/js-sdk": "^1.16.46",
56
57
  "@blocklet/logger": "^1.16.46",
57
- "@blocklet/payment-react": "1.19.7",
58
+ "@blocklet/payment-react": "1.19.9",
58
59
  "@blocklet/sdk": "^1.16.46",
59
- "@blocklet/ui-react": "^3.0.32",
60
+ "@blocklet/ui-react": "^3.0.36",
60
61
  "@blocklet/uploader": "^0.2.4",
61
62
  "@blocklet/xss": "^0.2.2",
62
63
  "@mui/icons-material": "^7.1.2",
@@ -123,7 +124,7 @@
123
124
  "devDependencies": {
124
125
  "@abtnode/types": "^1.16.46",
125
126
  "@arcblock/eslint-config-ts": "^0.3.3",
126
- "@blocklet/payment-types": "1.19.7",
127
+ "@blocklet/payment-types": "1.19.9",
127
128
  "@types/cookie-parser": "^1.4.9",
128
129
  "@types/cors": "^2.8.19",
129
130
  "@types/debug": "^4.1.12",
@@ -169,5 +170,5 @@
169
170
  "parser": "typescript"
170
171
  }
171
172
  },
172
- "gitHead": "83b0e5ac23d05e12d07929789f06f85b30929169"
173
+ "gitHead": "a65b166cee114b432be7c1e95b8ef7bc88b30fbd"
173
174
  }
@@ -1,6 +1,6 @@
1
1
  import { formatBNStr, CreditGrantsList, CreditTransactionsList, api } from '@blocklet/payment-react';
2
2
  import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
3
- import { Avatar, Box, Card, CardContent, Stack, Typography, Tabs, Tab } from '@mui/material';
3
+ import { Box, Card, CardContent, Stack, Typography, Tabs, Tab } from '@mui/material';
4
4
  import { useMemo, useState } from 'react';
5
5
  import type { TPaymentCurrency } from '@blocklet/payment-types';
6
6
  import { useRequest } from 'ahooks';
@@ -103,13 +103,16 @@ export default function CreditOverview({ customerId, settings, mode = 'portal' }
103
103
  <CardContent sx={{ flexGrow: 1 }}>
104
104
  <Stack spacing={2}>
105
105
  {/* 货币信息 */}
106
+
106
107
  <Stack
107
- direction="row"
108
- spacing={1}
108
+ direction="column"
109
+ spacing={0.5}
109
110
  sx={{
110
- alignItems: 'center',
111
+ alignItems: 'flex-start',
112
+ borderBottom: '1px solid',
113
+ borderColor: 'divider',
114
+ pb: 2,
111
115
  }}>
112
- <Avatar src={currency.logo} alt={currency.symbol} sx={{ width: 24, height: 24 }} />
113
116
  <Typography variant="h6" component="div">
114
117
  {currency.name}
115
118
  </Typography>
@@ -127,11 +130,11 @@ export default function CreditOverview({ customerId, settings, mode = 'portal' }
127
130
  </Typography>
128
131
  <Typography variant="h5" component="div" sx={{ fontWeight: 'normal' }}>
129
132
  {totalAmount === '0' && remainingAmount === '0' ? (
130
- <>0 {currency.symbol}</>
133
+ <>0 </>
131
134
  ) : (
132
135
  <>
133
136
  {formatBNStr(remainingAmount, currency.decimal, 6, true)} /{' '}
134
- {formatBNStr(totalAmount, currency.decimal, 6, true)} {currency.symbol}
137
+ {formatBNStr(totalAmount, currency.decimal, 6, true)}
135
138
  </>
136
139
  )}
137
140
  </Typography>
@@ -153,7 +156,7 @@ export default function CreditOverview({ customerId, settings, mode = 'portal' }
153
156
  sx={{
154
157
  color: 'error.main',
155
158
  }}>
156
- {formatBNStr(pendingAmount, currency.decimal, 6, true)} {currency.symbol}
159
+ {formatBNStr(pendingAmount, currency.decimal, 6, true)}
157
160
  </Typography>
158
161
  </Box>
159
162
  )}