payment-kit 1.22.29 → 1.22.31
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/libs/util.ts +3 -3
- package/api/src/queues/event.ts +3 -12
- package/api/src/queues/webhook.ts +60 -12
- package/api/src/routes/events.ts +196 -0
- package/api/src/routes/meter-events.ts +6 -3
- package/api/src/store/migrations/20251208-database-performance-indexes.ts +129 -0
- package/api/src/store/models/meter-event.ts +10 -2
- package/blocklet.yml +1 -1
- package/package.json +23 -23
- package/src/components/section/header.tsx +3 -2
- package/src/components/webhook/attempts.tsx +66 -11
- package/src/locales/en.tsx +15 -0
- package/src/locales/zh.tsx +14 -0
package/api/src/libs/util.ts
CHANGED
|
@@ -163,10 +163,10 @@ export function tryWithTimeout(asyncFn: Function, timeout = 5000) {
|
|
|
163
163
|
}
|
|
164
164
|
|
|
165
165
|
// simple exponential delay: 2^retryCount
|
|
166
|
-
export const getNextRetry = (retryCount: number) => {
|
|
166
|
+
export const getNextRetry = (retryCount: number, eventCreatedAt?: Date) => {
|
|
167
167
|
const delay = 2 ** retryCount;
|
|
168
|
-
const
|
|
169
|
-
return
|
|
168
|
+
const baseTime = eventCreatedAt ? dayjs(eventCreatedAt).unix() : dayjs().unix();
|
|
169
|
+
return baseTime + delay;
|
|
170
170
|
};
|
|
171
171
|
|
|
172
172
|
export const getWebhookJobId = (eventId: string, webhookId: string) => {
|
package/api/src/queues/event.ts
CHANGED
|
@@ -3,11 +3,10 @@ import { Op } from 'sequelize';
|
|
|
3
3
|
import { events } from '../libs/event';
|
|
4
4
|
import logger from '../libs/logger';
|
|
5
5
|
import createQueue from '../libs/queue';
|
|
6
|
-
import { getWebhookJobId } from '../libs/util';
|
|
7
6
|
import { Event } from '../store/models/event';
|
|
8
7
|
import { WebhookAttempt } from '../store/models/webhook-attempt';
|
|
9
8
|
import { WebhookEndpoint } from '../store/models/webhook-endpoint';
|
|
10
|
-
import {
|
|
9
|
+
import { addWebhookJob } from './webhook';
|
|
11
10
|
|
|
12
11
|
type EventJob = {
|
|
13
12
|
eventId: string;
|
|
@@ -52,16 +51,8 @@ export const handleEvent = async (job: EventJob) => {
|
|
|
52
51
|
|
|
53
52
|
// we should only push webhook if it's not successfully attempted before
|
|
54
53
|
if (attemptCount === 0) {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
if (!exist) {
|
|
58
|
-
logger.info(`Scheduling attempt for event ${event.id} and webhook ${webhook.id}`, job);
|
|
59
|
-
webhookQueue.push({
|
|
60
|
-
id: jobId,
|
|
61
|
-
job: { eventId: event.id, webhookId: webhook.id },
|
|
62
|
-
persist: false,
|
|
63
|
-
});
|
|
64
|
-
}
|
|
54
|
+
logger.info(`Scheduling attempt for event ${event.id} and webhook ${webhook.id}`, job);
|
|
55
|
+
await addWebhookJob(event.id, webhook.id, { persist: false });
|
|
65
56
|
}
|
|
66
57
|
});
|
|
67
58
|
|
|
@@ -14,6 +14,29 @@ import { WebhookAttempt } from '../store/models/webhook-attempt';
|
|
|
14
14
|
import { WebhookEndpoint } from '../store/models/webhook-endpoint';
|
|
15
15
|
import { addNotificationJob } from './notification';
|
|
16
16
|
|
|
17
|
+
async function decrementPendingWebhooks(event: Event, webhookId: string) {
|
|
18
|
+
const existingSuccessCount = await WebhookAttempt.count({
|
|
19
|
+
where: {
|
|
20
|
+
event_id: event.id,
|
|
21
|
+
webhook_endpoint_id: webhookId,
|
|
22
|
+
status: 'succeeded',
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
if (existingSuccessCount === 0) {
|
|
27
|
+
if (event.pending_webhooks > 0) {
|
|
28
|
+
await event.decrement('pending_webhooks');
|
|
29
|
+
logger.info('pending_webhooks decremented', { eventId: event.id, newCount: event.pending_webhooks, webhookId });
|
|
30
|
+
} else {
|
|
31
|
+
logger.warn('Attempted to decrement pending_webhooks below 0', {
|
|
32
|
+
eventId: event.id,
|
|
33
|
+
currentCount: event.pending_webhooks,
|
|
34
|
+
webhookId,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
17
40
|
type WebhookJob = {
|
|
18
41
|
eventId: string;
|
|
19
42
|
webhookId: string;
|
|
@@ -80,8 +103,7 @@ export const handleWebhook = async (job: WebhookJob) => {
|
|
|
80
103
|
});
|
|
81
104
|
logger.info('WebhookAttempt created successfully', { eventId: event.id, webhookId: webhook.id });
|
|
82
105
|
|
|
83
|
-
await event.
|
|
84
|
-
logger.info('pending_webhooks decremented', { eventId: event.id, newCount: event.pending_webhooks });
|
|
106
|
+
await decrementPendingWebhooks(event, webhook.id);
|
|
85
107
|
|
|
86
108
|
logger.info('webhook attempt success', { ...job, retryCount });
|
|
87
109
|
} catch (err: any) {
|
|
@@ -110,20 +132,13 @@ export const handleWebhook = async (job: WebhookJob) => {
|
|
|
110
132
|
|
|
111
133
|
if (retryCount < MAX_RETRY_COUNT) {
|
|
112
134
|
process.nextTick(() => {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
job: { eventId: event.id, webhookId: webhook.id },
|
|
116
|
-
runAt: getNextRetry(retryCount),
|
|
135
|
+
addWebhookJob(event.id, webhook.id, {
|
|
136
|
+
runAt: getNextRetry(retryCount, event.created_at),
|
|
117
137
|
persist: false,
|
|
118
138
|
});
|
|
119
|
-
logger.info('scheduled webhook job', { ...job, retryCount });
|
|
120
139
|
});
|
|
121
140
|
} else {
|
|
122
|
-
await event.
|
|
123
|
-
logger.info('Max retries reached, pending_webhooks decremented', {
|
|
124
|
-
eventId: event.id,
|
|
125
|
-
newCount: event.pending_webhooks,
|
|
126
|
-
});
|
|
141
|
+
await decrementPendingWebhooks(event, webhook.id);
|
|
127
142
|
}
|
|
128
143
|
}
|
|
129
144
|
};
|
|
@@ -183,3 +198,36 @@ export const webhookQueue = createQueue<WebhookJob>({
|
|
|
183
198
|
webhookQueue.on('failed', ({ id, job, error }) => {
|
|
184
199
|
logger.error('webhook job failed', { id, job, error });
|
|
185
200
|
});
|
|
201
|
+
|
|
202
|
+
export async function addWebhookJob(
|
|
203
|
+
eventId: string,
|
|
204
|
+
webhookId: string,
|
|
205
|
+
options: {
|
|
206
|
+
runAt?: number;
|
|
207
|
+
persist?: boolean;
|
|
208
|
+
replace?: boolean;
|
|
209
|
+
} = {}
|
|
210
|
+
) {
|
|
211
|
+
const { runAt, persist = false, replace = true } = options;
|
|
212
|
+
const jobId = getWebhookJobId(eventId, webhookId);
|
|
213
|
+
const exist = await webhookQueue.get(jobId);
|
|
214
|
+
|
|
215
|
+
if (exist && !replace) {
|
|
216
|
+
logger.info('Webhook job already exists, skipping', { eventId, webhookId, jobId });
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (exist) {
|
|
221
|
+
await webhookQueue.delete(jobId);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
webhookQueue.push({
|
|
225
|
+
id: jobId,
|
|
226
|
+
job: { eventId, webhookId },
|
|
227
|
+
runAt,
|
|
228
|
+
persist,
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
logger.info('Webhook job added to queue', { eventId, webhookId, jobId, runAt, persist });
|
|
232
|
+
return true;
|
|
233
|
+
}
|
package/api/src/routes/events.ts
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import { Router } from 'express';
|
|
2
2
|
import Joi from 'joi';
|
|
3
3
|
import type { WhereOptions } from 'sequelize';
|
|
4
|
+
import { Op } from 'sequelize';
|
|
4
5
|
|
|
5
6
|
import { createListParamSchema, getOrder } from '../libs/api';
|
|
6
7
|
import { authenticate } from '../libs/security';
|
|
7
8
|
import { Event } from '../store/models/event';
|
|
8
9
|
import { blocklet } from '../libs/auth';
|
|
9
10
|
import logger from '../libs/logger';
|
|
11
|
+
import { addWebhookJob } from '../queues/webhook';
|
|
12
|
+
import { WebhookEndpoint } from '../store/models/webhook-endpoint';
|
|
13
|
+
import { Subscription } from '../store/models/subscription';
|
|
10
14
|
|
|
11
15
|
const router = Router();
|
|
12
16
|
const auth = authenticate<Event>({ component: true, roles: ['owner', 'admin'] });
|
|
@@ -55,6 +59,157 @@ router.get('/', auth, async (req, res) => {
|
|
|
55
59
|
}
|
|
56
60
|
});
|
|
57
61
|
|
|
62
|
+
const retryWebhooksSchema = createListParamSchema<{
|
|
63
|
+
eventType?: string;
|
|
64
|
+
objectType?: string;
|
|
65
|
+
objectId?: string;
|
|
66
|
+
objectIds?: string;
|
|
67
|
+
eventIds?: string;
|
|
68
|
+
subscriptionStatus?: string;
|
|
69
|
+
latestOnly?: boolean;
|
|
70
|
+
}>({
|
|
71
|
+
eventType: Joi.string().empty(''),
|
|
72
|
+
objectType: Joi.string().empty(''),
|
|
73
|
+
objectId: Joi.string().empty(''),
|
|
74
|
+
objectIds: Joi.string().empty(''),
|
|
75
|
+
eventIds: Joi.string().empty(''),
|
|
76
|
+
subscriptionStatus: Joi.string().empty(''),
|
|
77
|
+
latestOnly: Joi.boolean().optional(),
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
router.get('/retry-webhooks', auth, async (req, res) => {
|
|
81
|
+
try {
|
|
82
|
+
const { eventType, objectType, objectId, objectIds, eventIds, subscriptionStatus, latestOnly } =
|
|
83
|
+
await retryWebhooksSchema.validateAsync(req.query, {
|
|
84
|
+
stripUnknown: true,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const where: WhereOptions<Event> = { livemode: req.livemode };
|
|
88
|
+
let targetObjectIds: string[] = [];
|
|
89
|
+
|
|
90
|
+
// Handle subscription status filter
|
|
91
|
+
if (subscriptionStatus) {
|
|
92
|
+
const subscriptions = await Subscription.findAll({
|
|
93
|
+
where: {
|
|
94
|
+
status: subscriptionStatus,
|
|
95
|
+
livemode: req.livemode,
|
|
96
|
+
},
|
|
97
|
+
attributes: ['id'],
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
if (subscriptions.length === 0) {
|
|
101
|
+
return res.json({
|
|
102
|
+
message: `No subscriptions found with status: ${subscriptionStatus}`,
|
|
103
|
+
scheduled: 0,
|
|
104
|
+
eventsProcessed: 0,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
targetObjectIds = subscriptions.map((sub) => sub.id);
|
|
109
|
+
where.object_type = 'subscription';
|
|
110
|
+
where.object_id = { [Op.in]: targetObjectIds };
|
|
111
|
+
|
|
112
|
+
logger.info(`Found ${targetObjectIds.length} subscriptions with status: ${subscriptionStatus}`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Handle explicit object filters
|
|
116
|
+
if (objectType) {
|
|
117
|
+
where.object_type = objectType;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (objectId) {
|
|
121
|
+
where.object_id = objectId;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (objectIds) {
|
|
125
|
+
const ids = objectIds
|
|
126
|
+
.split(',')
|
|
127
|
+
.map((x) => x.trim())
|
|
128
|
+
.filter(Boolean);
|
|
129
|
+
where.object_id = ids.length > 1 ? { [Op.in]: ids } : ids[0];
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (eventType) {
|
|
133
|
+
where.type = eventType;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (eventIds) {
|
|
137
|
+
const ids = eventIds
|
|
138
|
+
.split(',')
|
|
139
|
+
.map((x) => x.trim())
|
|
140
|
+
.filter(Boolean);
|
|
141
|
+
where.id = ids.length > 1 ? { [Op.in]: ids } : ids[0];
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
let events = await Event.findAll({
|
|
145
|
+
where,
|
|
146
|
+
order: [['created_at', 'DESC']],
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
if (events.length === 0) {
|
|
150
|
+
return res.json({
|
|
151
|
+
message: 'No events found matching the criteria',
|
|
152
|
+
scheduled: 0,
|
|
153
|
+
eventsProcessed: 0,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// If latestOnly is true, group by object_id and keep only the latest event for each
|
|
158
|
+
if (latestOnly) {
|
|
159
|
+
const eventsByObject = new Map<string, Event>();
|
|
160
|
+
events.forEach((event) => {
|
|
161
|
+
if (!eventsByObject.has(event.object_id)) {
|
|
162
|
+
eventsByObject.set(event.object_id, event);
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
events = Array.from(eventsByObject.values());
|
|
166
|
+
logger.info(`Filtered to ${events.length} latest events (from ${eventsByObject.size} unique objects)`);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const webhooks = await WebhookEndpoint.findAll({
|
|
170
|
+
where: { status: 'enabled', livemode: req.livemode },
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
if (webhooks.length === 0) {
|
|
174
|
+
return res.json({
|
|
175
|
+
message: 'No enabled webhook endpoints found',
|
|
176
|
+
scheduled: 0,
|
|
177
|
+
eventsProcessed: events.length,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
let scheduled = 0;
|
|
182
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
183
|
+
for (const event of events) {
|
|
184
|
+
const eventWebhooks = webhooks.filter((webhook) => webhook.enabled_events.includes(event.type));
|
|
185
|
+
|
|
186
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
187
|
+
for (const webhook of eventWebhooks) {
|
|
188
|
+
// eslint-disable-next-line no-await-in-loop
|
|
189
|
+
const added = await addWebhookJob(event.id, webhook.id, { persist: false });
|
|
190
|
+
if (added) {
|
|
191
|
+
scheduled += 1;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
logger.info('Batch webhook retry completed', {
|
|
197
|
+
eventsProcessed: events.length,
|
|
198
|
+
webhooksScheduled: scheduled,
|
|
199
|
+
criteria: { eventType, objectType, objectId, subscriptionStatus, latestOnly },
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
return res.json({
|
|
203
|
+
message: `Successfully scheduled ${scheduled} webhooks for retry across ${events.length} events`,
|
|
204
|
+
scheduled,
|
|
205
|
+
eventsProcessed: events.length,
|
|
206
|
+
});
|
|
207
|
+
} catch (err: any) {
|
|
208
|
+
logger.error('Failed to batch retry webhooks', err);
|
|
209
|
+
return res.status(500).json({ error: `Failed to retry webhooks: ${err.message}` });
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
|
|
58
213
|
router.get('/:id', auth, async (req, res) => {
|
|
59
214
|
try {
|
|
60
215
|
const doc = await Event.findOne({
|
|
@@ -77,4 +232,45 @@ router.get('/:id', auth, async (req, res) => {
|
|
|
77
232
|
}
|
|
78
233
|
});
|
|
79
234
|
|
|
235
|
+
router.post('/:id/retry-webhooks', auth, async (req, res) => {
|
|
236
|
+
try {
|
|
237
|
+
const event = await Event.findOne({
|
|
238
|
+
where: { id: req.params.id },
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
if (!event) {
|
|
242
|
+
return res.status(404).json({ error: 'Event not found' });
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const webhooks = await WebhookEndpoint.findAll({
|
|
246
|
+
where: { status: 'enabled', livemode: event.livemode },
|
|
247
|
+
});
|
|
248
|
+
const eventWebhooks = webhooks.filter((webhook) => webhook.enabled_events.includes(event.type));
|
|
249
|
+
|
|
250
|
+
if (eventWebhooks.length === 0) {
|
|
251
|
+
return res.json({ message: 'No enabled webhook endpoints found for this event type', scheduled: 0 });
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
let scheduled = 0;
|
|
255
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
256
|
+
for (const webhook of eventWebhooks) {
|
|
257
|
+
// eslint-disable-next-line no-await-in-loop
|
|
258
|
+
const added = await addWebhookJob(event.id, webhook.id, { persist: false });
|
|
259
|
+
if (added) {
|
|
260
|
+
scheduled += 1;
|
|
261
|
+
logger.info('Manually scheduled webhook retry', { eventId: event.id, webhookId: webhook.id });
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return res.json({
|
|
266
|
+
message: `Successfully scheduled ${scheduled} webhooks for retry`,
|
|
267
|
+
scheduled,
|
|
268
|
+
total: eventWebhooks.length,
|
|
269
|
+
});
|
|
270
|
+
} catch (err: any) {
|
|
271
|
+
logger.error('Failed to retry webhooks for event', err);
|
|
272
|
+
return res.status(500).json({ error: `Failed to retry webhooks: ${err.message}` });
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
|
|
80
276
|
export default router;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Router } from 'express';
|
|
2
2
|
import Joi from 'joi';
|
|
3
|
-
import { Op, QueryTypes } from 'sequelize';
|
|
3
|
+
import { Op, QueryTypes, Sequelize } from 'sequelize';
|
|
4
4
|
|
|
5
5
|
import { fromTokenToUnit } from '@ocap/util';
|
|
6
6
|
import { createListParamSchema, getOrder, getWhereFromKvQuery, MetadataSchema } from '../libs/api';
|
|
@@ -95,7 +95,10 @@ router.get('/', authMine, async (req, res) => {
|
|
|
95
95
|
}
|
|
96
96
|
|
|
97
97
|
if (query.customer_id) {
|
|
98
|
-
where[
|
|
98
|
+
where[Op.and] = where[Op.and] || [];
|
|
99
|
+
where[Op.and].push(
|
|
100
|
+
Sequelize.where(Sequelize.fn('json_extract', Sequelize.col('payload'), '$.customer_id'), query.customer_id)
|
|
101
|
+
);
|
|
99
102
|
}
|
|
100
103
|
|
|
101
104
|
if (query.start || query.end) {
|
|
@@ -171,7 +174,7 @@ router.get('/stats', authMine, async (req, res) => {
|
|
|
171
174
|
};
|
|
172
175
|
|
|
173
176
|
if (customerId) {
|
|
174
|
-
whereClause += " AND payload
|
|
177
|
+
whereClause += " AND json_extract(payload, '$.customer_id') = :customerId";
|
|
175
178
|
replacements.customerId = customerId;
|
|
176
179
|
}
|
|
177
180
|
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/* eslint-disable no-console */
|
|
2
|
+
import { createIndexIfNotExists, type Migration } from '../migrate';
|
|
3
|
+
|
|
4
|
+
export const up: Migration = async ({ context }) => {
|
|
5
|
+
console.log('🚀 Starting database performance optimization...');
|
|
6
|
+
|
|
7
|
+
// meter_events
|
|
8
|
+
await createIndexIfNotExists(context, 'meter_events', ['status', 'created_at'], 'idx_meter_events_status_created');
|
|
9
|
+
await createIndexIfNotExists(
|
|
10
|
+
context,
|
|
11
|
+
'meter_events',
|
|
12
|
+
['event_name', 'livemode', 'created_at'],
|
|
13
|
+
'idx_meter_events_event_livemode_created'
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
await context.sequelize.query(
|
|
17
|
+
"CREATE INDEX IF NOT EXISTS idx_meter_events_customer_status ON meter_events(json_extract(payload, '$.customer_id'), status, livemode, created_at)"
|
|
18
|
+
);
|
|
19
|
+
await context.sequelize.query(
|
|
20
|
+
"CREATE INDEX IF NOT EXISTS idx_meter_events_subscription_status ON meter_events(json_extract(payload, '$.subscription_id'), status, livemode, created_at)"
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
// credit_transactions
|
|
24
|
+
await createIndexIfNotExists(context, 'credit_transactions', ['created_at'], 'idx_credit_transactions_created');
|
|
25
|
+
await createIndexIfNotExists(
|
|
26
|
+
context,
|
|
27
|
+
'credit_transactions',
|
|
28
|
+
['customer_id', 'created_at'],
|
|
29
|
+
'idx_credit_transactions_customer_created'
|
|
30
|
+
);
|
|
31
|
+
await createIndexIfNotExists(
|
|
32
|
+
context,
|
|
33
|
+
'credit_transactions',
|
|
34
|
+
['meter_id', 'created_at'],
|
|
35
|
+
'idx_credit_transactions_meter_created'
|
|
36
|
+
);
|
|
37
|
+
await createIndexIfNotExists(
|
|
38
|
+
context,
|
|
39
|
+
'credit_transactions',
|
|
40
|
+
['meter_event_name', 'created_at'],
|
|
41
|
+
'idx_credit_transactions_event_created'
|
|
42
|
+
);
|
|
43
|
+
await createIndexIfNotExists(
|
|
44
|
+
context,
|
|
45
|
+
'credit_transactions',
|
|
46
|
+
['subscription_id', 'created_at'],
|
|
47
|
+
'idx_credit_transactions_subscription_created'
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
// credit_grants
|
|
51
|
+
await createIndexIfNotExists(
|
|
52
|
+
context,
|
|
53
|
+
'credit_grants',
|
|
54
|
+
['customer_id', 'currency_id', 'status'],
|
|
55
|
+
'idx_credit_grants_customer_currency_status'
|
|
56
|
+
);
|
|
57
|
+
await createIndexIfNotExists(context, 'credit_grants', ['customer_id'], 'idx_credit_grants_customer');
|
|
58
|
+
await createIndexIfNotExists(context, 'credit_grants', ['currency_id'], 'idx_credit_grants_currency');
|
|
59
|
+
await createIndexIfNotExists(context, 'credit_grants', ['status'], 'idx_credit_grants_status');
|
|
60
|
+
|
|
61
|
+
// customers
|
|
62
|
+
await createIndexIfNotExists(context, 'customers', ['did'], 'idx_customers_did');
|
|
63
|
+
|
|
64
|
+
// subscriptions
|
|
65
|
+
await createIndexIfNotExists(context, 'subscriptions', ['customer_id'], 'idx_subscriptions_customer_id');
|
|
66
|
+
|
|
67
|
+
// meters
|
|
68
|
+
await createIndexIfNotExists(context, 'meters', ['livemode', 'status'], 'idx_meters_livemode_status');
|
|
69
|
+
|
|
70
|
+
// events
|
|
71
|
+
await createIndexIfNotExists(context, 'events', ['type', 'created_at'], 'idx_events_type_created');
|
|
72
|
+
await createIndexIfNotExists(
|
|
73
|
+
context,
|
|
74
|
+
'events',
|
|
75
|
+
['object_type', 'object_id', 'created_at'],
|
|
76
|
+
'idx_events_object_type_id_created'
|
|
77
|
+
);
|
|
78
|
+
await createIndexIfNotExists(context, 'events', ['object_id', 'created_at'], 'idx_events_object_id_created');
|
|
79
|
+
|
|
80
|
+
// jobs
|
|
81
|
+
await createIndexIfNotExists(context, 'jobs', ['queue', 'will_run_at'], 'idx_jobs_queue_run_at');
|
|
82
|
+
await createIndexIfNotExists(context, 'jobs', ['cancelled', 'will_run_at'], 'idx_jobs_cancelled_run_at');
|
|
83
|
+
|
|
84
|
+
// invoices
|
|
85
|
+
await createIndexIfNotExists(context, 'invoices', ['status'], 'idx_invoices_status');
|
|
86
|
+
|
|
87
|
+
// checkout_sessions
|
|
88
|
+
await createIndexIfNotExists(
|
|
89
|
+
context,
|
|
90
|
+
'checkout_sessions',
|
|
91
|
+
['payment_link_id', 'status', 'livemode'],
|
|
92
|
+
'idx_checkout_sessions_payment_link_status_livemode'
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
console.log('✅ Performance optimization completed');
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
export const down: Migration = async ({ context }) => {
|
|
99
|
+
console.log('🔄 Rolling back performance optimization...');
|
|
100
|
+
|
|
101
|
+
await context.removeIndex('meter_events', 'idx_meter_events_status_created');
|
|
102
|
+
await context.removeIndex('meter_events', 'idx_meter_events_event_livemode_created');
|
|
103
|
+
await context.removeIndex('meter_events', 'idx_meter_events_customer_status');
|
|
104
|
+
await context.removeIndex('meter_events', 'idx_meter_events_subscription_status');
|
|
105
|
+
|
|
106
|
+
await context.removeIndex('credit_transactions', 'idx_credit_transactions_created');
|
|
107
|
+
await context.removeIndex('credit_transactions', 'idx_credit_transactions_customer_created');
|
|
108
|
+
await context.removeIndex('credit_transactions', 'idx_credit_transactions_meter_created');
|
|
109
|
+
await context.removeIndex('credit_transactions', 'idx_credit_transactions_event_created');
|
|
110
|
+
await context.removeIndex('credit_transactions', 'idx_credit_transactions_subscription_created');
|
|
111
|
+
|
|
112
|
+
await context.removeIndex('credit_grants', 'idx_credit_grants_customer_currency_status');
|
|
113
|
+
await context.removeIndex('credit_grants', 'idx_credit_grants_customer');
|
|
114
|
+
await context.removeIndex('credit_grants', 'idx_credit_grants_currency');
|
|
115
|
+
await context.removeIndex('credit_grants', 'idx_credit_grants_status');
|
|
116
|
+
|
|
117
|
+
await context.removeIndex('customers', 'idx_customers_did');
|
|
118
|
+
await context.removeIndex('subscriptions', 'idx_subscriptions_customer_id');
|
|
119
|
+
await context.removeIndex('meters', 'idx_meters_livemode_status');
|
|
120
|
+
await context.removeIndex('events', 'idx_events_type_created');
|
|
121
|
+
await context.removeIndex('events', 'idx_events_object_type_id_created');
|
|
122
|
+
await context.removeIndex('events', 'idx_events_object_id_created');
|
|
123
|
+
await context.removeIndex('jobs', 'idx_jobs_queue_run_at');
|
|
124
|
+
await context.removeIndex('jobs', 'idx_jobs_cancelled_run_at');
|
|
125
|
+
await context.removeIndex('invoices', 'idx_invoices_status');
|
|
126
|
+
await context.removeIndex('checkout_sessions', 'idx_checkout_sessions_payment_link_status_livemode');
|
|
127
|
+
|
|
128
|
+
console.log('✅ Rollback completed');
|
|
129
|
+
};
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
Model,
|
|
8
8
|
Op,
|
|
9
9
|
QueryTypes,
|
|
10
|
+
Sequelize,
|
|
10
11
|
WhereOptions,
|
|
11
12
|
} from 'sequelize';
|
|
12
13
|
import type { LiteralUnion } from 'type-fest';
|
|
@@ -419,11 +420,18 @@ export class MeterEvent extends Model<InferAttributes<MeterEvent>, InferCreation
|
|
|
419
420
|
if (status) {
|
|
420
421
|
where.status = Array.isArray(status) ? { [Op.in]: status } : status;
|
|
421
422
|
}
|
|
423
|
+
|
|
422
424
|
if (subscriptionId) {
|
|
423
|
-
where[
|
|
425
|
+
where[Op.and] = where[Op.and] || [];
|
|
426
|
+
where[Op.and].push(
|
|
427
|
+
Sequelize.where(Sequelize.fn('json_extract', Sequelize.col('payload'), '$.subscription_id'), subscriptionId)
|
|
428
|
+
);
|
|
424
429
|
}
|
|
425
430
|
if (customerId) {
|
|
426
|
-
where[
|
|
431
|
+
where[Op.and] = where[Op.and] || [];
|
|
432
|
+
where[Op.and].push(
|
|
433
|
+
Sequelize.where(Sequelize.fn('json_extract', Sequelize.col('payload'), '$.customer_id'), customerId)
|
|
434
|
+
);
|
|
427
435
|
}
|
|
428
436
|
|
|
429
437
|
return this._getPendingAmounts(where, currencyId);
|
package/blocklet.yml
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "payment-kit",
|
|
3
|
-
"version": "1.22.
|
|
3
|
+
"version": "1.22.31",
|
|
4
4
|
"scripts": {
|
|
5
5
|
"dev": "blocklet dev --open",
|
|
6
6
|
"lint": "tsc --noEmit && eslint src api/src --ext .mjs,.js,.jsx,.ts,.tsx",
|
|
@@ -44,35 +44,35 @@
|
|
|
44
44
|
]
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
|
-
"@abtnode/cron": "^1.17.4
|
|
48
|
-
"@arcblock/did": "^1.27.
|
|
47
|
+
"@abtnode/cron": "^1.17.4",
|
|
48
|
+
"@arcblock/did": "^1.27.14",
|
|
49
49
|
"@arcblock/did-connect-react": "^3.2.11",
|
|
50
50
|
"@arcblock/did-connect-storage-nedb": "^1.8.0",
|
|
51
|
-
"@arcblock/did-util": "^1.27.
|
|
52
|
-
"@arcblock/jwt": "^1.27.
|
|
51
|
+
"@arcblock/did-util": "^1.27.14",
|
|
52
|
+
"@arcblock/jwt": "^1.27.14",
|
|
53
53
|
"@arcblock/react-hooks": "^3.2.11",
|
|
54
54
|
"@arcblock/ux": "^3.2.11",
|
|
55
|
-
"@arcblock/validator": "^1.27.
|
|
56
|
-
"@blocklet/did-space-js": "^1.2.
|
|
55
|
+
"@arcblock/validator": "^1.27.14",
|
|
56
|
+
"@blocklet/did-space-js": "^1.2.9",
|
|
57
57
|
"@blocklet/error": "^0.3.4",
|
|
58
|
-
"@blocklet/js-sdk": "^1.17.4
|
|
59
|
-
"@blocklet/logger": "^1.17.4
|
|
60
|
-
"@blocklet/payment-broker-client": "1.22.
|
|
61
|
-
"@blocklet/payment-react": "1.22.
|
|
62
|
-
"@blocklet/payment-vendor": "1.22.
|
|
63
|
-
"@blocklet/sdk": "^1.17.4
|
|
58
|
+
"@blocklet/js-sdk": "^1.17.4",
|
|
59
|
+
"@blocklet/logger": "^1.17.4",
|
|
60
|
+
"@blocklet/payment-broker-client": "1.22.31",
|
|
61
|
+
"@blocklet/payment-react": "1.22.31",
|
|
62
|
+
"@blocklet/payment-vendor": "1.22.31",
|
|
63
|
+
"@blocklet/sdk": "^1.17.4",
|
|
64
64
|
"@blocklet/ui-react": "^3.2.11",
|
|
65
|
-
"@blocklet/uploader": "^0.3.
|
|
66
|
-
"@blocklet/xss": "^0.3.
|
|
65
|
+
"@blocklet/uploader": "^0.3.14",
|
|
66
|
+
"@blocklet/xss": "^0.3.12",
|
|
67
67
|
"@mui/icons-material": "^7.1.2",
|
|
68
68
|
"@mui/lab": "7.0.0-beta.14",
|
|
69
69
|
"@mui/material": "^7.1.2",
|
|
70
70
|
"@mui/system": "^7.1.1",
|
|
71
|
-
"@ocap/asset": "^1.27.
|
|
72
|
-
"@ocap/client": "^1.27.
|
|
73
|
-
"@ocap/mcrypto": "^1.27.
|
|
74
|
-
"@ocap/util": "^1.27.
|
|
75
|
-
"@ocap/wallet": "^1.27.
|
|
71
|
+
"@ocap/asset": "^1.27.14",
|
|
72
|
+
"@ocap/client": "^1.27.14",
|
|
73
|
+
"@ocap/mcrypto": "^1.27.14",
|
|
74
|
+
"@ocap/util": "^1.27.14",
|
|
75
|
+
"@ocap/wallet": "^1.27.14",
|
|
76
76
|
"@stripe/react-stripe-js": "^2.9.0",
|
|
77
77
|
"@stripe/stripe-js": "^2.4.0",
|
|
78
78
|
"ahooks": "^3.8.5",
|
|
@@ -127,9 +127,9 @@
|
|
|
127
127
|
"web3": "^4.16.0"
|
|
128
128
|
},
|
|
129
129
|
"devDependencies": {
|
|
130
|
-
"@abtnode/types": "^1.17.4
|
|
130
|
+
"@abtnode/types": "^1.17.4",
|
|
131
131
|
"@arcblock/eslint-config-ts": "^0.3.3",
|
|
132
|
-
"@blocklet/payment-types": "1.22.
|
|
132
|
+
"@blocklet/payment-types": "1.22.31",
|
|
133
133
|
"@types/cookie-parser": "^1.4.9",
|
|
134
134
|
"@types/cors": "^2.8.19",
|
|
135
135
|
"@types/debug": "^4.1.12",
|
|
@@ -176,5 +176,5 @@
|
|
|
176
176
|
"parser": "typescript"
|
|
177
177
|
}
|
|
178
178
|
},
|
|
179
|
-
"gitHead": "
|
|
179
|
+
"gitHead": "fc03087134d309474256b122eba125e4a01dd6c0"
|
|
180
180
|
}
|
|
@@ -4,11 +4,12 @@ import type { ReactNode } from 'react';
|
|
|
4
4
|
type Props = {
|
|
5
5
|
title: string | ReactNode;
|
|
6
6
|
children?: ReactNode;
|
|
7
|
+
action?: ReactNode;
|
|
7
8
|
mb?: number;
|
|
8
9
|
mt?: number;
|
|
9
10
|
};
|
|
10
11
|
|
|
11
|
-
export default function SectionHeader({ title, children = null, mb = 1.5, mt = 1.5 }: Props) {
|
|
12
|
+
export default function SectionHeader({ title, children = null, action = null, mb = 1.5, mt = 1.5 }: Props) {
|
|
12
13
|
return (
|
|
13
14
|
<Stack
|
|
14
15
|
className="section-header"
|
|
@@ -33,7 +34,7 @@ export default function SectionHeader({ title, children = null, mb = 1.5, mt = 1
|
|
|
33
34
|
component="div">
|
|
34
35
|
{title}
|
|
35
36
|
</Typography>
|
|
36
|
-
{children}
|
|
37
|
+
{action || children}
|
|
37
38
|
</Stack>
|
|
38
39
|
);
|
|
39
40
|
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/* eslint-disable react/no-unstable-nested-components */
|
|
2
2
|
import CodeBlock from '@arcblock/ux/lib/CodeBlock';
|
|
3
|
-
import
|
|
3
|
+
import Toast from '@arcblock/ux/lib/Toast';
|
|
4
|
+
import { api, formatTime, ConfirmDialog } from '@blocklet/payment-react';
|
|
4
5
|
import type { Paginated, TEvent, TWebhookAttemptExpanded } from '@blocklet/payment-types';
|
|
5
|
-
import { CheckCircleOutlined, ErrorOutlined } from '@mui/icons-material';
|
|
6
|
+
import { CheckCircleOutlined, ErrorOutlined, RefreshOutlined } from '@mui/icons-material';
|
|
6
7
|
import {
|
|
7
8
|
Box,
|
|
8
9
|
Button,
|
|
@@ -17,7 +18,8 @@ import {
|
|
|
17
18
|
Stack,
|
|
18
19
|
Typography,
|
|
19
20
|
} from '@mui/material';
|
|
20
|
-
import {
|
|
21
|
+
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
22
|
+
import { useInfiniteScroll, useRequest, useSetState } from 'ahooks';
|
|
21
23
|
import React, { useEffect, useState } from 'react';
|
|
22
24
|
|
|
23
25
|
import { isEmpty } from 'lodash';
|
|
@@ -51,7 +53,8 @@ type Props = {
|
|
|
51
53
|
};
|
|
52
54
|
|
|
53
55
|
export default function WebhookAttempts({ event_id = '', webhook_endpoint_id = '', event = undefined }: Props) {
|
|
54
|
-
const {
|
|
56
|
+
const { t } = useLocaleContext();
|
|
57
|
+
const { data, loadMore, loadingMore, loading, reload } = useInfiniteScroll<Paginated<TWebhookAttemptExpanded>>(
|
|
55
58
|
(d) => {
|
|
56
59
|
const size = 15;
|
|
57
60
|
const page = d ? Math.ceil(d.list.length / size) + 1 : 1;
|
|
@@ -68,8 +71,27 @@ export default function WebhookAttempts({ event_id = '', webhook_endpoint_id = '
|
|
|
68
71
|
const [selected, setSelected] = useState<
|
|
69
72
|
(TWebhookAttemptExpanded & { event: TEvent & { requestInfo?: RequestInfo } }) | null
|
|
70
73
|
>(null);
|
|
74
|
+
|
|
75
|
+
const [state, setState] = useSetState({
|
|
76
|
+
retryEventId: '',
|
|
77
|
+
});
|
|
78
|
+
|
|
71
79
|
const groupedAttempts = groupAttemptsByDate(attempts);
|
|
72
80
|
|
|
81
|
+
const { loading: retrying, run: retryWebhook } = useRequest(
|
|
82
|
+
(eventId: string) => api.post(`/api/events/${eventId}/retry-webhooks`).then((res) => res.data),
|
|
83
|
+
{
|
|
84
|
+
manual: true,
|
|
85
|
+
onSuccess: (result) => {
|
|
86
|
+
Toast.success(result.message || t('admin.event.retryOptions.success'));
|
|
87
|
+
reload();
|
|
88
|
+
},
|
|
89
|
+
onError: (err: any) => {
|
|
90
|
+
Toast.error(err.response?.data?.error || t('admin.event.retryOptions.error'));
|
|
91
|
+
},
|
|
92
|
+
}
|
|
93
|
+
);
|
|
94
|
+
|
|
73
95
|
useEffect(() => {
|
|
74
96
|
if (!selected && data?.list.length) {
|
|
75
97
|
setSelected(data.list[0] as TWebhookAttemptExpanded);
|
|
@@ -80,6 +102,17 @@ export default function WebhookAttempts({ event_id = '', webhook_endpoint_id = '
|
|
|
80
102
|
setSelected(attempt);
|
|
81
103
|
};
|
|
82
104
|
|
|
105
|
+
const handleRetryClick = (eventId: string) => {
|
|
106
|
+
setState({ retryEventId: eventId });
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const handleConfirmRetry = () => {
|
|
110
|
+
if (state.retryEventId) {
|
|
111
|
+
retryWebhook(state.retryEventId);
|
|
112
|
+
setState({ retryEventId: '' });
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
|
|
83
116
|
if (loading) {
|
|
84
117
|
return <CircularProgress />;
|
|
85
118
|
}
|
|
@@ -94,7 +127,7 @@ export default function WebhookAttempts({ event_id = '', webhook_endpoint_id = '
|
|
|
94
127
|
sx={{
|
|
95
128
|
color: 'text.secondary',
|
|
96
129
|
}}>
|
|
97
|
-
|
|
130
|
+
{t('admin.event.noAttempts')}
|
|
98
131
|
</Typography>
|
|
99
132
|
) : (
|
|
100
133
|
<>
|
|
@@ -140,7 +173,7 @@ export default function WebhookAttempts({ event_id = '', webhook_endpoint_id = '
|
|
|
140
173
|
</List>
|
|
141
174
|
{hasMore && (
|
|
142
175
|
<Button variant="text" type="button" color="inherit" onClick={loadMore} disabled={loadingMore}>
|
|
143
|
-
{loadingMore ? '
|
|
176
|
+
{loadingMore ? t('common.loading') : t('common.loadMore')}
|
|
144
177
|
</Button>
|
|
145
178
|
)}
|
|
146
179
|
{!hasMore && (
|
|
@@ -148,7 +181,7 @@ export default function WebhookAttempts({ event_id = '', webhook_endpoint_id = '
|
|
|
148
181
|
sx={{
|
|
149
182
|
color: 'text.secondary',
|
|
150
183
|
}}>
|
|
151
|
-
|
|
184
|
+
{t('common.noMoreData')}
|
|
152
185
|
</Typography>
|
|
153
186
|
)}
|
|
154
187
|
</>
|
|
@@ -161,9 +194,31 @@ export default function WebhookAttempts({ event_id = '', webhook_endpoint_id = '
|
|
|
161
194
|
}}>
|
|
162
195
|
{selected && (
|
|
163
196
|
<Stack direction="column" spacing={2} sx={{ pt: 3, pl: 3, borderLeft: '1px solid', borderColor: 'divider' }}>
|
|
164
|
-
<
|
|
197
|
+
<Stack direction="row" sx={{ justifyContent: 'space-between', alignItems: 'center' }}>
|
|
198
|
+
<Typography variant="h6">{event_id ? selected.endpoint.url : selected.event.type}</Typography>
|
|
199
|
+
<Button
|
|
200
|
+
variant="outlined"
|
|
201
|
+
size="small"
|
|
202
|
+
startIcon={<RefreshOutlined />}
|
|
203
|
+
onClick={() => handleRetryClick(selected.event_id)}
|
|
204
|
+
disabled={retrying}>
|
|
205
|
+
{retrying ? t('admin.event.retrying') : t('admin.event.retry')}
|
|
206
|
+
</Button>
|
|
207
|
+
</Stack>
|
|
208
|
+
{state.retryEventId && (
|
|
209
|
+
<ConfirmDialog
|
|
210
|
+
onConfirm={handleConfirmRetry}
|
|
211
|
+
onCancel={() => setState({ retryEventId: '' })}
|
|
212
|
+
title={t('admin.event.retryOptions.title')}
|
|
213
|
+
message={t('admin.event.retryOptions.confirmMessage')}
|
|
214
|
+
loading={retrying}
|
|
215
|
+
color="primary"
|
|
216
|
+
/>
|
|
217
|
+
)}
|
|
165
218
|
<Box>
|
|
166
|
-
<Typography variant="h6">
|
|
219
|
+
<Typography variant="h6">
|
|
220
|
+
{t('admin.event.response')} ({selected.response_status})
|
|
221
|
+
</Typography>
|
|
167
222
|
{/* @ts-ignore */}
|
|
168
223
|
<CodeBlock language="json">{JSON.stringify(selected.response_body, null, 2)}</CodeBlock>
|
|
169
224
|
</Box>
|
|
@@ -174,7 +229,7 @@ export default function WebhookAttempts({ event_id = '', webhook_endpoint_id = '
|
|
|
174
229
|
sx={{
|
|
175
230
|
alignItems: 'center',
|
|
176
231
|
}}>
|
|
177
|
-
<Typography variant="h6">
|
|
232
|
+
<Typography variant="h6">{t('admin.event.request')}</Typography>
|
|
178
233
|
<RequestInfoPopper
|
|
179
234
|
// @ts-ignore
|
|
180
235
|
requestInfo={selected?.event?.requestInfo}
|
|
@@ -194,7 +249,7 @@ export default function WebhookAttempts({ event_id = '', webhook_endpoint_id = '
|
|
|
194
249
|
sx={{
|
|
195
250
|
alignItems: 'center',
|
|
196
251
|
}}>
|
|
197
|
-
<Typography variant="h6">
|
|
252
|
+
<Typography variant="h6">{t('admin.event.eventData')}</Typography>
|
|
198
253
|
<RequestInfoPopper
|
|
199
254
|
// @ts-ignore
|
|
200
255
|
requestInfo={event.requestInfo}
|
package/src/locales/en.tsx
CHANGED
|
@@ -31,6 +31,8 @@ export default flat({
|
|
|
31
31
|
latinOnly:
|
|
32
32
|
'Must contain at least one letter and cannot include Chinese characters or special characters such as <, >, ", \' or \\',
|
|
33
33
|
loading: 'Loading...',
|
|
34
|
+
loadMore: 'Load more',
|
|
35
|
+
noMoreData: 'No more data',
|
|
34
36
|
rechargeTime: 'Recharge Time',
|
|
35
37
|
submit: 'Submit',
|
|
36
38
|
custom: 'Custom',
|
|
@@ -1217,6 +1219,19 @@ export default flat({
|
|
|
1217
1219
|
webhooks: 'Webhook Attempts',
|
|
1218
1220
|
type: 'Type',
|
|
1219
1221
|
pendingWebhooks: 'Pending Webhooks',
|
|
1222
|
+
noAttempts: 'No Attempt',
|
|
1223
|
+
retry: 'Retry',
|
|
1224
|
+
retrying: 'Retrying...',
|
|
1225
|
+
retryOptions: {
|
|
1226
|
+
title: 'Retry Webhook',
|
|
1227
|
+
confirmMessage:
|
|
1228
|
+
'Are you sure you want to retry this webhook? This will send another request to the webhook endpoint.',
|
|
1229
|
+
success: 'Webhook scheduled for retry',
|
|
1230
|
+
error: 'Failed to retry webhook',
|
|
1231
|
+
},
|
|
1232
|
+
response: 'Response',
|
|
1233
|
+
request: 'Request',
|
|
1234
|
+
eventData: 'Event Data',
|
|
1220
1235
|
},
|
|
1221
1236
|
invoice: {
|
|
1222
1237
|
view: 'View invoice',
|
package/src/locales/zh.tsx
CHANGED
|
@@ -29,6 +29,8 @@ export default flat({
|
|
|
29
29
|
invalidCharacters: '无效字符',
|
|
30
30
|
latinOnly: '至少包含一个字母,并且不能包含中文字符和特殊字符如 <, >、"、’ 或 \\',
|
|
31
31
|
loading: '加载中...',
|
|
32
|
+
loadMore: '加载更多',
|
|
33
|
+
noMoreData: '没有更多数据',
|
|
32
34
|
rechargeTime: '充值时间',
|
|
33
35
|
submit: '提交',
|
|
34
36
|
custom: '自定义',
|
|
@@ -1171,6 +1173,18 @@ export default flat({
|
|
|
1171
1173
|
webhooks: '钩子调用',
|
|
1172
1174
|
type: '类型',
|
|
1173
1175
|
pendingWebhooks: '待处理的钩子',
|
|
1176
|
+
noAttempts: '无调用记录',
|
|
1177
|
+
retry: '重试',
|
|
1178
|
+
retrying: '重试中...',
|
|
1179
|
+
retryOptions: {
|
|
1180
|
+
title: '重试 Webhook',
|
|
1181
|
+
confirmMessage: '确定要重试此 webhook 吗?这将向 webhook 端点发送另一个请求。',
|
|
1182
|
+
success: 'Webhook 已安排重试',
|
|
1183
|
+
error: '重试 webhook 失败',
|
|
1184
|
+
},
|
|
1185
|
+
response: '响应',
|
|
1186
|
+
request: '请求',
|
|
1187
|
+
eventData: '事件数据',
|
|
1174
1188
|
events: {
|
|
1175
1189
|
dailyEventCount: '每日事件数量',
|
|
1176
1190
|
dailyTotalValue: '每日总使用量',
|