payment-kit 1.19.0 → 1.19.2

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 (139) hide show
  1. package/api/src/crons/index.ts +8 -0
  2. package/api/src/index.ts +4 -0
  3. package/api/src/libs/credit-grant.ts +146 -0
  4. package/api/src/libs/env.ts +1 -0
  5. package/api/src/libs/invoice.ts +4 -3
  6. package/api/src/libs/notification/template/base.ts +388 -2
  7. package/api/src/libs/notification/template/customer-credit-grant-granted.ts +149 -0
  8. package/api/src/libs/notification/template/customer-credit-grant-low-balance.ts +151 -0
  9. package/api/src/libs/notification/template/customer-credit-insufficient.ts +254 -0
  10. package/api/src/libs/notification/template/subscription-canceled.ts +193 -202
  11. package/api/src/libs/notification/template/subscription-refund-succeeded.ts +215 -237
  12. package/api/src/libs/notification/template/subscription-renewed.ts +130 -200
  13. package/api/src/libs/notification/template/subscription-succeeded.ts +100 -202
  14. package/api/src/libs/notification/template/subscription-trial-start.ts +142 -188
  15. package/api/src/libs/notification/template/subscription-trial-will-end.ts +146 -174
  16. package/api/src/libs/notification/template/subscription-upgraded.ts +96 -192
  17. package/api/src/libs/notification/template/subscription-will-canceled.ts +94 -135
  18. package/api/src/libs/notification/template/subscription-will-renew.ts +220 -245
  19. package/api/src/libs/payment.ts +69 -0
  20. package/api/src/libs/queue/index.ts +3 -2
  21. package/api/src/libs/session.ts +8 -0
  22. package/api/src/libs/subscription.ts +74 -3
  23. package/api/src/libs/util.ts +3 -1
  24. package/api/src/libs/ws.ts +23 -1
  25. package/api/src/locales/en.ts +33 -0
  26. package/api/src/locales/zh.ts +31 -0
  27. package/api/src/queues/credit-consume.ts +728 -0
  28. package/api/src/queues/credit-grant.ts +572 -0
  29. package/api/src/queues/notification.ts +173 -128
  30. package/api/src/queues/payment.ts +210 -122
  31. package/api/src/queues/subscription.ts +179 -0
  32. package/api/src/routes/checkout-sessions.ts +157 -9
  33. package/api/src/routes/connect/shared.ts +3 -2
  34. package/api/src/routes/credit-grants.ts +241 -0
  35. package/api/src/routes/credit-transactions.ts +208 -0
  36. package/api/src/routes/customers.ts +34 -5
  37. package/api/src/routes/index.ts +8 -0
  38. package/api/src/routes/meter-events.ts +347 -0
  39. package/api/src/routes/meters.ts +219 -0
  40. package/api/src/routes/payment-currencies.ts +20 -2
  41. package/api/src/routes/payment-links.ts +1 -1
  42. package/api/src/routes/payment-methods.ts +14 -2
  43. package/api/src/routes/prices.ts +43 -0
  44. package/api/src/routes/pricing-table.ts +13 -7
  45. package/api/src/routes/products.ts +63 -4
  46. package/api/src/routes/settings.ts +1 -1
  47. package/api/src/routes/subscriptions.ts +4 -0
  48. package/api/src/routes/webhook-endpoints.ts +0 -3
  49. package/api/src/store/migrations/20250610-billing-credit.ts +43 -0
  50. package/api/src/store/models/credit-grant.ts +486 -0
  51. package/api/src/store/models/credit-transaction.ts +268 -0
  52. package/api/src/store/models/customer.ts +8 -0
  53. package/api/src/store/models/index.ts +52 -1
  54. package/api/src/store/models/meter-event.ts +423 -0
  55. package/api/src/store/models/meter.ts +176 -0
  56. package/api/src/store/models/payment-currency.ts +66 -14
  57. package/api/src/store/models/price.ts +6 -0
  58. package/api/src/store/models/product.ts +2 -2
  59. package/api/src/store/models/subscription.ts +24 -0
  60. package/api/src/store/models/types.ts +28 -2
  61. package/api/tests/libs/subscription.spec.ts +53 -0
  62. package/blocklet.yml +9 -1
  63. package/package.json +4 -4
  64. package/scripts/sdk.js +233 -1
  65. package/src/app.tsx +10 -0
  66. package/src/components/collapse.tsx +11 -1
  67. package/src/components/conditional-section.tsx +87 -0
  68. package/src/components/customer/credit-grant-item-list.tsx +99 -0
  69. package/src/components/customer/credit-overview.tsx +246 -0
  70. package/src/components/customer/form.tsx +7 -3
  71. package/src/components/invoice/list.tsx +19 -1
  72. package/src/components/metadata/form.tsx +287 -91
  73. package/src/components/meter/actions.tsx +101 -0
  74. package/src/components/meter/add-usage-dialog.tsx +239 -0
  75. package/src/components/meter/events-list.tsx +657 -0
  76. package/src/components/meter/form.tsx +245 -0
  77. package/src/components/meter/products.tsx +264 -0
  78. package/src/components/meter/usage-guide.tsx +174 -0
  79. package/src/components/payment-currency/form.tsx +2 -0
  80. package/src/components/payment-intent/list.tsx +19 -1
  81. package/src/components/payment-link/item.tsx +2 -2
  82. package/src/components/payment-link/preview.tsx +1 -1
  83. package/src/components/payment-link/product-select.tsx +52 -12
  84. package/src/components/payment-method/arcblock.tsx +2 -0
  85. package/src/components/payment-method/base.tsx +2 -0
  86. package/src/components/payment-method/bitcoin.tsx +2 -0
  87. package/src/components/payment-method/ethereum.tsx +2 -0
  88. package/src/components/payment-method/stripe.tsx +2 -0
  89. package/src/components/payouts/list.tsx +19 -1
  90. package/src/components/payouts/portal/list.tsx +6 -11
  91. package/src/components/price/currency-select.tsx +56 -32
  92. package/src/components/price/form.tsx +912 -407
  93. package/src/components/pricing-table/preview.tsx +1 -1
  94. package/src/components/product/add-price.tsx +9 -7
  95. package/src/components/product/create.tsx +7 -4
  96. package/src/components/product/edit-price.tsx +21 -12
  97. package/src/components/product/features.tsx +17 -7
  98. package/src/components/product/form.tsx +100 -90
  99. package/src/components/refund/list.tsx +19 -1
  100. package/src/components/section/header.tsx +5 -18
  101. package/src/components/subscription/items/index.tsx +1 -1
  102. package/src/components/subscription/metrics.tsx +37 -5
  103. package/src/components/subscription/portal/actions.tsx +2 -1
  104. package/src/contexts/products.tsx +26 -9
  105. package/src/hooks/subscription.ts +34 -0
  106. package/src/libs/meter-utils.ts +196 -0
  107. package/src/libs/util.ts +4 -0
  108. package/src/locales/en.tsx +389 -5
  109. package/src/locales/zh.tsx +368 -1
  110. package/src/pages/admin/billing/index.tsx +61 -33
  111. package/src/pages/admin/billing/invoices/detail.tsx +1 -1
  112. package/src/pages/admin/billing/meters/create.tsx +60 -0
  113. package/src/pages/admin/billing/meters/detail.tsx +435 -0
  114. package/src/pages/admin/billing/meters/index.tsx +210 -0
  115. package/src/pages/admin/billing/meters/meter-event.tsx +346 -0
  116. package/src/pages/admin/billing/subscriptions/detail.tsx +47 -14
  117. package/src/pages/admin/customers/customers/credit-grant/detail.tsx +391 -0
  118. package/src/pages/admin/customers/customers/detail.tsx +14 -10
  119. package/src/pages/admin/customers/index.tsx +5 -0
  120. package/src/pages/admin/developers/events/detail.tsx +1 -1
  121. package/src/pages/admin/developers/index.tsx +1 -1
  122. package/src/pages/admin/payments/intents/detail.tsx +1 -1
  123. package/src/pages/admin/payments/payouts/detail.tsx +1 -1
  124. package/src/pages/admin/payments/refunds/detail.tsx +1 -1
  125. package/src/pages/admin/products/index.tsx +3 -2
  126. package/src/pages/admin/products/links/detail.tsx +1 -1
  127. package/src/pages/admin/products/prices/actions.tsx +16 -4
  128. package/src/pages/admin/products/prices/detail.tsx +30 -3
  129. package/src/pages/admin/products/prices/list.tsx +8 -1
  130. package/src/pages/admin/products/pricing-tables/detail.tsx +1 -1
  131. package/src/pages/admin/products/products/create.tsx +233 -57
  132. package/src/pages/admin/products/products/detail.tsx +2 -1
  133. package/src/pages/admin/settings/payment-methods/index.tsx +3 -0
  134. package/src/pages/customer/credit-grant/detail.tsx +308 -0
  135. package/src/pages/customer/index.tsx +44 -9
  136. package/src/pages/customer/recharge/account.tsx +5 -5
  137. package/src/pages/customer/subscription/change-payment.tsx +4 -2
  138. package/src/pages/customer/subscription/detail.tsx +48 -14
  139. package/src/pages/customer/subscription/embed.tsx +1 -1
