payment-kit 1.21.2 → 1.21.4

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.
@@ -409,4 +409,10 @@ export class DidnamesAdapter implements VendorAdapter {
409
409
 
410
410
  return data;
411
411
  }
412
+
413
+ connectTest(): Promise<any> {
414
+ const url = formatVendorUrl(this.vendorConfig!, '/api/vendor/health');
415
+ const { headers } = VendorAuth.signRequestWithHeaders({});
416
+ return fetch(url, { headers }).then((res) => res.json());
417
+ }
412
418
  }
@@ -267,4 +267,10 @@ export class LauncherAdapter implements VendorAdapter {
267
267
 
268
268
  return doRequestVendorData(vendor, orderId, url, options);
269
269
  }
270
+
271
+ connectTest(): Promise<any> {
272
+ const url = formatVendorUrl(this.vendorConfig!, '/api/vendor/health');
273
+ const { headers } = VendorAuth.signRequestWithHeaders({});
274
+ return fetch(url, { headers }).then((res) => res.json());
275
+ }
270
276
  }
@@ -89,4 +89,5 @@ export interface VendorAdapter {
89
89
  checkOrderStatus(params: CheckOrderStatusParams): Promise<CheckOrderStatusResult>;
90
90
  getOrder(vendor: ProductVendor, orderId: string, option?: Record<string, any>): Promise<any>;
91
91
  getOrderStatus(vendor: ProductVendor, orderId: string, option?: Record<string, any>): Promise<any>;
92
+ connectTest(): Promise<any>;
92
93
  }
@@ -2,6 +2,6 @@ import { joinURL } from 'ufo';
2
2
 
3
3
  import type { VendorConfig } from './types';
4
4
 
5
- export const formatVendorUrl = (vendorConfig: VendorConfig, p: string) => {
6
- return joinURL(vendorConfig.app_url, vendorConfig.metadata?.mountPoint || '', p);
5
+ export const formatVendorUrl = (vendorConfig: VendorConfig, ...args: string[]) => {
6
+ return joinURL(vendorConfig.app_url, vendorConfig.metadata?.mountPoint || '', ...args);
7
7
  };
@@ -1329,6 +1329,11 @@ router.get('/retrieve/:id', user, async (req, res) => {
1329
1329
  });
1330
1330
  });
1331
1331
 
1332
+ async function checkVendorConfig(items: TLineItemExpanded[]) {
1333
+ const lineItems = await Price.expand(items, { upsell: true });
1334
+ return lineItems?.some((item: TLineItemExpanded) => !!item?.price?.product?.vendor_config?.length);
1335
+ }
1336
+
1332
1337
  // submit order
1333
1338
  router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
1334
1339
  try {
@@ -1336,6 +1341,17 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
1336
1341
  return res.status(403).json({ code: 'REQUIRE_LOGIN', error: 'Please login to continue' });
1337
1342
  }
1338
1343
 
1344
+ const hasVendorConfig = await checkVendorConfig(req.doc.line_items);
1345
+ if (hasVendorConfig) {
1346
+ const { user: userDetail } = await blocklet.getUser(req.user.did, { enableConnectedAccount: true });
1347
+ if (!userDetail?.sourceAppPid) {
1348
+ return res.status(403).json({
1349
+ code: 'UNIFIED_APP_REQUIRED',
1350
+ error: 'This action requires a unified account. Please switch accounts and try again.',
1351
+ });
1352
+ }
1353
+ }
1354
+
1339
1355
  const checkoutSession = req.doc as CheckoutSession;
