payment-kit 1.18.25 → 1.18.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/api/src/libs/notification/template/aggregated-subscription-renewed.ts +165 -0
  2. package/api/src/libs/notification/template/one-time-payment-succeeded.ts +2 -5
  3. package/api/src/libs/notification/template/subscription-canceled.ts +2 -3
  4. package/api/src/libs/notification/template/subscription-refund-succeeded.ts +7 -4
  5. package/api/src/libs/notification/template/subscription-renew-failed.ts +3 -5
  6. package/api/src/libs/notification/template/subscription-renewed.ts +2 -2
  7. package/api/src/libs/notification/template/subscription-stake-slash-succeeded.ts +2 -3
  8. package/api/src/libs/notification/template/subscription-succeeded.ts +2 -2
  9. package/api/src/libs/notification/template/subscription-upgraded.ts +5 -5
  10. package/api/src/libs/notification/template/subscription-will-renew.ts +2 -2
  11. package/api/src/libs/queue/index.ts +6 -0
  12. package/api/src/libs/queue/store.ts +13 -1
  13. package/api/src/libs/util.ts +22 -1
  14. package/api/src/locales/en.ts +5 -0
  15. package/api/src/locales/zh.ts +5 -0
  16. package/api/src/queues/notification.ts +353 -11
  17. package/api/src/routes/customers.ts +61 -0
  18. package/api/src/routes/subscriptions.ts +1 -1
  19. package/api/src/store/migrations/20250328-notification-preference.ts +29 -0
  20. package/api/src/store/models/customer.ts +15 -1
  21. package/api/src/store/models/types.ts +17 -1
  22. package/blocklet.yml +1 -1
  23. package/package.json +19 -19
  24. package/src/components/customer/form.tsx +21 -2
  25. package/src/components/customer/notification-preference.tsx +428 -0
  26. package/src/components/layout/user.tsx +1 -1
  27. package/src/locales/en.tsx +30 -0
  28. package/src/locales/zh.tsx +30 -0
  29. package/src/pages/customer/index.tsx +26 -22
  30. package/src/pages/customer/recharge/account.tsx +7 -7
  31. package/src/pages/customer/subscription/embed.tsx +1 -0
@@ -1,6 +1,8 @@
1
1
  /* eslint-disable @typescript-eslint/indent */
2
+ import { Op } from 'sequelize';
2
3
  import { events } from '../libs/event';
3
4
  import logger from '../libs/logger';
5
+ import dayjs from '../libs/dayjs';
4
6
  import { Notification } from '../libs/notification';
5
7
  import type { BaseEmailTemplate } from '../libs/notification/template/base';