@@ -19,6 +19,8 @@ import {
19
19
  TCustomer,
20
20
  TLineItemExpanded,
21
21
  Payout,
22
+ CreditGrant,
23
+ Customer,
22
24
  } from '../store/models';
23
25
  import type { TPaymentCurrency } from '../store/models/payment-currency';
24
26
  import { blocklet, ethWallet, wallet, getVaultAddress } from './auth';
@@ -26,6 +28,7 @@ import logger from './logger';
26
28
  import { getBlockletJson, getUserOrAppInfo, OCAP_PAYMENT_TX_TYPE, resolveAddressChainTypes } from './util';
27
29
  import { CHARGE_SUPPORTED_CHAIN_TYPES, EVM_CHAIN_TYPES } from './constants';
28
30
  import { getTokenByAddress } from '../integrations/arcblock/stake';
31
+ import { isCreditMetered } from './session';
29
32
 
30
33
  export interface SufficientForPaymentResult {
31
34
  sufficient: boolean;
@@ -38,6 +41,10 @@ export interface SufficientForPaymentResult {
38
41
  | 'NO_TOKEN'
39
42
  | 'NO_ENOUGH_ALLOWANCE'
40
43
  | 'NO_ENOUGH_TOKEN'
44
+ | 'NO_CUSTOMER'
45
+ | 'NO_CREDIT_GRANTS'
46
+ | 'NO_ENOUGH_CREDIT'
47
+ | 'CREDIT_CHECK_ERROR'
41
48
  | 'NOT_SUPPORTED',
42
49
  string
43
50
  >;
@@ -139,12 +146,61 @@ export async function checkTokenBalance(args: {
139
146
  };
140
147
  }
141
148
 
149
+ export async function isCreditGrantSufficientForPayment(args: {
150
+ paymentMethod: PaymentMethod;
151
+ paymentCurrency: TPaymentCurrency;
152
+ userDid: string;
153
+ amount: string;
154
+ lineItems?: TLineItemExpanded[];
155
+ }): Promise<SufficientForPaymentResult> {
156
+ const { paymentMethod, paymentCurrency, userDid, amount, lineItems } = args;
157
+ if (paymentMethod.type === 'arcblock' && paymentCurrency.type === 'credit') {
158
+ const customer = await Customer.findOne({ where: { did: userDid } });
159
+ if (!customer) {
160
+ return { sufficient: false, reason: 'NO_CUSTOMER' };
161
+ }
162
+
163
+ const priceIds: string[] = [];
164
+ if (lineItems && lineItems.length > 0) {
165
+ lineItems.forEach((item) => {
166
+ if (isCreditMetered(item.price)) {
167
+ priceIds.push(item.price_id);
168
+ }
169
+ });
170
+ }
171
+
172
+ const creditSummary = await CreditGrant.getEffectiveCreditSummary({
173
+ customerId: customer.id,
174
+ currencyId: paymentCurrency.id,
175
+ priceIds,
176
+ });
177
+ if (!creditSummary) {
178
+ return { sufficient: false, reason: 'NO_CREDIT_GRANTS' };
179
+ }
180
+ const availableCredit = new BN(creditSummary[paymentCurrency.id]?.remainingAmount || '0');
181
+ if (availableCredit.lt(new BN(amount))) {
182
+ return { sufficient: false, reason: 'NO_ENOUGH_CREDIT' };
183
+ }
184
+
185
+ return {
186
+ sufficient: true,
187
+ token: { address: paymentCurrency.id, balance: availableCredit.toString() },
188
+ requestedAmount: amount,
189
+ };
190
+ }
191
+ return {
192
+ sufficient: false,
193
+ reason: 'NOT_SUPPORTED',
194
+ };
195
+ }
196
+
142
197
  export async function isDelegationSufficientForPayment(args: {
143
198
  paymentMethod: PaymentMethod;
144
199
  paymentCurrency: TPaymentCurrency;
145
200
  userDid: string;
146
201
  amount: string;
147
202
  delegatorAmounts?: string[];
203
+ lineItems?: TLineItemExpanded[];
148
204
  }): Promise<SufficientForPaymentResult> {
149
205
  const { paymentCurrency, paymentMethod, userDid, amount, delegatorAmounts } = args;
150
206
  const tokenAddress = paymentCurrency.contract as string;
@@ -156,6 +212,19 @@ export async function isDelegationSufficientForPayment(args: {
156
212
  }
157
213
 
158
214
  if (paymentMethod.type === 'arcblock') {
215
+ // Check if this is a credit currency type
216
+ if (paymentCurrency.type === 'credit') {
217
+ const result = await isCreditGrantSufficientForPayment({
218
+ paymentMethod,
219
+ paymentCurrency,
220
+ userDid,
221
+ amount,
222
+ lineItems: args.lineItems,
223
+ });
224
+ return result;
225
+ }
226
+
227
+ // Regular token handling for non-credit currencies
159
228
  // user have bond wallet did?
160
229
  const { user } = await blocklet.getUser(userDid, { enableConnectedAccount: true });
161
230
  const delegator = getWalletDid(user);
@@ -8,6 +8,7 @@ import { nanoid } from 'nanoid';
8
8
  import { AsyncLocalStorage } from 'async_hooks';
9
9
  import logger from '../logger';
10
10
  import { sleep, tryWithTimeout } from '../util';
11
+ import dayjs from '../dayjs';
11
12
  import createQueueStore from './store';
12
13
  import { Job } from '../../store/models/job';
13
14
  import { sequelize } from '../../store/sequelize';
@@ -112,6 +113,7 @@ export default function createQueue<T = any>({ name, onJob, options = defaults }
112
113
  queueEvents.emit(e, data);
113
114
  jobEvents.emit(e, data);
114
115
  };
116
+ const now = dayjs().unix();
115
117
 
116
118
  if (!job) {
117
119
  throw new Error('Can not queue empty job');
@@ -119,7 +121,7 @@ export default function createQueue<T = any>({ name, onJob, options = defaults }
119
121
 
120
122
  const jobId = getJobId(id, job);
121
123
 
122
- if (delay || runAt) {
124
+ if ((delay && delay >= MIN_DELAY) || (runAt && runAt > now)) {
123
125
  if (!enableScheduledJob) {
124
126
  throw new Error('Must set options.enableScheduledJob to true to run delay jobs');
125
127
  }
@@ -135,7 +137,6 @@ export default function createQueue<T = any>({ name, onJob, options = defaults }
135
137
  attrs.will_run_at = Date.now() + delay * 1000;
136
138
  }
137
139
  if (runAt) {
138
- // 如果 runAt 大于当前时间,则延迟执行,否则直接执行
139
140
  attrs.delay = 1;
140
141
  attrs.will_run_at = runAt * 1000;
141
142
  }
@@ -975,3 +975,11 @@ export async function getSubscriptionLineItems(
975
975
 
976
976
  return subItems;
977
977
  }
978
+
979
+ export function isCreditMetered(price: TPrice | TPriceExpanded) {
980
+ return price.type === 'recurring' && price.recurring?.usage_type === 'metered' && !!price.recurring?.meter_id;
981
+ }
982
+
983
+ export function isCreditMeteredLineItems(lineItems: TLineItemExpanded[]) {
984
+ return lineItems.every((item) => item.price && isCreditMetered(item.price));
985
+ }
@@ -189,15 +189,76 @@ export function getSubscriptionStakeSetup(items: TLineItemExpanded[], currencyId
189
189
  return staking;
190
190
  }
191
191
 
192
- export function getSubscriptionCycleSetup(recurring: PriceRecurring, previousPeriodEnd: number) {
192
+ export function getSubscriptionCycleSetup(
193
+ recurring: PriceRecurring,
194
+ previousPeriodEnd: number,
195
+ options?: {
196
+ catchUp?: boolean;
197
+ maxMissedPeriods?: number;
198
+ }
199
+ ) {
193
200
  const cycle = getRecurringPeriod(recurring);
201
+ const now = dayjs().unix();
202
+
203
+ // Basic setup without catch-up
204
+ if (!options?.catchUp) {
205
+ return {
206
+ recurring,
207
+ cycle,
208
+ period: {
209
+ start: previousPeriodEnd,
210
+ end: dayjs.unix(previousPeriodEnd).add(cycle, 'millisecond').unix(),
211
+ },
212
+ missedPeriods: 0,
213
+ };
214
+ }
215
+
216
+ // Calculate missed periods if catch-up is enabled
217
+ const expectedEnd = dayjs.unix(previousPeriodEnd).add(cycle, 'millisecond').unix();
218
+
219
+ if (now <= expectedEnd) {
220
+ // No catch-up needed
221
+ return {
222
+ recurring,
223
+ cycle,
224
+ period: {
225
+ start: previousPeriodEnd,
226
+ end: expectedEnd,
227
+ },
228
+ missedPeriods: 0,
229
+ };
230
+ }
231
+
232
+ // Calculate how many periods we've missed
233
+ const timeSinceExpected = now - expectedEnd;
234
+ const cycleInSeconds = cycle / 1000;
235
+ const missedPeriods = Math.floor(timeSinceExpected / cycleInSeconds);
236
+ const maxMissedPeriods = options.maxMissedPeriods || 100; // Default limit to prevent excessive catch-up
237
+
238
+ const actualMissedPeriods = Math.min(missedPeriods, maxMissedPeriods);
239
+
240
+ // Calculate the current period we should be in
241
+ const currentPeriodStart = dayjs
242
+ .unix(previousPeriodEnd)
243
+ .add((actualMissedPeriods + 1) * cycle, 'millisecond')
244
+ .unix();
245
+ const currentPeriodEnd = dayjs.unix(currentPeriodStart).add(cycle, 'millisecond').unix();
194
246
 
195
247
  return {
196
248
  recurring,
197
249
  cycle,
198
250
  period: {
199
- start: previousPeriodEnd,
200
- end: dayjs.unix(previousPeriodEnd).add(cycle, 'millisecond').unix(),
251
+ start: currentPeriodStart,
252
+ end: currentPeriodEnd,
253
+ },
254
+ missedPeriods: actualMissedPeriods,
255
+ // Additional info for recovery handling
256
+ recovery: {
257
+ originalPeriodEnd: previousPeriodEnd,
258
+ expectedPeriodEnd: expectedEnd,
259
+ timeMissed: timeSinceExpected,
260
+ periodsSkipped: actualMissedPeriods,
261
+ wasLimited: missedPeriods > maxMissedPeriods,
201
262
  },
202
263
  };
203
264
  }
@@ -1627,3 +1688,13 @@ export function calculateRecommendedRechargeAmount(subscriptions: any[], currenc
1627
1688
  interval: recommendedInterval,
1628
1689
  };
1629
1690
  }
1691
+
1692
+ export async function getMeterPriceIdsFromSubscription(subscription: Subscription): Promise<string[]> {
1693
+ const subscriptionItems = (await SubscriptionItem.findAll({
1694
+ where: { subscription_id: subscription.id },
1695
+ include: [{ model: Price, as: 'price' }],
1696
+ })) as (SubscriptionItem & { price: Price })[];
1697
+
1698
+ const meterItems = subscriptionItems.filter((x) => x.price.recurring?.meter_id);
1699
+ return meterItems.map((m) => m.price.id);
1700
+ }
@@ -12,6 +12,7 @@ import { joinURL, withQuery, withTrailingSlash } from 'ufo';
12
12
  import axios from 'axios';
13
13
  import { ethers } from 'ethers';
14
14
  import { fromUnitToToken } from '@ocap/util';
15
+ import get from 'lodash/get';
15
16
  import dayjs from './dayjs';
16
17
  import { blocklet, wallet } from './auth';
17
18
  import type { PaymentCurrency, PaymentMethod, Subscription } from '../store/models';
@@ -268,11 +269,12 @@ export async function getUserOrAppInfo(
268
269
  }
269
270
  const { user } = await blocklet.getUser(address);
270
271
  if (user) {
272
+ const locale = get(user, 'locale', 'en');
271
273
  return {
272
274
  name: user?.fullName,
273
275
  avatar: joinURL(process.env.BLOCKLET_APP_URL!, user?.avatar),
274
276
  type: 'user',
275
- url: getCustomerProfileUrl({ userDid: address, locale: 'en' }),
277
+ url: getCustomerProfileUrl({ userDid: address, locale }),
276
278
  };
277
279
  }
278
280
  return {
@@ -1,6 +1,14 @@
1
1
  import { sendToRelay } from '@blocklet/sdk/service/notification';
2
2
  import { publish } from '@blocklet/sdk/service/eventbus';
3
- import type { CheckoutSession, Invoice, PaymentIntent, Subscription, Refund } from '../store/models';
3
+ import type {
4
+ CheckoutSession,
5
+ Invoice,
6
+ PaymentIntent,
7
+ Subscription,
8
+ Refund,
9
+ CreditGrant,
10
+ Customer,
11
+ } from '../store/models';
4
12
  import { events } from './event';
5
13
 
6
14
  export function broadcast(eventName: string, data: any, extraParams?: Record<string, any>) {
@@ -93,4 +101,18 @@ export function initEventBroadcast() {
93
101
  events.on('manual.notification', (data: Record<string, any>) => {
94
102
  broadcast('manual.notification', data);
95
103
  });
104
+
105
+ // credit
106
+ events.on('customer.credit_grant.granted', (data: CreditGrant, extraParams?: Record<string, any>) => {
107
+ broadcast('customer.credit_grant.granted', data, extraParams);
108
+ });
109
+ events.on('customer.credit_grant.low_balance', (data: CreditGrant, extraParams?: Record<string, any>) => {
110
+ broadcast('customer.credit_grant.low_balance', data, extraParams);
111
+ });
112
+ events.on('customer.credit_grant.depleted', (data: CreditGrant, extraParams?: Record<string, any>) => {
113
+ broadcast('customer.credit_grant.depleted', data, extraParams);
114
+ });
115
+ events.on('customer.credit.insufficient', (data: Customer, extraParams?: Record<string, any>) => {
116
+ broadcast('customer.credit.insufficient', data, extraParams);
117
+ });
96
118
  }
@@ -48,6 +48,7 @@ export default flat({
48
48
  subscriptionId: 'Subscription ID',
49
49
  shouldPayAmount: 'Should pay amount',
50
50
  billedAmount: 'Billed amount',
51
+ viewCreditGrant: 'View Credit Balance',
51
52
  },
52
53
 
53
54
  billingDiscrepancy: {
@@ -217,5 +218,37 @@ export default flat({
217
218
  title: '{count} Subscriptions Renewed',
218
219
  body: 'During {startTime} - {endTime}, {count} subscriptions were successfully renewed, total amount: {totalAmount}.\n\nSubscription List:\n{subscriptionList}',
219
220
  },
221
+
222
+ creditInsufficient: {
223
+ title: 'Insufficient Credit',
224
+ bodyWithSubscription:
225
+ 'Your available credit is only {availableAmount}, which is not enough to cover your subscription to {subscriptionName}. Please top up to avoid service interruption.',
226
+ bodyWithoutSubscription:
227
+ 'Your available credit is only {availableAmount}, which is not enough to continue using the service. Please top up to avoid restrictions.',
228
+ exhaustedTitle: 'Credit Exhausted – Please Top Up',
229
+ exhaustedBodyWithSubscription:
230
+ 'Your credit is fully exhausted and can no longer cover your subscription to {subscriptionName}. Please top up to avoid service interruption.',
231
+ exhaustedBodyWithoutSubscription:
232
+ 'Your credit is fully exhausted (remaining balance: 0). Please top up to ensure uninterrupted service.',
233
+ meterEventName: 'Service',
234
+ availableCredit: 'Available Credit',
235
+ requiredCredit: 'Required Credit',
236
+ topUpNow: 'Top Up Now',
237
+ },
238
+
239
+ creditGrantGranted: {
240
+ title: 'Congratulations! Your granted credit is now active',
241
+ body: 'You have been granted {grantedAmount} credit, activated on {at}, valid until {expiresAt}. Enjoy your service!',
242
+ bodyNoExpire: 'You have been granted {grantedAmount} credit, activated on {at}. Enjoy your service!',
243
+ grantedCredit: 'Granted Credit',
244
+ validUntil: 'Valid Until',
245
+ neverExpires: 'Never Expires',
246
+ },
247
+
248
+ creditGrantLowBalance: {
249
+ title: 'Low Granted Credit Balance Warning',
250
+ body: 'Your granted credit balance is below 10%. Current available credit is {availableAmount}. Please top up or contact support to avoid service interruption.',
251
+ totalGrantedCredit: 'Total Granted Credit',
252
+ },
220
253
  },
221
254
  });
@@ -48,6 +48,7 @@ export default flat({
48
48
  subscriptionId: '订阅 ID',
49
49
  shouldPayAmount: '应收金额',
50
50
  billedAmount: '实缴金额',
51
+ viewCreditGrant: '查看额度',
51
52
  },
52
53
 
53
54
  sendTo: '发送给',
@@ -211,5 +212,35 @@ export default flat({
211
212
  title: '{count} 个订阅续费成功',
212
213
  body: '在 {startTime} - {endTime} 期间,共有 {count} 个订阅成功续费,总金额:{totalAmount}。\n\n订阅清单:\n{subscriptionList}',
213
214
  },
215
+
216
+ creditInsufficient: {
217
+ title: '额度不足,服务可能受限',
218
+ bodyWithSubscription:
219
+ '您的信用额度仅剩 {availableAmount},不足以支付您订阅的 {subscriptionName}。请及时充值以避免服务中断。',
220
+ bodyWithoutSubscription: '您的信用额度仅剩 {availableAmount},不足以继续使用服务。请及时充值以避免服务受限。',
221
+ exhaustedTitle: '额度已用尽,请尽快充值',
222
+ exhaustedBodyWithSubscription:
223
+ '您的信用额度已用尽,无法继续支付您订阅的 {subscriptionName}。为确保服务不中断,请及时充值。',
224
+ exhaustedBodyWithoutSubscription: '您的信用额度已用尽(剩余额度为 0)。为确保服务正常使用,请及时充值。',
225
+ meterEventName: '服务项目',
226
+ availableCredit: '剩余额度',
227
+ requiredCredit: '所需额度',
228
+ topUpNow: '立即充值',
229
+ },
230
+
231
+ creditGrantGranted: {
232
+ title: '恭喜!您的额度授予已激活',
233
+ body: '您已获得 {grantedAmount} 的额度授予,激活时间为 {at},有效期至 {expiresAt}。祝您使用愉快!',
234
+ bodyNoExpire: '您已获得 {grantedAmount} 的额度授予,激活时间为 {at}。祝您使用愉快!',
235
+ grantedCredit: '授予额度',
236
+ validUntil: '有效期至',
237
+ neverExpires: '永不过期',
238
+ },
239
+
240
+ creditGrantLowBalance: {
241
+ title: '额度授予余额不足提醒',
242
+ body: '您的额度授予余额已低于 10%,当前剩余额度为 {availableAmount}。请及时充值或联系管理员以避免服务受限。',
243
+ totalGrantedCredit: '总授予额度',
244
+ },
214
245
  },
215
246
  });