payment-kit 1.13.205 → 1.13.207

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.
@@ -173,9 +173,9 @@ export async function checkStakeRevokeTx() {
173
173
  }
174
174
  }
175
175
 
176
- export async function getStakeSummaryByDid(did: string): Promise<GroupedBN> {
176
+ export async function getStakeSummaryByDid(did: string, livemode: boolean): Promise<GroupedBN> {
177
177
  const methods = await PaymentMethod.findAll({
178
- where: { type: 'arcblock' },
178
+ where: { type: 'arcblock', livemode },
179
179
  include: [{ model: PaymentCurrency, as: 'payment_currencies' }],
180
180
  });
181
181
  if (methods.length === 0) {
@@ -199,3 +199,29 @@ export async function getStakeSummaryByDid(did: string): Promise<GroupedBN> {
199
199
 
200
200
  return results;
201
201
  }
202
+
203
+ export async function getTokenSummaryByDid(did: string, livemode: boolean): Promise<GroupedBN> {
204
+ const methods = await PaymentMethod.findAll({
205
+ where: { type: 'arcblock', livemode },
206
+ include: [{ model: PaymentCurrency, as: 'payment_currencies' }],
207
+ });
208
+ if (methods.length === 0) {
209
+ return {};
210
+ }
211
+
212
+ const results: GroupedBN = {};
213
+ await Promise.all(
214
+ methods.map(async (method: any) => {
215
+ const client = method.getOcapClient();
216
+ const { tokens } = await client.getAccountTokens({ address: did });
217
+ (tokens || []).forEach((t: any) => {
218
+ const currency = method.payment_currencies.find((c: any) => t.address === c.contract);
219
+ if (currency) {
220
+ results[currency.id] = t.balance;
221
+ }
222
+ });
223
+ })
224
+ );
225
+
226
+ return results;
227
+ }
@@ -3,7 +3,7 @@ import { Router } from 'express';
3
3
  import Joi from 'joi';
4
4
  import pick from 'lodash/pick';
5
5
 
6
- import { getStakeSummaryByDid } from '../integrations/blockchain/stake';
6
+ import { getStakeSummaryByDid, getTokenSummaryByDid } from '../integrations/blockchain/stake';
7
7
  import { getWhereFromKvQuery, getWhereFromQuery } from '../libs/api';
8
8
  import { authenticate } from '../libs/security';
9
9
  import { formatMetadata } from '../libs/util';
@@ -111,8 +111,12 @@ router.get('/me', user(), async (req, res) => {
111
111
  if (!doc) {
112
112
  res.json(null);
113
113
  } else {
114
- const [summary, stake] = await Promise.all([doc.getSummary(), getStakeSummaryByDid(doc.did)]);
115
- res.json({ ...doc.toJSON(), summary: { ...summary, stake } });
114
+ const [summary, stake, token] = await Promise.all([
115
+ doc.getSummary(),
116
+ getStakeSummaryByDid(doc.did, doc.livemode),
117
+ getTokenSummaryByDid(doc.did, doc.livemode),
118
+ ]);
119
+ res.json({ ...doc.toJSON(), summary: { ...summary, stake, token } });
116
120
  }
117
121
  } catch (err) {
118
122
  console.error(err);
@@ -142,8 +146,12 @@ router.get('/:id/summary', auth, async (req, res) => {
142
146
  return;
143
147
  }
144
148
 
145
- const [summary, stake] = await Promise.all([doc.getSummary(), getStakeSummaryByDid(doc.did)]);
146
- res.json({ ...summary, stake });
149
+ const [summary, stake, token] = await Promise.all([
150
+ doc.getSummary(),
151
+ getStakeSummaryByDid(doc.did, doc.livemode),
152
+ getTokenSummaryByDid(doc.did, doc.livemode),
153
+ ]);
154
+ res.json({ ...summary, stake, token });
147
155
  } catch (err) {
148
156
  console.error(err);
149
157
  res.json(null);
@@ -20,6 +20,7 @@ import type { TLineItemExpanded } from '../store/models';
20
20
  import { Customer } from '../store/models/customer';
21
21
  import { Invoice } from '../store/models/invoice';
22
22
  import { InvoiceItem } from '../store/models/invoice-item';
23
+ import { Lock } from '../store/models/lock';
23
24
  import { PaymentCurrency } from '../store/models/payment-currency';
24
25
  import { PaymentIntent } from '../store/models/payment-intent';
25
26
  import { PaymentMethod } from '../store/models/payment-method';
@@ -694,6 +695,11 @@ router.put('/:id', authPortal, async (req, res) => {
694
695
  throw new Error('Updating subscription item not allowed for subscriptions paid with stripe');
695
696
  }
696
697
 
698
+ const locked = await Lock.isLocked(`${subscription.id}-change-plan`);
699
+ if (locked) {
700
+ throw new Error('Updating subscription item not allowed now until next billing cycle');
701
+ }
702
+
697
703
  // validate the request
698
704
  const { existingItems, addedItems, updatedItems, deletedItems, newItems } =
699
705
  await validateSubscriptionUpdateRequest(subscription, value.items);
@@ -867,6 +873,11 @@ router.put('/:id', authPortal, async (req, res) => {
867
873
  }
868
874
  }
869
875
  }
876
+
877
+ // rate limit for plan change
878
+ const releaseAt = updates.current_period_end || subscription.current_period_end;
879
+ await Lock.acquire(`${subscription.id}-change-plan`, releaseAt);
880
+ logger.info('subscription plan change lock acquired', { subscription: req.params.id, releaseAt });
870
881
  } else if (req.body.billing_cycle_anchor === 'now') {
871
882
  if (subscription.isActive() === false) {
872
883
  throw new Error('Updating billing_cycle_anchor not allowed for inactive subscriptions');
@@ -1010,6 +1021,10 @@ router.get('/:id/change-plan', authPortal, async (req, res) => {
1010
1021
  if (subscription.isScheduledToCancel()) {
1011
1022
  return res.status(400).json({ error: 'Subscription is scheduled to cancel' });
1012
1023
  }
1024
+ const locked = await Lock.isLocked(`${subscription.id}-change-plan`);
1025
+ if (locked) {
1026
+ return res.status(400).json({ error: 'Subscription plan change is not allowed until next billing cycle' });
1027
+ }
1013
1028
 
1014
1029
  const paymentMethod = await PaymentMethod.findByPk(subscription.default_payment_method_id);
1015
1030
  if (paymentMethod?.type === 'stripe') {
@@ -1037,6 +1052,10 @@ router.post('/:id/change-plan', authPortal, async (req, res) => {
1037
1052
  if (subscription.isScheduledToCancel()) {
1038
1053
  return res.status(400).json({ error: 'Subscription is scheduled to cancel' });
1039
1054
  }
1055
+ const locked = await Lock.isLocked(`${subscription.id}-change-plan`);
1056
+ if (locked) {
1057
+ return res.status(400).json({ error: 'Subscription plan change is not allowed until next billing cycle' });
1058
+ }
1040
1059
 
1041
1060
  const paymentMethod = await PaymentMethod.findByPk(subscription.default_payment_method_id);
1042
1061
  if (paymentMethod?.type === 'stripe') {
@@ -0,0 +1,10 @@
1
+ import type { Migration } from '../migrate';
2
+ import models from '../models';
3
+
4
+ export const up: Migration = async ({ context }) => {
5
+ await context.createTable('locks', models.Lock.GENESIS_ATTRIBUTES);
6
+ };
7
+
8
+ export const down: Migration = async ({ context }) => {
9
+ await context.dropTable('locks');
10
+ };
@@ -6,6 +6,7 @@ import { Event, TEvent } from './event';
6
6
  import { Invoice, TInvoice } from './invoice';
7
7
  import { InvoiceItem, TInvoiceItem } from './invoice-item';
8
8
  import { Job } from './job';
9
+ import { Lock } from './lock';
9
10
  import { PaymentCurrency, TPaymentCurrency } from './payment-currency';
10
11
  import { PaymentIntent, TPaymentIntent } from './payment-intent';
11
12
  import { PaymentLink, TPaymentLink } from './payment-link';
@@ -49,6 +50,7 @@ const models = {
49
50
  WebhookEndpoint,
50
51
  WebhookAttempt,
51
52
  Job,
53
+ Lock,
52
54
  };
53
55
 
54
56
  export function initialize(sequelize: any) {
@@ -70,6 +72,7 @@ export * from './event';
70
72
  export * from './invoice';
71
73
  export * from './invoice-item';
72
74
  export * from './job';
75
+ export * from './lock';
73
76
  export * from './payment-currency';
74
77
  export * from './payment-intent';
75
78
  export * from './payment-link';
@@ -0,0 +1,82 @@
1
+ /* eslint-disable @typescript-eslint/lines-between-class-members */
2
+ import { CreationOptional, DataTypes, InferAttributes, InferCreationAttributes, Model } from 'sequelize';
3
+
4
+ import dayjs from '../../libs/dayjs';
5
+
6
+ // eslint-disable-next-line prettier/prettier
7
+ export class Lock extends Model<InferAttributes<Lock>, InferCreationAttributes<Lock>> {
8
+ declare id: CreationOptional<string>;
9
+ declare lock_at: number;
10
+ declare release_at: number;
11
+
12
+ declare reason?: string;
13
+
14
+ declare created_at: CreationOptional<Date>;
15
+ declare updated_at: CreationOptional<Date>;
16
+
17
+ public static readonly GENESIS_ATTRIBUTES = {
18
+ id: {
19
+ type: DataTypes.STRING(30),
20
+ primaryKey: true,
21
+ allowNull: false,
22
+ },
23
+ lock_at: {
24
+ type: DataTypes.INTEGER,
25
+ defaultValue: 0,
26
+ },
27
+ release_at: {
28
+ type: DataTypes.INTEGER,
29
+ defaultValue: -1,
30
+ },
31
+ reason: {
32
+ type: DataTypes.STRING(255),
33
+ allowNull: true,
34
+ },
35
+ created_at: {
36
+ type: DataTypes.DATE,
37
+ defaultValue: DataTypes.NOW,
38
+ allowNull: false,
39
+ },
40
+ updated_at: {
41
+ type: DataTypes.DATE,
42
+ defaultValue: DataTypes.NOW,
43
+ allowNull: false,
44
+ },
45
+ };
46
+
47
+ public static initialize(sequelize: any) {
48
+ this.init(Lock.GENESIS_ATTRIBUTES, {
49
+ sequelize,
50
+ modelName: 'Lock',
51
+ tableName: 'locks',
52
+ createdAt: 'created_at',
53
+ updatedAt: 'updated_at',
54
+ });
55
+ }
56
+
57
+ public static async acquire(id: string, releaseAt: number, reason?: string): Promise<boolean> {
58
+ const now = dayjs().unix();
59
+ const exist = await this.findByPk(id);
60
+ if (exist) {
61
+ if (exist.release_at <= now) {
62
+ await exist.update({ lock_at: now, release_at: releaseAt, reason });
63
+ return true;
64
+ }
65
+
66
+ return false;
67
+ }
68
+
69
+ await this.create({ id, lock_at: now, release_at: releaseAt, reason });
70
+ return true;
71
+ }
72
+
73
+ public static async isLocked(id: string): Promise<boolean> {
74
+ const now = dayjs().unix();
75
+ const exist = await this.findByPk(id);
76
+ return !!exist && exist.release_at > now;
77
+ }
78
+
79
+ public static associate() {}
80
+ }
81
+
82
+ export type TLock = InferAttributes<Lock>;
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.13.205
17
+ version: 1.13.207
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.13.205",
3
+ "version": "1.13.207",
4
4
  "scripts": {
5
5
  "dev": "cross-env COMPONENT_STORE_URL=https://test.store.blocklet.dev blocklet dev --open",
6
6
  "eject": "vite eject",
@@ -50,7 +50,7 @@
50
50
  "@arcblock/jwt": "^1.18.113",
51
51
  "@arcblock/ux": "^2.9.63",
52
52
  "@blocklet/logger": "1.16.24",
53
- "@blocklet/payment-react": "1.13.205",
53
+ "@blocklet/payment-react": "1.13.207",
54
54
  "@blocklet/sdk": "1.16.24",
55
55
  "@blocklet/ui-react": "^2.9.63",
56
56
  "@blocklet/uploader": "^0.0.75",
@@ -110,7 +110,7 @@
110
110
  "devDependencies": {
111
111
  "@abtnode/types": "1.16.24",
112
112
  "@arcblock/eslint-config-ts": "^0.3.0",
113
- "@blocklet/payment-types": "1.13.205",
113
+ "@blocklet/payment-types": "1.13.207",
114
114
  "@types/cookie-parser": "^1.4.6",
115
115
  "@types/cors": "^2.8.17",
116
116
  "@types/dotenv-flow": "^3.3.3",
@@ -149,5 +149,5 @@
149
149
  "parser": "typescript"
150
150
  }
151
151
  },
152
- "gitHead": "8c16c5d362df5c497c7a95a0ae20e2ef61f747b3"
152
+ "gitHead": "7b4918ac7bb09317df0b2a78e337312cc4b50b67"
153
153
  }
@@ -1,5 +1,5 @@
1
1
  import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
2
- import { formatAmount, usePaymentContext } from '@blocklet/payment-react';
2
+ import { formatBNStr, usePaymentContext } from '@blocklet/payment-react';
3
3
  import type { GroupedBN } from '@blocklet/payment-types';
4
4
  import { Stack, Typography } from '@mui/material';
5
5
  import flatten from 'lodash/flatten';
@@ -26,13 +26,13 @@ export default function BalanceList(props: Props) {
26
26
  return null;
27
27
  }
28
28
  return (
29
- <Stack key={currencyId} sx={{ width: '100%' }} direction="row" spacing={1}>
30
- <Typography sx={{ flex: 1 }} color="text.primary">
31
- {formatAmount(amount, currency.decimal)}
32
- </Typography>
29
+ <Stack key={currencyId} sx={{ width: '100%', maxWidth: '200px' }} direction="row" spacing={1}>
33
30
  <Typography sx={{ width: '32px' }} color="text.secondary">
34
31
  {currency.symbol}
35
32
  </Typography>
33
+ <Typography sx={{ flex: 1, textAlign: 'right' }} color="text.primary">
34
+ {formatBNStr(amount, currency.decimal, 6, false)}
35
+ </Typography>
36
36
  </Stack>
37
37
  );
38
38
  })}
@@ -399,6 +399,7 @@ export default flat({
399
399
  dispute: 'Dispute Amount',
400
400
  due: 'Due Amount',
401
401
  stake: 'Stake Amount',
402
+ token: 'Token Balance',
402
403
  name: 'Name',
403
404
  email: 'Email',
404
405
  phone: 'Phone',
@@ -389,6 +389,7 @@ export default flat({
389
389
  refund: '退款金额',
390
390
  stake: '质押金额',
391
391
  dispute: '争议金额',
392
+ token: '链上余额',
392
393
  due: '欠款金额',
393
394
  name: '名称',
394
395
  email: '电子邮件',
@@ -167,6 +167,7 @@ export default function CustomerDetail(props: { id: string }) {
167
167
  <InfoMetric label={t('common.updatedAt')} value={formatTime(data.customer.updated_at)} divider />
168
168
  <InfoMetric label={t('admin.customer.spent')} value={<BalanceList data={data.summary.paid} />} divider />
169
169
  <InfoMetric label={t('admin.customer.stake')} value={<BalanceList data={data.summary.stake} />} divider />
170
+ <InfoMetric label={t('admin.customer.token')} value={<BalanceList data={data.summary.token} />} divider />
170
171
  <InfoMetric label={t('admin.customer.due')} value={<BalanceList data={data.summary.due} />} divider />
171
172
  <InfoMetric
172
173
  label={t('admin.customer.refund')}
@@ -179,6 +179,7 @@ export default function CustomerHome() {
179
179
  sx={{ width: '100%' }}>
180
180
  <InfoMetric label={t('admin.customer.spent')} value={<BalanceList data={data.summary.paid} />} />
181
181
  <InfoMetric label={t('admin.customer.stake')} value={<BalanceList data={data.summary.stake} />} />
182
+ <InfoMetric label={t('admin.customer.token')} value={<BalanceList data={data.summary.token} />} />
182
183
  <InfoMetric label={t('admin.customer.due')} value={<BalanceList data={data.summary.due} />} />
183
184
  <InfoMetric label={t('admin.customer.refund')} value={<BalanceList data={data.summary.refunded} />} />
184
185
  </Stack>