1340
1356
  if (checkoutSession.line_items) {
1341
1357
  try {
@@ -222,7 +222,7 @@ router.post('/', auth, async (req, res) => {
222
222
  return res.status(400).json({ error: `Event with identifier "${req.body.identifier}" already exists` });
223
223
  }
224
224
 
225
- const meter = await Meter.getMeterByEventName(req.body.event_name, !!req.livemode);
225
+ const meter = await Meter.getMeterByEventName(req.body.event_name);
226
226
  if (!meter) {
227
227
  return res
228
228
  .status(400)
@@ -361,7 +361,7 @@ router.get('/:id', authMine, async (req, res) => {
361
361
  if (customer.did !== req.user?.did && !['owner', 'admin'].includes(req.user?.role || '')) {
362
362
  return res.status(403).json({ error: 'You are not allowed to access this resource' });
363
363
  }
364
- const meter = await Meter.getMeterByEventName(event.event_name, event.livemode);
364
+ const meter = await Meter.getMeterByEventName(event.event_name);
365
365
  let paymentCurrency = null;
366
366
  if (meter) {
367
367
  paymentCurrency = await PaymentCurrency.findByPk(meter.currency_id);
@@ -68,7 +68,7 @@ router.post('/', auth, async (req, res) => {
68
68
  }
69
69
 
70
70
  const existing = await Meter.findOne({
71
- where: { event_name: req.body.event_name, livemode: !!req.livemode },
71
+ where: { event_name: req.body.event_name },
72
72
  });
73
73
  if (existing) {
74
74
  return res.status(409).json({ error: `Meter with event_name "${req.body.event_name}" already exists` });
@@ -1,11 +1,14 @@
1
+ import { middleware } from '@blocklet/payment-vendor';
1
2
  import { getUrl } from '@blocklet/sdk/lib/component';
2
3
  import { Router } from 'express';
3
4
  import Joi from 'joi';
4
- import { middleware } from '@blocklet/payment-vendor';
5
5
 
6
+ // eslint-disable-next-line import/no-extraneous-dependencies
7
+ import { gte } from 'semver';
6
8
  import { MetadataSchema } from '../libs/api';
7
9
  import { wallet } from '../libs/auth';
8
10
  import dayjs from '../libs/dayjs';
11
+ import env from '../libs/env';
9
12
  import logger from '../libs/logger';
10
13
  import { authenticate } from '../libs/security';
11
14
  import { formatToShortUrl } from '../libs/url';
@@ -298,18 +301,40 @@ async function testVendorConnection(req: any, res: any) {
298
301
  try {
299
302
  const vendor = await ProductVendor.findByPk(req.params.id);
300
303
  if (!vendor) {
301
- return res.status(404).json({ error: 'Vendor not found' });
304
+ return res.status(404).json({
305
+ code: 'VENDOR_NOT_FOUND',
306
+ error: 'Vendor not found',
307
+ });
302
308
  }
303
309
 
304
- return res.json({
305
- success: true,
306
- message: 'Connection test completed',
307
- vendor: {
308
- id: vendor.id,
309
- name: vendor.name,
310
- app_url: vendor.app_url,
311
- },
312
- });
310
+ const vendorAdapter = await VendorFulfillmentService.getVendorAdapter(vendor.vendor_key);
311
+ const result = await vendorAdapter.connectTest();
312
+
313
+ if (result.connected) {
314
+ if (env.appId !== result.did) {
315
+ return res.status(400).json({
316
+ code: 'VENDOR_CONNECT_TEST_FAILED',
317
+ error: 'Vendor connection test failed! Please check the DID in the vendor preferences config',
318
+ });
319
+ }
320
+ return res.status(200).json({ ...result });
321
+ }
322
+
323
+ if (result.error) {
324
+ return res.status(400).json({
325
+ code: 'VENDOR_CONNECT_TEST_FAILED',
326
+ error: result.error,
327
+ });
328
+ }
329
+
330
+ if (!result.sdkVersion) {
331
+ return res.status(400).json({
332
+ code: 'VENDOR_CONNECT_TEST_FAILED',
333
+ error: 'Vendor SDK version is too low. Please upgrade to the latest version (>=1.21.4)',
334
+ });
335
+ }
336
+
337
+ return res.status(400).json({ ...result });
313
338
  } catch (error: any) {
314
339
  logger.error('Failed to test vendor connection', { error, id: req.params.id });
315
340
  return res.status(500).json({ error: 'Internal server error' });
@@ -588,22 +613,35 @@ async function handleSubscriptionRedirect(req: any, res: any) {
588
613
  return res.redirect(getUrl(`/customer/subscription/${checkoutSession.subscription_id}`));
589
614
  }
590
615
 
616
+ function getVendorConnectTest(req: any, res: any) {
617
+ const sdkVersion = req.headers['x-broker-vendor-version'];
618
+ if (sdkVersion && gte(sdkVersion, '1.21.4')) {
619
+ return res.json({ connected: true, sdkVersion });
620
+ }
621
+ return res.json({
622
+ connected: false,
623
+ sdkVersion,
624
+ error: 'Vendor SDK version is too low, please upgrade to the latest version (>=1.21.4)',
625
+ });
626
+ }
627
+
591
628
  const router = Router();
592
629
 
593
630
  const ensureVendorAuth = middleware.ensureVendorAuth((vendorPk: string) =>
594
631
  ProductVendor.findOne({ where: { 'extends.appPk': vendorPk } }).then((v) => v as any)
595
632
  );
596
633
 
597
- // FIXME: Authentication not yet added, awaiting implementation @Pengfei
598
634
  router.get('/order/:sessionId/status', loginAuth, validateParams(sessionIdParamSchema), getVendorFulfillmentStatus);
599
635
  router.get('/order/:sessionId/detail', loginAuth, validateParams(sessionIdParamSchema), getVendorFulfillmentDetail);
600
636
 
637
+ // Those for Vendor Call
638
+ router.get('/connectTest', ensureVendorAuth, getVendorConnectTest);
601
639
  router.get('/subscription/:sessionId/redirect', handleSubscriptionRedirect);
602
640
  router.get('/subscription/:sessionId', ensureVendorAuth, getVendorSubscription);
603
641
 
604
642
  router.get(
605
643
  '/open/:subscriptionId',
606
- authAdmin,
644
+ loginAuth,
607
645
  validateParams(subscriptionIdParamSchema),
608
646
  validateQuery(vendorRedirectQuerySchema),
609
647
  redirectToVendor
@@ -374,7 +374,7 @@ export class MeterEvent extends Model<InferAttributes<MeterEvent>, InferCreation
374
374
 
375
375
  await Promise.all(
376
376
  events.map(async (event) => {
377
- const meter = await Meter.getMeterByEventName(event.event_name, event.livemode);
377
+ const meter = await Meter.getMeterByEventName(event.event_name);
378
378
  if (!meter) {
379
379
  return;
380
380
  }
@@ -146,11 +146,8 @@ export class Meter extends Model<InferAttributes<Meter>, InferCreationAttributes
146
146
  });
147
147
  }
148
148
 
149
- public static getMeterByEventName(eventName: string, livemode?: boolean): Promise<Meter | null> {
150
- const whereClause: any = { event_name: eventName, livemode: true };
151
- if (livemode !== undefined) {
152
- whereClause.livemode = livemode;
153
- }
149
+ public static getMeterByEventName(eventName: string): Promise<Meter | null> {
150
+ const whereClause: any = { event_name: eventName };
154
151
  return this.findOne({ where: whereClause });
155
152
  }
156
153
 
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.21.2
17
+ version: 1.21.4
18
18
  logo: logo.png
19
19
  files:
20
20
  - dist
@@ -84,11 +84,11 @@ capabilities:
84
84
  clusterMode: false
85
85
  component: true
86
86
  screenshots:
87
- - 3a4cab81c52c29662db8794b05ccc7c7.png
88
- - 77ac49b79ae920f0f253ce8c694ffd65.png
89
- - 1ef9e15ac36d4af5bef34941000ba3af.png
90
- - 7ea8ef758865ecf6edb712d3534d2974.png
91
- - 0ffe164ebe4aa2eb43f8d87f87683f7f.png
87
+ - checkout-payment-form.png
88
+ - customer-billing-dashboard.png
89
+ - subscription-details-view.png
90
+ - admin-payment-integrations.png
91
+ - payment-transactions-list.png
92
92
  components:
93
93
  - name: image-bin
94
94
  source:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "payment-kit",
3
- "version": "1.21.2",
3
+ "version": "1.21.4",
4
4
  "scripts": {
5
5
  "dev": "blocklet dev --open",
6
6
  "lint": "tsc --noEmit && eslint src api/src --ext .mjs,.js,.jsx,.ts,.tsx",
@@ -56,9 +56,9 @@
56
56
  "@blocklet/error": "^0.2.5",
57
57
  "@blocklet/js-sdk": "^1.16.52-beta-20250912-112002-e3499e9c",
58
58
  "@blocklet/logger": "^1.16.52-beta-20250912-112002-e3499e9c",
59
- "@blocklet/payment-broker-client": "1.21.2",
60
- "@blocklet/payment-react": "1.21.2",
61
- "@blocklet/payment-vendor": "1.21.2",
59
+ "@blocklet/payment-broker-client": "1.21.4",
60
+ "@blocklet/payment-react": "1.21.4",
61
+ "@blocklet/payment-vendor": "1.21.4",
62
62
  "@blocklet/sdk": "^1.16.52-beta-20250912-112002-e3499e9c",
63
63
  "@blocklet/ui-react": "^3.1.43",
64
64
  "@blocklet/uploader": "^0.2.12",
@@ -128,7 +128,7 @@
128
128
  "devDependencies": {
129
129
  "@abtnode/types": "^1.16.52-beta-20250912-112002-e3499e9c",
130
130
  "@arcblock/eslint-config-ts": "^0.3.3",
131
- "@blocklet/payment-types": "1.21.2",
131
+ "@blocklet/payment-types": "1.21.4",
132
132
  "@types/cookie-parser": "^1.4.9",
133
133
  "@types/cors": "^2.8.19",
134
134
  "@types/debug": "^4.1.12",
@@ -175,5 +175,5 @@
175
175
  "parser": "typescript"
176
176
  }
177
177
  },
178
- "gitHead": "abd74e83a89ea2fdc1261aef68355b1ce299fa4b"
178
+ "gitHead": "d650fbb4c391c1d58bc7ebbd9dedf11901db9f89"
179
179
  }
@@ -0,0 +1,33 @@
1
+ # Payment Kit Screenshots
2
+
3
+ This document provides a visual overview of the Payment Kit application, a decentralized Stripe-like payment solution for blocklets. Below are screenshots showcasing the key features and interfaces of the application.
4
+
5
+ ## Checkout Payment Form
6
+
7
+ ![Checkout Payment Form](./checkout-payment-form.png)
8
+
9
+ This screenshot displays the checkout payment interface where customers complete their purchases. The form shows an order summary for "DID Names Service" priced at 5 USD, alongside payment details where users can select their preferred payment method (PLAY3, TBA, ABT, or USD). The interface includes fields for customer information such as name, email, and postal code, with a prominent "Pay" button to complete the transaction.
10
+
11
+ ## Customer Billing Dashboard
12
+
13
+ ![Customer Billing Dashboard](./customer-billing-dashboard.png)
14
+
15
+ The customer billing dashboard provides users with a comprehensive overview of their account status and subscriptions. It displays wallet balances across multiple cryptocurrencies (TBA and PLAY3), showing balance, spent, and stake amounts. The credits section tracks token usage and transactions. Below that, users can view and manage their active subscriptions including "Usage Payment Service," "Test Service A," "Notification Service," and "Test Service B," each with renewal dates and management options.
16
+
17
+ ## Subscription Details View
18
+
19
+ ![Subscription Details View](./subscription-details-view.png)
20
+
21
+ This detailed subscription management screen shows comprehensive information about "Test Service A." It displays the subscription status (Active), next invoice date, amount, current balance, and subdcard information. The interface includes customer details, billing information, payment method (ArcBlock Beta), and state transaction data. The Products section shows the subscription item (0.01 TBA/month), and the Invoice History table provides a complete record of past payments with amounts, payment methods, invoice numbers, and statuses.
22
+
23
+ ## Admin Payment Integrations
24
+
25
+ ![Admin Payment Integrations](./admin-payment-integrations.png)
26
+
27
+ The admin integrations screen shows the configuration interface for payment methods and services. This view displays the vault settings page where administrators can configure app bundles and buffer thresholds for managing payment-related vault transfers. The right panel shows the available payment methods including ArcBlock (with ArcBlock Beta for testing), ETHng (with Ether payments via Alchemy Network), and BASE (with Base Mainnet and Sepolia options), each with toggle switches to enable or disable the payment method.
28
+
29
+ ## Payment Transactions List
30
+
31
+ ![Payment Transactions List](./payment-transactions-list.png)
32
+
33
+ The payment transactions overview displays a comprehensive table of all payment activities in the system. The list shows transaction amounts (ranging from 0.1 TBA to 1 TBA), payment methods (ArcBlock Beta), customer information (primarily showing "Xiao Fang"), descriptions (subscription cycles, creation, and purchase confirmations), creation and update timestamps, and transaction statuses (marked as "Succeeded"). This view provides administrators with complete visibility into all payment processing activities within the platform.
@@ -4,6 +4,7 @@ import { ConfirmDialog, api, formatError } from '@blocklet/payment-react';
4
4
  import { useSetState } from 'ahooks';
5
5
  import type { LiteralUnion } from 'type-fest';
6
6
 
7
+ import { useEffect, useRef } from 'react';
7
8
  import Actions from '../actions';
8
9
  import ClickBoundary from '../click-boundary';
9
10
 
@@ -62,6 +63,33 @@ export default function VendorActions({ data, variant = 'compact', onChange }: V
62
63
  }
63
64
  };
64
65
 
66
+ const onTestConnectionRef = useRef(async () => {
67
+ try {
68
+ setState({ loading: true });
69
+ const result = await api.post(`/api/vendors/${data.id}/test-connection`).then((res: any) => res.data);
70
+ if (result.error) {
71
+ Toast.error(result.error);
72
+ } else {
73
+ Toast.success(t('admin.vendor.testConnectionSuccess'));
74
+ }
75
+ } catch (err) {
76
+ console.error(err);
77
+ if (err.response?.data?.code === 'VENDOR_CONNECT_TEST_FAILED') {
78
+ Toast.error(err.response?.data?.error);
79
+ } else {
80
+ Toast.error(formatError(err));
81
+ }
82
+ } finally {
83
+ setState({ loading: false, action: '' });
84
+ }
85
+ });
86
+
87
+ useEffect(() => {
88
+ if (state.action === 'testConnection') {
89
+ onTestConnectionRef.current();
90
+ }
91
+ }, [state.action]);
92
+
65
93
  return (
66
94
  <ClickBoundary>
67
95
  <Actions
@@ -84,6 +112,11 @@ export default function VendorActions({ data, variant = 'compact', onChange }: V
84
112
  color: 'error',
85
113
  divider: true,
86
114
  },
115
+ {
116
+ label: t('admin.vendor.testConnection'),
117
+ handler: () => setState({ action: 'testConnection' }),
118
+ color: 'primary',
119
+ },
87
120
  ].filter(Boolean)}
88
121
  />
89
122
  {state.action === 'toggle_status' && (
@@ -463,7 +463,7 @@ export default flat({
463
463
  commissionType: 'Commission Type',
464
464
  commissionRate: 'Commission Rate',
465
465
  commissionRateRequired: 'Commission rate is required',
466
- commissionRateMin: 'Commission rate must be greater than or equal to 0',
466
+ commissionRateMin: 'Commission rate must be at least 0',
467
467
  commissionRateMax: 'Commission rate cannot exceed 100%',
468
468
  amount: 'Fixed Amount',
469
469
  noVendor: 'No vendor available',
@@ -501,7 +501,7 @@ export default flat({
501
501
  unit_amount: {
502
502
  required: 'Price is required',
503
503
  positive: 'Price must be positive',
504
- stripeTip: 'Stripe requires the price to be greater than or equal to 0.5',
504
+ stripeTip: 'Stripe requires the price to be at least 0.5',
505
505
  },
506
506
  nickname: {
507
507
  label: 'Price description',
@@ -518,8 +518,8 @@ export default flat({
518
518
  meteredTip:
519
519
  'Metered billing lets you charge customers based on reported usage at the end of each billing period.',
520
520
  aggregate: 'Charge for metered usage by',
521
- intervalCountTip: 'Billing interval must be a positive integer',
522
- stripeTip: 'Stripe requires the billing period to be greater than or equal to 1 day',
521
+ intervalCountTip: 'Billing interval must be a positive number',
522
+ stripeTip: 'Stripe requires the billing period to be at least 1 day',
523
523
  },
524
524
  currency: {
525
525
  add: 'Add more currencies',
@@ -566,7 +566,7 @@ export default flat({
566
566
  choosePricingModel: 'Choose your pricing model',
567
567
  usage: 'Usage',
568
568
  usageDesc:
569
- 'Price by number of users, units, or seats. Needs a record for Stripe to track customer service usage.',
569
+ 'Price by number of users, units, or seats. Requires a record for Stripe to track customer service usage.',
570
570
  meter: 'Meter',
571
571
  billingPeriod: 'Billing period',
572
572
  aggregate: {
@@ -581,14 +581,14 @@ export default flat({
581
581
  tip: '',
582
582
  },
583
583
  quantity: {
584
- tip: 'Quantity must be equal to or greater than 0',
584
+ tip: 'Quantity must be at least 0',
585
585
  },
586
586
  quantityAvailable: {
587
587
  label: 'Available quantity',
588
588
  placeholder: '0 means unlimited',
589
589
  format: 'Available {num} pieces',
590
590
  noLimit: 'No limit on available quantity',
591
- valid: 'Available quantity must be greater than or equal to sold quantity',
591
+ valid: 'Available quantity must be at least the sold quantity',
592
592
  description: 'Enter the number of units that can be sold, 0 means unlimited',
593
593
  },
594
594
  quantitySold: {
@@ -644,7 +644,7 @@ export default flat({
644
644
  promotionCodes: 'Promotion Codes',
645
645
  promotionCodesHelp: 'Promotion codes will be created after the coupon is saved.',
646
646
  addPromotionCode: 'Add promotion code',
647
- eligibleFirstTime: 'Eligible for first-time order onlyeach user can only enjoy one coupon discount',
647
+ eligibleFirstTime: 'Eligible for first-time order only (each user can only use one coupon discount)',
648
648
  limitNumberRedemptions: 'Limit the number of times this code can be redeemed',
649
649
  addExpirationDate: 'Add an expiration date',
650
650
  requireMinimumOrder: 'Require minimum order value',
@@ -1112,6 +1112,9 @@ export default flat({
1112
1112
  saved: 'Vendor saved successfully',
1113
1113
  test: 'Test',
1114
1114
  testConnection: 'Test Connection',
1115
+ testConnectionTip: 'Are you sure you want to test the connection for vendor "{name}"?',
1116
+ testConnectionSuccess: 'Connection test successful',
1117
+ testConnectionFailed: 'Connection test failed',
1115
1118
  testSuccess: 'Connection test successful',
1116
1119
  testAfterSave: 'Vendor saved successfully. You can test the connection to verify the configuration.',
1117
1120
  testToEnable: 'Vendor saved successfully. Test the connection to enable the vendor.',
@@ -1158,7 +1161,7 @@ export default flat({
1158
1161
  commission: 'Commission',
1159
1162
  commissionRate: 'Commission Rate',
1160
1163
  commissionRateRequired: 'Commission rate is required',
1161
- commissionRateMin: 'Commission rate must be greater than or equal to 0',
1164
+ commissionRateMin: 'Commission rate must be at least 0',
1162
1165
  commissionRateMax: 'Commission rate is too high',
1163
1166
  commissionType: 'Commission Type',
1164
1167
  commissionRateHelp: 'Commission rate as percentage',
@@ -1472,7 +1475,7 @@ export default flat({
1472
1475
  bufferThreshold: 'Buffer Threshold',
1473
1476
  bufferThresholdHelp:
1474
1477
  'Only when the amount exceeding the deposit threshold reaches the buffer threshold will the collection operation be triggered.',
1475
- bufferThresholdInvalid: 'Buffer threshold must be greater than or equal to 0',
1478
+ bufferThresholdInvalid: 'Buffer threshold must be at least 0',
1476
1479
  edit: 'Configure',
1477
1480
  enable: 'Enable',
1478
1481
  editTitle: 'Configure {currency} Vault Settings',
@@ -1488,7 +1491,7 @@ export default flat({
1488
1491
  noLimit: 'No limit',
1489
1492
  withdrawThresholdNoLimit: '0 means no withdrawal limit',
1490
1493
  depositThresholdRequired: 'Deposit threshold must be greater than 0',
1491
- withdrawThresholdInvalid: 'Withdrawal threshold must be greater or equal to 0',
1494
+ withdrawThresholdInvalid: 'Withdrawal threshold must be at least 0',
1492
1495
  enableSuccess: 'Successfully enabled vault wallet for {currency}',
1493
1496
  disableSuccess: 'Successfully disabled vault wallet for {currency}',
1494
1497
  updateSuccess: 'Successfully updated vault wallet settings for {currency}',
@@ -1084,6 +1084,9 @@ export default flat({
1084
1084
  saved: '供应商保存成功',
1085
1085
  test: '测试',
1086
1086
  testConnection: '测试连接',
1087
+ testConnectionTip: '确定要测试供应商 "{name}" 的连接吗?',
1088
+ testConnectionSuccess: '连接测试成功',
1089
+ testConnectionFailed: '连接测试失败',
1087
1090
  testSuccess: '连接测试成功',
1088
1091
  testAfterSave: '供应商保存成功。您可以测试连接以验证配置是否正确。',
1089
1092
  testToEnable: '供应商保存成功。请测试连接以启用供应商。',
@@ -261,7 +261,7 @@ export default function Overview() {
261
261
  actions={
262
262
  <Box sx={{ display: 'flex', gap: 2 }}>
263
263
  <Button onClick={() => setOpenMeterDialog(false)}>{t('common.cancel')}</Button>
264
- <Button onClick={() => navigate('/admin/meters')} variant="contained">
264
+ <Button onClick={() => navigate('/admin/billing/meters')} variant="contained">
265
265
  {t('common.goToConfigure')}
266
266
  </Button>
267
267
  </Box>
@@ -1,196 +0,0 @@
1
- import { api } from '@blocklet/payment-react';
2
- import type { TMeter, TCustomer, TPaymentCurrency, TSubscription } from '@blocklet/payment-types';
3
-
4
- export interface MeterInfo {
5
- meter?: TMeter & { paymentCurrency?: TPaymentCurrency };
6
- customer?: TCustomer;
7
- paymentCurrency?: TPaymentCurrency;
8
- subscription?: TSubscription;
9
- }
10
-
11
- export interface GetMeterInfoOptions {
12
- meterId?: string;
13
- customerId?: string;
14
- eventName?: string;
15
- includeCustomer?: boolean;
16
- includePaymentCurrency?: boolean;
17
- includeSubscription?: boolean;
18
- subscriptionId?: string;
19
- }
20
-
21
- /**
22
- * 获取meter相关信息的通用方法
23
- * @param options 配置选项
24
- * @returns Promise<MeterInfo>
25
- */
26
- export async function getMeterInfo(options: GetMeterInfoOptions = {}): Promise<MeterInfo> {
27
- const {
28
- meterId,
29
- customerId,
30
- eventName,
31
- includeCustomer = false,
32
- includePaymentCurrency = false,
33
- includeSubscription = false,
34
- subscriptionId,
35
- } = options;
36
-
37
- const result: MeterInfo = {};
38
-
39
- try {
40
- // 获取meter信息
41
- if (meterId) {
42
- const { data: meter } = await api.get(`/api/meters/${meterId}`);
43
- result.meter = meter;
44
-
45
- // 如果meter包含paymentCurrency且需要包含
46
- if (includePaymentCurrency && meter.paymentCurrency) {
47
- result.paymentCurrency = meter.paymentCurrency;
48
- }
49
- } else if (eventName) {
50
- // 通过event_name查找meter
51
- const { data: meters } = await api.get(`/api/meters?event_name=${encodeURIComponent(eventName)}`);
52
- if (meters.list && meters.list.length > 0) {
53
- const [firstMeter] = meters.list;
54
- result.meter = firstMeter;
55
-
56
- if (includePaymentCurrency && firstMeter.paymentCurrency) {
57
- result.paymentCurrency = firstMeter.paymentCurrency;
58
- }
59
- }
60
- }
61
-
62
- // 获取customer信息
63
- if (includeCustomer && customerId) {
64
- const { data: customer } = await api.get(`/api/customers/${customerId}`);
65
- result.customer = customer;
66
- }
67
-
68
- // 获取subscription信息
69
- if (includeSubscription && subscriptionId) {
70
- const { data: subscription } = await api.get(`/api/subscriptions/${subscriptionId}`);
71
- result.subscription = subscription;
72
- }
73
-
74
- // 如果需要paymentCurrency但还没有获取到,尝试从meter的currency_id获取
75
- if (includePaymentCurrency && !result.paymentCurrency && result.meter?.currency_id) {
76
- try {
77
- const { data: paymentCurrency } = await api.get(`/api/payment-currencies/${result.meter.currency_id}`);
78
- result.paymentCurrency = paymentCurrency;
79
- } catch (err) {
80
- console.warn('Failed to fetch payment currency:', err);
81
- }
82
- }
83
-
84
- return result;
85
- } catch (error) {
86
- console.error('Error fetching meter info:', error);
87
- throw error;
88
- }
89
- }
90
-
91
- /**
92
- * 获取meter的完整信息(包含所有关联数据)
93
- * @param meterId meter ID
94
- * @returns Promise<MeterInfo>
95
- */
96
- export function getMeterFullInfo(meterId: string): Promise<MeterInfo> {
97
- return getMeterInfo({
98
- meterId,
99
- includePaymentCurrency: true,
100
- });
101
- }
102
-
103
- /**
104
- * 根据事件名称获取meter信息
105
- * @param eventName 事件名称
106
- * @param includePaymentCurrency 是否包含支付货币信息
107
- * @returns Promise<MeterInfo>
108
- */
109
- export function getMeterByEventName(eventName: string, includePaymentCurrency = true): Promise<MeterInfo> {
110
- return getMeterInfo({
111
- eventName,
112
- includePaymentCurrency,
113
- });
114
- }
115
-
116
- /**
117
- * 获取meter事件的完整上下文信息
118
- * @param options 配置选项
119
- * @returns Promise<MeterInfo>
120
- */
121
- export function getMeterEventContext(options: {
122
- meterId?: string;
123
- eventName?: string;
124
- customerId?: string;
125
- subscriptionId?: string;
126
- }): Promise<MeterInfo> {
127
- const { meterId, eventName, customerId, subscriptionId } = options;
128
-
129
- return getMeterInfo({
130
- meterId,
131
- eventName,
132
- customerId,
133
- subscriptionId,
134
- includeCustomer: !!customerId,
135
- includePaymentCurrency: true,
136
- includeSubscription: !!subscriptionId,
137
- });
138
- }
139
-
140
- /**
141
- * 批量获取多个meter的信息
142
- * @param meterIds meter ID数组
143
- * @param includePaymentCurrency 是否包含支付货币信息
144
- * @returns Promise<MeterInfo[]>
145
- */
146
- export function getBatchMeterInfo(meterIds: string[], includePaymentCurrency = true): Promise<MeterInfo[]> {
147
- const promises = meterIds.map((meterId) =>
148
- getMeterInfo({
149
- meterId,
150
- includePaymentCurrency,
151
- }).catch((error) => {
152
- console.warn(`Failed to fetch meter ${meterId}:`, error);
153
- return { meter: undefined };
154
- })
155
- );
156
-
157
- return Promise.all(promises);
158
- }
159
-
160
- /**
161
- * 获取客户的meter使用情况
162
- * @param customerId 客户ID
163
- * @param meterId 可选的meter ID,如果不提供则获取所有meter
164
- * @returns Promise<MeterInfo[]>
165
- */
166
- export async function getCustomerMeterUsage(customerId: string, meterId?: string): Promise<MeterInfo[]> {
167
- try {
168
- // 获取客户的credit transactions来找到相关的meters
169
- const params = new URLSearchParams();
170
- params.append('customer_id', customerId);
171
- if (meterId) {
172
- params.append('meter_id', meterId);
173
- }
174
-
175
- const { data: transactions } = await api.get(`/api/credit-transactions?${params.toString()}`);
176
-
177
- // 提取唯一的meter IDs
178
- const uniqueMeterIds = [
179
- ...new Set(
180
- transactions.list
181
- ?.map((t: any) => t.meter?.id)
182
- .filter((id: any): id is string => Boolean(id) && typeof id === 'string')
183
- ),
184
- ] as string[];
185
-
186
- if (uniqueMeterIds.length === 0) {
187
- return [];
188
- }
189
-
190
- // 批量获取meter信息
191
- return await getBatchMeterInfo(uniqueMeterIds);
192
- } catch (error) {
193
- console.error('Error fetching customer meter usage:', error);
194
- return [];
195
- }
196
- }