6
8
  import {
@@ -56,7 +58,16 @@ import {
56
58
  SubscriptionStakeSlashSucceededEmailTemplateOptions,
57
59
  } from '../libs/notification/template/subscription-stake-slash-succeeded';
58
60
  import createQueue from '../libs/queue';
59
- import { CheckoutSession, EventType, Invoice, PaymentLink, Payout, Refund, Subscription } from '../store/models';
61
+ import {
62
+ CheckoutSession,
63
+ EventType,
64
+ Invoice,
65
+ PaymentLink,
66
+ Payout,
67
+ Refund,
68
+ Subscription,
69
+ Customer,
70
+ } from '../store/models';
60
71
  import {
61
72
  UsageReportEmptyEmailTemplate,
62
73
  UsageReportEmptyEmailTemplateOptions,
@@ -73,9 +84,22 @@ import {
73
84
  CustomerRevenueSucceededEmailTemplate,
74
85
  CustomerRevenueSucceededEmailTemplateOptions,
75
86
  } from '../libs/notification/template/customer-revenue-succeeded';
87
+ import {
88
+ AggregatedSubscriptionRenewedEmailTemplate,
89
+ AggregatedSubscriptionRenewedEmailTemplateOptions,
90
+ } from '../libs/notification/template/aggregated-subscription-renewed';
91
+ import type { TJob } from '../store/models/job';
76
92
 
77
93
  export type NotificationQueueJobOptions = any;
78
94
 
95
+ interface NotificationPreference {
96
+ frequency: 'default' | 'daily' | 'weekly' | 'monthly';
97
+ schedule?: {
98
+ time: string; // HH:mm 格式
99
+ date?: number; // weekly: 0-6, monthly: 1-31
100
+ };
101
+ }
102
+
79
103
  export type NotificationQueueJobType =
80
104
  | EventType
81
105
  | 'customer.subscription.will_renew'
@@ -91,6 +115,62 @@ export type NotificationQueueJob = {
91
115
  options: NotificationQueueJobOptions;
92
116
  };
93
117
 
118
+ export interface AggregatedNotificationJob {
119
+ type: NotificationQueueJobType;
120
+ options: {
121
+ customer_id: string;
122
+ items: NotificationItem[];
123
+ time_range: {
124
+ start: number;
125
+ end: number;
126
+ };
127
+ };
128
+ }
129
+
130
+ function calculateNextNotificationTime(preference: NotificationPreference): number {
131
+ if (preference.frequency === 'default' || !preference.schedule) {
132
+ return Math.floor(Date.now() / 1000);
133
+ }
134
+
135
+ const now = dayjs();
136
+ const [hour = 10, minute = 0] = preference.schedule?.time ? preference.schedule.time.split(':').map(Number) : [10, 0];
137
+ let nextTime = dayjs().hour(hour).minute(minute).second(0);
138
+ const targetDate = preference.schedule?.date ?? 1;
139
+
140
+ switch (preference.frequency) {
141
+ case 'daily':
142
+ if (nextTime.isBefore(now)) {
143
+ nextTime = nextTime.add(1, 'day');
144
+ }
145
+ break;
146
+
147
+ case 'weekly':
148
+ nextTime = nextTime.day(targetDate);
149
+ if (nextTime.isBefore(now)) {
150
+ nextTime = nextTime.add(1, 'week');
151
+ }
152
+ break;
153
+
154
+ case 'monthly':
155
+ nextTime = nextTime.date(targetDate);
156
+ // if the date is not the same, set to the end of the month
157
+ if (nextTime.date() !== targetDate) {
158
+ nextTime = nextTime.endOf('month').hour(hour).minute(minute);
159
+ }
160
+ if (nextTime.isBefore(now)) {
161
+ nextTime = nextTime.add(1, 'month');
162
+ if (nextTime.date() !== targetDate) {
163
+ nextTime = nextTime.endOf('month').hour(hour).minute(minute);
164
+ }
165
+ }
166
+ break;
167
+ default:
168
+ throw new Error(`Unknown frequency: ${preference.frequency}`);
169
+ }
170
+
171
+ return Math.floor(nextTime.valueOf() / 1000);
172
+ }
173
+
94
174
  function getNotificationTemplate(job: NotificationQueueJob): BaseEmailTemplate {
95
175
  if (job.type === 'usage.report.empty') {
96
176
  return new UsageReportEmptyEmailTemplate(job.options as UsageReportEmptyEmailTemplateOptions);
@@ -156,10 +236,158 @@ async function handleNotificationJob(job: NotificationQueueJob): Promise<void> {
156
236
  try {
157
237
  const template = getNotificationTemplate(job);
158
238
  await new Notification(template).send();
159
- logger.info('handleNotificationJob.success', { job });
239
+ logger.info('handleImmediateNotificationJob.success', { job });
160
240
  } catch (error) {
161
- logger.error('handleNotificationJob.error.$job', job);
162
- logger.error('handleNotificationJob.error', error);
241
+ logger.error('handleImmediateNotificationJob.error', error);
242
+ throw error;
243
+ }
244
+ }
245
+
246
+ const MAX_AGGREGATED_ITEMS = 1000;
247
+
248
+ interface NotificationItem<T = Record<string, any>> {
249
+ event_id: string;
250
+ occurred_at: number;
251
+ data: T;
252
+ }
253
+
254
+ /**
255
+ * Handles immediate notifications by pushing them directly to the notification queue
256
+ */
257
+ function handleImmediateNotification(type: string, data: Record<string, any>, extraIds: string[] = []) {
258
+ const idParts = [type];
259
+ if (extraIds.length) {
260
+ idParts.push(...extraIds);
261
+ } else {
262
+ idParts.push(Date.now().toString());
263
+ }
264
+
265
+ return notificationQueue.push({
266
+ id: idParts.join('.'),
267
+ job: {
268
+ type,
269
+ options: data,
270
+ },
271
+ });
272
+ }
273
+
274
+ /**
275
+ * Handles aggregated notifications with support for:
276
+ * - Automatic fallback to immediate notification on error
277
+ * - Item limit enforcement
278
+ * - Time range tracking
279
+ */
280
+ async function handleAggregatedNotification<T extends Record<string, any>>({
281
+ type,
282
+ customerId,
283
+ preference,
284
+ newItem,
285
+ extraIds,
286
+ }: {
287
+ type: NotificationQueueJobType;
288
+ customerId: string;
289
+ preference: NotificationPreference;
290
+ newItem: NotificationItem<T>;
291
+ extraIds: string[];
292
+ }) {
293
+ try {
294
+ const nextNotificationTime = calculateNextNotificationTime(preference);
295
+ const jobId = `${type}.${customerId}.${nextNotificationTime}`;
296
+ const existingJob = await aggregatedNotificationQueue.get(jobId);
297
+
298
+ if (existingJob) {
299
+ const updatedItems = [...(existingJob.options.items || []), newItem];
300
+
301
+ if (updatedItems.length >= MAX_AGGREGATED_ITEMS) {
302
+ await aggregatedNotificationQueue.push({
303
+ id: `${jobId}.immediate`,
304
+ job: {
305
+ type,
306
+ options: {
307
+ customer_id: customerId,
308
+ items: existingJob.options.items,
309
+ time_range: {
310
+ start: existingJob.options.time_range.start,
311
+ end: Math.floor(Date.now() / 1000),
312
+ },
313
+ },
314
+ },
315
+ });
316
+ logger.info('immediate aggregated notification job', { jobId });
317
+
318
+ await aggregatedNotificationQueue.update(jobId, {
319
+ job: {
320
+ type,
321
+ options: {
322
+ customer_id: customerId,
323
+ items: [newItem],
324
+ time_range: {
325
+ start: Math.floor(Date.now() / 1000),
326
+ end: nextNotificationTime,
327
+ },
328
+ },
329
+ },
330
+ runAt: nextNotificationTime,
331
+ });
332
+ logger.info('update aggregated notification job', { jobId });
333
+ } else {
334
+ await aggregatedNotificationQueue.update(jobId, {
335
+ job: {
336
+ type,
337
+ options: {
338
+ customer_id: customerId,
339
+ items: updatedItems,
340
+ time_range: {
341
+ start: existingJob.options.time_range.start,
342
+ end: nextNotificationTime,
343
+ },
344
+ },
345
+ },
346
+ runAt: nextNotificationTime,
347
+ });
348
+ }
349
+ logger.info('updated aggregated notification job', { jobId });
350
+ } else {
351
+ // Create new aggregation task
352
+ await aggregatedNotificationQueue.push({
353
+ id: jobId,
354
+ job: {
355
+ type,
356
+ options: {
357
+ customer_id: customerId,
358
+ items: [newItem],
359
+ time_range: {
360
+ start: Math.floor(Date.now() / 1000),
361
+ end: nextNotificationTime,
362
+ },
363
+ },
364
+ },
365
+ runAt: nextNotificationTime,
366
+ });
367
+ }
368
+ logger.info('created aggregated notification job', { jobId });
369
+ } catch (error) {
370
+ logger.error('Failed to handle aggregated notification', { error });
371
+ await handleImmediateNotification(type, newItem.data, extraIds);
372
+ }
373
+ }
374
+
375
+ function getAggregatedNotificationTemplate(job: AggregatedNotificationJob): BaseEmailTemplate {
376
+ if (job.type === 'customer.subscription.renewed') {
377
+ return new AggregatedSubscriptionRenewedEmailTemplate(
378
+ job.options as AggregatedSubscriptionRenewedEmailTemplateOptions
379
+ );
380
+ }
381
+ throw new Error(`Unknown aggregated notification type: ${job.type}`);
382
+ }
383
+
384
+ async function handleAggregatedNotificationJob(job: AggregatedNotificationJob): Promise<void> {
385
+ try {
386
+ const template = await getAggregatedNotificationTemplate(job);
387
+ await new Notification(template).send();
388
+ logger.info('handleAggregatedNotificationJob.success', { job });
389
+ } catch (error) {
390
+ logger.error('handleAggregatedNotificationJob.error', error);
163
391
  throw error;
164
392
  }
165
393
  }
@@ -174,6 +402,16 @@ export const notificationQueue = createQueue<NotificationQueueJob>({
174
402
  },
175
403
  });
176
404
 
405
+ export const aggregatedNotificationQueue = createQueue<AggregatedNotificationJob>({
406
+ name: 'aggregated_notification',
407
+ onJob: handleAggregatedNotificationJob,
408
+ options: {
409
+ concurrency: 10,
410
+ maxRetries: 3,
411
+ enableScheduledJob: true,
412
+ },
413
+ });
414
+
177
415
  /**
178
416
  *
179
417
  * @see https://team.arcblock.io/comment/discussions/0fba06ff-75b4-47d2-8c5c-f0379540ce03
@@ -239,16 +477,39 @@ export async function startNotificationQueue() {
239
477
  }
240
478
  });
241
479
 
242
- events.on('customer.subscription.renewed', (subscription: Subscription) => {
243
- notificationQueue.push({
244
- id: `customer.subscription.renewed.${subscription.id}.${subscription.latest_invoice_id}`,
245
- job: {
246
- type: 'customer.subscription.renewed',
247
- options: {
480
+ events.on('customer.subscription.renewed', async (subscription: Subscription) => {
481
+ const customer = await Customer.findByPk(subscription.customer_id);
482
+ const preference = customer?.preference?.notification;
483
+ if (!subscription.latest_invoice_id) {
484
+ logger.warn('Missing latest_invoice_id for subscription', { subscriptionId: subscription.id });
485
+ return;
486
+ }
487
+
488
+ if (!preference || preference.frequency === 'default') {
489
+ handleImmediateNotification(
490
+ 'customer.subscription.renewed',
491
+ {
492
+ subscriptionId: subscription.id,
493
+ invoiceId: subscription.latest_invoice_id,
494
+ },
495
+ [subscription.id, subscription.latest_invoice_id]
496
+ );
497
+ return;
498
+ }
499
+
500
+ await handleAggregatedNotification({
501
+ type: 'customer.subscription.renewed',
502
+ customerId: customer.id,
503
+ preference,
504
+ newItem: {
505
+ event_id: `${subscription.id}.${subscription.latest_invoice_id}`,
506
+ occurred_at: Math.floor(Date.now() / 1000),
507
+ data: {
248
508
  subscriptionId: subscription.id,
249
509
  invoiceId: subscription.latest_invoice_id,
250
- } as SubscriptionRenewedEmailTemplateOptions,
510
+ },
251
511
  },
512
+ extraIds: [subscription.id, subscription.latest_invoice_id],
252
513
  });
253
514
  });
254
515
 
@@ -362,3 +623,84 @@ export async function startNotificationQueue() {
362
623
  }
363
624
  });
364
625
  }
626
+
627
+ export async function handleNotificationPreferenceChange(
628
+ customerId: string,
629
+ newPreference?: NotificationPreference
630
+ ): Promise<void> {
631
+ // 1. get pending scheduled jobs for this customer
632
+ const pendingJobs = await aggregatedNotificationQueue.store.findJobs({
633
+ delay: { [Op.not]: -1 },
634
+ will_run_at: { [Op.gt]: Date.now() },
635
+ });
636
+
637
+ const customerJobs = pendingJobs.filter((job: TJob) => job.job?.options?.customer_id === customerId);
638
+
639
+ if (!customerJobs.length) {
640
+ return;
641
+ }
642
+
643
+ // 2. if changed to immediate notification, delete the old job and create a new one immediately
644
+ if (newPreference?.frequency === 'default') {
645
+ /* eslint-disable no-await-in-loop */
646
+ for (const job of customerJobs) {
647
+ try {
648
+ await aggregatedNotificationQueue.delete(job.id);
649
+ aggregatedNotificationQueue.push({
650
+ id: job.id,
651
+ job: {
652
+ ...job.job,
653
+ options: {
654
+ ...job.job.options,
655
+ time_range: {
656
+ start: job.job.options.time_range.start,
657
+ end: Math.floor(Date.now() / 1000),
658
+ },
659
+ },
660
+ },
661
+ });
662
+ } catch (error) {
663
+ logger.error('Failed to process immediate notification job', { error, jobId: job.id });
664
+ }
665
+ }
666
+ /* eslint-enable no-await-in-loop */
667
+ return;
668
+ }
669
+
670
+ // 3. for other frequency changes, recalculate the send time
671
+ if (newPreference) {
672
+ /* eslint-disable no-await-in-loop */
673
+ for (const job of customerJobs) {
674
+ try {
675
+ const nextTime = calculateNextNotificationTime(newPreference);
676
+ const oldScheduleTime = job.will_run_at ? job.will_run_at / 1000 : 0;
677
+
678
+ // if next notification time is different from the old schedule time, update the job
679
+ if (nextTime !== oldScheduleTime) {
680
+ // Keep the original job id format but update the timestamp
681
+ const jobIdParts = job.id.split('.');
682
+ const jobId = `${jobIdParts.slice(0, -1).join('.')}.${nextTime}`;
683
+
684
+ await aggregatedNotificationQueue.delete(job.id);
685
+ aggregatedNotificationQueue.push({
686
+ id: jobId,
687
+ job: {
688
+ ...job.job,
689
+ options: {
690
+ ...job.job.options,
691
+ time_range: {
692
+ start: job.job.options.time_range.start,
693
+ end: nextTime,
694
+ },
695
+ },
696
+ },
697
+ runAt: nextTime,
698
+ });
699
+ }
700
+ } catch (error) {
701
+ logger.error('Failed to process notification job', { error, jobId: job.id });
702
+ }
703
+ }
704
+ /* eslint-enable no-await-in-loop */
705
+ }
706
+ }
@@ -24,6 +24,7 @@ import {
24
24
  } from '../store/models';
25
25
  import { getSubscriptionPaymentAddress, calculateRecommendedRechargeAmount } from '../libs/subscription';
26
26
  import { expandLineItems } from '../libs/session';
27
+ import { handleNotificationPreferenceChange } from '../queues/notification';
27
28
 
28
29
  const router = Router();
29
30
  const auth = authenticate<Customer>({ component: true, roles: ['owner', 'admin'] });
@@ -181,6 +182,9 @@ router.get('/:id/overdue/invoices', sessionMiddleware(), async (req, res) => {
181
182
  if (!doc) {
182
183
  return res.status(404).json({ error: 'Customer not found' });
183
184
  }
185
+ if (doc.did !== req.user.did && !['admin', 'owner'].includes(req.user?.role)) {
186
+ return res.status(403).json({ error: 'You are not allowed to access this customer invoices' });
187
+ }
184
188
  const { rows: invoices, count } = await Invoice.findAndCountAll({
185
189
  where: {
186
190
  customer_id: doc.id,
@@ -367,6 +371,63 @@ router.get('/:id/summary', auth, async (req, res) => {
367
371
  }
368
372
  });
369
373
 
374
+ const updatePreferenceSchema = Joi.object({
375
+ notification: Joi.object({
376
+ frequency: Joi.string().valid('default', 'daily', 'weekly', 'monthly').required(),
377
+ schedule: Joi.object({
378
+ time: Joi.string().pattern(/^([01]?[0-9]|2[0-3]):[0-5][0-9]$/),
379
+ date: Joi.alternatives().conditional('..frequency', {
380
+ switch: [
381
+ { is: 'weekly', then: Joi.number().min(0).max(6).required() },
382
+ { is: 'monthly', then: Joi.number().min(1).max(31).required() },
383
+ { is: Joi.valid('daily', 'default'), then: Joi.number().optional() },
384
+ ],
385
+ }),
386
+ }).when('frequency', {
387
+ is: 'default',
388
+ then: Joi.optional(),
389
+ otherwise: Joi.required(),
390
+ }),
391
+ }).optional(),
392
+ }).unknown(false);
393
+
394
+ router.put('/preference', sessionMiddleware(), async (req, res) => {
395
+ try {
396
+ if (!req.user) {
397
+ return res.status(403).json({ error: 'Unauthorized' });
398
+ }
399
+
400
+ const doc = await Customer.findByPkOrDid(req.user.did as string);
401
+ if (!doc) {
402
+ return res.status(404).json({ error: 'Customer not found' });
403
+ }
404
+
405
+ const { error, value } = updatePreferenceSchema.validate(req.body);
406
+ if (error) {
407
+ return res.status(400).json({ error: error.message });
408
+ }
409
+
410
+ // Get old preference before update
411
+ const oldPreference = doc.preference?.notification;
412
+
413
+ await doc.update({ preference: value });
414
+
415
+ // Handle notification queue updates if notification preference changed
416
+ if (
417
+ oldPreference?.frequency !== value.notification?.frequency ||
418
+ oldPreference?.schedule?.time !== value.notification?.schedule?.time ||
419
+ oldPreference?.schedule?.date !== value.notification?.schedule?.date
420
+ ) {
421
+ await handleNotificationPreferenceChange(doc.id, value.notification);
422
+ }
423
+
424
+ return res.json(doc);
425
+ } catch (err) {
426
+ logger.error('Failed to update customer preference', err);
427
+ return res.status(400).json({ error: `Failed to update preference: ${err.message}` });
428
+ }
429
+ });
430
+
370
431
  const updateCustomerSchema = Joi.object({
371
432
  metadata: MetadataSchema,
372
433
  name: Joi.string().min(2).max(30).empty(''),
@@ -1906,7 +1906,7 @@ router.get('/:id/overdue/invoices', authPortal, async (req, res) => {
1906
1906
  return res.status(404).json({ error: 'Subscription not found' });
1907
1907
  }
1908
1908
  // @ts-ignore
1909
- if (subscription.customer?.did !== req.user?.did) {
1909
+ if (subscription.customer?.did !== req.user?.did && !['admin', 'owner'].includes(req.user?.role)) {
1910
1910
  return res.status(403).json({ error: 'You are not allowed to access this subscription' });
1911
1911
  }
1912
1912
  const { rows: invoices, count } = await Invoice.findAndCountAll({
@@ -0,0 +1,29 @@
1
+ import { DataTypes } from 'sequelize';
2
+
3
+ import { Migration, safeApplyColumnChanges } from '../migrate';
4
+
5
+ export const up: Migration = async ({ context }) => {
6
+ await safeApplyColumnChanges(context, {
7
+ customers: [
8
+ {
9
+ name: 'preference',
10
+ field: {
11
+ type: DataTypes.JSON,
12
+ defaultValue: JSON.stringify({
13
+ notification: {
14
+ frequency: 'monthly',
15
+ schedule: {
16
+ time: '10:00',
17
+ date: 1,
18
+ },
19
+ },
20
+ }),
21
+ },
22
+ },
23
+ ],
24
+ });
25
+ };
26
+
27
+ export const down: Migration = async ({ context }) => {
28
+ await context.removeColumn('customers', 'preference');
29
+ };
@@ -17,7 +17,7 @@ import { createEvent } from '../../libs/audit';
17
17
  import CustomError from '../../libs/error';
18
18
  import { getLock } from '../../libs/lock';
19
19
  import { createCodeGenerator, createIdGenerator } from '../../libs/util';
20
- import type { CustomerAddress, CustomerShipping } from './types';
20
+ import type { CustomerAddress, CustomerPreferences, CustomerShipping } from './types';
21
21
 
22
22
  export const nextCustomerId = createIdGenerator('cus', 14);
23
23
  export const nextInvoicePrefix = createCodeGenerator('', 8);
@@ -65,6 +65,7 @@ export class Customer extends Model<InferAttributes<Customer>, InferCreationAttr
65
65
  declare created_at: CreationOptional<Date>;
66
66
  declare updated_at: CreationOptional<Date>;
67
67
  declare last_sync_at?: number;
68
+ declare preference?: CustomerPreferences;
68
69
 
69
70
  public static readonly GENESIS_ATTRIBUTES = {
70
71
  id: {
@@ -234,6 +235,19 @@ export class Customer extends Model<InferAttributes<Customer>, InferCreationAttr
234
235
  type: DataTypes.INTEGER,
235
236
  allowNull: true,
236
237
  },
238
+ preference: {
239
+ type: DataTypes.JSON,
240
+ allowNull: true,
241
+ defaultValue: {
242
+ notification: {
243
+ frequency: 'monthly',
244
+ schedule: {
245
+ time: '10:00',
246
+ date: 1,
247
+ },
248
+ },
249
+ },
250
+ },
237
251
  },
238
252
  {
239
253
  sequelize,
@@ -442,7 +442,7 @@ export type PricingTableItem = {
442
442
  // Enables user redeemable promotion codes.
443
443
  allow_promotion_codes: boolean;
444
444
 
445
- // Configuration for collecting the customers billing address.
445
+ // Configuration for collecting the customer's billing address.
446
446
  billing_address_collection?: LiteralUnion<'auto' | 'required', string>;
447
447
 
448
448
  is_highlight: boolean;
@@ -704,3 +704,19 @@ export type EventType = LiteralUnion<
704
704
  export type StripeRefundReason = 'duplicate' | 'fraudulent' | 'requested_by_customer';
705
705
 
706
706
  export type SettingType = LiteralUnion<'donate', string>;
707
+
708
+ export type NotificationFrequency = 'default' | 'daily' | 'weekly' | 'monthly';
709
+ export type NotificationSchedule = {
710
+ time: string; // HH:mm 格式
711
+ date?: number; // weekly: 0-6, monthly: 1-31
712
+ };
713
+
714
+ export type NotificationSettings = {
715
+ frequency: NotificationFrequency;
716
+ schedule?: NotificationSchedule;
717
+ };
718
+
719
+ export type CustomerPreferences = {
720
+ notification?: NotificationSettings;
721
+ // support more preferences
722
+ };
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.18.25
17
+ version: 1.18.27
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.18.25",
3
+ "version": "1.18.27",
4
4
  "scripts": {
5
5
  "dev": "blocklet dev --open",
6
6
  "eject": "vite eject",
@@ -44,29 +44,29 @@
44
44
  },
45
45
  "dependencies": {
46
46
  "@abtnode/cron": "^1.16.41",
47
- "@arcblock/did": "^1.19.15",
47
+ "@arcblock/did": "^1.19.19",
48
48
  "@arcblock/did-auth-storage-nedb": "^1.7.1",
49
- "@arcblock/did-connect": "^2.12.52",
50
- "@arcblock/did-util": "^1.19.15",
51
- "@arcblock/jwt": "^1.19.15",
52
- "@arcblock/ux": "^2.12.52",
53
- "@arcblock/validator": "^1.19.15",
49
+ "@arcblock/did-connect": "^2.12.60",
50
+ "@arcblock/did-util": "^1.19.19",
51
+ "@arcblock/jwt": "^1.19.19",
52
+ "@arcblock/ux": "^2.12.60",
53
+ "@arcblock/validator": "^1.19.19",
54
54
  "@blocklet/js-sdk": "^1.16.41",
55
55
  "@blocklet/logger": "^1.16.41",
56
- "@blocklet/payment-react": "1.18.25",
56
+ "@blocklet/payment-react": "1.18.27",
57
57
  "@blocklet/sdk": "^1.16.41",
58
- "@blocklet/ui-react": "^2.12.52",
59
- "@blocklet/uploader": "^0.1.81",
60
- "@blocklet/xss": "^0.1.30",
58
+ "@blocklet/ui-react": "^2.12.60",
59
+ "@blocklet/uploader": "^0.1.82",
60
+ "@blocklet/xss": "^0.1.31",
61
61
  "@mui/icons-material": "^5.16.6",
62
62
  "@mui/lab": "^5.0.0-alpha.173",
63
63
  "@mui/material": "^5.16.6",
64
64
  "@mui/system": "^5.16.6",
65
- "@ocap/asset": "^1.19.15",
66
- "@ocap/client": "^1.19.15",
67
- "@ocap/mcrypto": "^1.19.15",
68
- "@ocap/util": "^1.19.15",
69
- "@ocap/wallet": "^1.19.15",
65
+ "@ocap/asset": "^1.19.19",
66
+ "@ocap/client": "^1.19.19",
67
+ "@ocap/mcrypto": "^1.19.19",
68
+ "@ocap/util": "^1.19.19",
69
+ "@ocap/wallet": "^1.19.19",
70
70
  "@stripe/react-stripe-js": "^2.7.3",
71
71
  "@stripe/stripe-js": "^2.4.0",
72
72
  "ahooks": "^3.8.0",
@@ -121,7 +121,7 @@
121
121
  "devDependencies": {
122
122
  "@abtnode/types": "^1.16.41",
123
123
  "@arcblock/eslint-config-ts": "^0.3.3",
124
- "@blocklet/payment-types": "1.18.25",
124
+ "@blocklet/payment-types": "1.18.27",
125
125
  "@types/cookie-parser": "^1.4.7",
126
126
  "@types/cors": "^2.8.17",
127
127
  "@types/debug": "^4.1.12",
@@ -151,7 +151,7 @@
151
151
  "vite": "^5.3.5",
152
152
  "vite-node": "^2.0.4",
153
153
  "vite-plugin-babel-import": "^2.0.5",
154
- "vite-plugin-blocklet": "^0.9.29",
154
+ "vite-plugin-blocklet": "^0.9.31",
155
155
  "vite-plugin-node-polyfills": "^0.21.0",
156
156
  "vite-plugin-svgr": "^4.2.0",
157
157
  "vite-tsconfig-paths": "^4.3.2",
@@ -167,5 +167,5 @@
167
167
  "parser": "typescript"
168
168
  }
169
169
  },
170
- "gitHead": "1665ec667d621a3e607650e486cb0967371b9771"
170
+ "gitHead": "2bbcff1991daf5de147813c598e13d755da0c989"
171
171
  }