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.
- package/api/src/integrations/blockchain/stake.ts +28 -2
- package/api/src/routes/customers.ts +13 -5
- package/api/src/routes/subscriptions.ts +19 -0
- package/api/src/store/migrations/20240328-lock.ts +10 -0
- package/api/src/store/models/index.ts +3 -0
- package/api/src/store/models/lock.ts +82 -0
- package/blocklet.yml +1 -1
- package/package.json +4 -4
- package/src/components/balance-list.tsx +5 -5
- package/src/locales/en.tsx +1 -0
- package/src/locales/zh.tsx +1 -0
- package/src/pages/admin/customers/customers/detail.tsx +1 -0
- package/src/pages/customer/index.tsx +1 -0
|
@@ -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([
|
|
115
|
-
|
|
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([
|
|
146
|
-
|
|
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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "payment-kit",
|
|
3
|
-
"version": "1.13.
|
|
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.
|
|
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.
|
|
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": "
|
|
152
|
+
"gitHead": "7b4918ac7bb09317df0b2a78e337312cc4b50b67"
|
|
153
153
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
2
|
-
import {
|
|
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
|
})}
|
package/src/locales/en.tsx
CHANGED
package/src/locales/zh.tsx
CHANGED
|
@@ -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>
|