payment-kit 1.20.9 → 1.20.11

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/README.md CHANGED
@@ -1,46 +1,47 @@
1
1
  # Payment Kit
2
2
 
3
- The decentralized stripe for blocklet platform.
3
+ The decentralized Stripe for the Blocklet platform.
4
4
 
5
5
  ## Contribution
6
6
 
7
7
  ### Development
8
8
 
9
- 1. clone the repo
10
- 2. run `make build`
11
- 3. run `cd blocklets/core && blocklet dev`
9
+ 1. Clone the repository
10
+ 2. Run `make build`
11
+ 3. Run `cd blocklets/core && blocklet dev`
12
12
 
13
- ##### when error
13
+ #### Troubleshooting
14
14
 
15
- 1. pre-start error component xxx is not running or unreachable
15
+ **Error: "pre-start error component xxx is not running or unreachable"**
16
16
 
17
- - create .env.local file in this root
18
- - add BLOCKLET_DEV_APP_DID="did:abt:your payment kit server did"
19
- - add BLOCKLET_DEV_MOUNT_POINT="/example"
20
- - copy .env.local to be under the /core
21
- - edit BLOCKLET_DEV_MOUNT_POINT="/"
17
+ - Create a `.env.local` file in the project root
18
+ - Add `BLOCKLET_DEV_APP_DID="did:abt:your payment kit server did"`
19
+ - Add `BLOCKLET_DEV_MOUNT_POINT="/example"`
20
+ - Copy `.env.local` to the `/core` directory
21
+ - Edit `BLOCKLET_DEV_MOUNT_POINT="/"`
22
22
 
23
- 2. Insufficient fund to pay for tx cost from xxx, expected 1.0020909, got 0
24
- - copy BLOCKLET_DEV_APP_DID
25
- - transfer 2 TBA in your DID Wallet to your copied address
23
+ **Error: "Insufficient funds to pay for transaction cost from xxx, expected 1.0020909, got 0"**
24
+
25
+ - Copy the `BLOCKLET_DEV_APP_DID`
26
+ - Transfer 2 TBA from your DID Wallet to the copied address
26
27
 
27
28
  ### Debug Stripe
28
29
 
29
- 1. Install and login with instructions from: https://stripe.com/docs/stripe-cli
30
- 2. Start your local payment-kit server, get it's port
30
+ 1. Install and log in following the instructions at: https://stripe.com/docs/stripe-cli
31
+ 2. Start your local Payment Kit server and note its port
31
32
  3. Run `stripe listen --forward-to http://127.0.0.1:8188/api/integrations/stripe/webhook --log-level=debug --latest`
32
33
 
33
34
  ### Test Stripe
34
35
 
35
- Invoices for subscriptions are not finalized automatically. You can use stripe postman collection to finalize it and then confirm the payment.
36
+ Invoices for subscriptions are not finalized automatically. You can use the Stripe Postman collection to finalize them and then confirm the payment.
36
37
 
37
38
  ### Easter Eggs
38
39
 
39
- None public environment variables used in the code, can change the behavior of the payment-kit.
40
+ Non-public environment variables used in the code that can change the behavior of Payment Kit:
40
41
 
41
- - PAYMENT_CHANGE_LOCKED_PRICE: allow change locked price, must be set to "1" to work
42
- - PAYMENT_RELOAD_SUBSCRIPTION_JOBS: reload subscription jobs on start, must be set to "1" to work
43
- - PAYMENT_BILLING_THRESHOLD: global default billing threshold, must be a number
44
- - PAYMENT_MIN_STAKE_AMOUNT: global min stake amount limit, must be a number
45
- - PAYMENT_DAYS_UNTIL_DUE: global default days until due, must be a number
46
- - PAYMENT_DAYS_UNTIL_CANCEL: global default days until cancel, must be a number
42
+ - `PAYMENT_CHANGE_LOCKED_PRICE`: Allows changing locked price (must be set to "1" to enable)
43
+ - `PAYMENT_RELOAD_SUBSCRIPTION_JOBS`: Reloads subscription jobs on start (must be set to "1" to enable)
44
+ - `PAYMENT_BILLING_THRESHOLD`: Global default billing threshold (must be a number)
45
+ - `PAYMENT_MIN_STAKE_AMOUNT`: Global minimum stake amount limit (must be a number)
46
+ - `PAYMENT_DAYS_UNTIL_DUE`: Global default days until due (must be a number)
47
+ - `PAYMENT_DAYS_UNTIL_CANCEL`: Global default days until cancellation (must be a number)
@@ -6,7 +6,7 @@ import { handlePaymentIntentEvent } from './payment-intent';
6
6
  import { handleSetupIntentEvent } from './setup-intent';
7
7
  import { handleSubscriptionEvent } from './subscription';
8
8
 
9
- export default function handleStripeEvent(event: any, client: Stripe) {
9
+ export default async function handleStripeEvent(event: any, client: Stripe) {
10
10
  switch (event.type) {
11
11
  case 'payment_intent.canceled':
12
12
  case 'payment_intent.created':
@@ -14,8 +14,16 @@ export default function handleStripeEvent(event: any, client: Stripe) {
14
14
  case 'payment_intent.payment_failed':
15
15
  case 'payment_intent.processing':
16
16
  case 'payment_intent.requires_action':
17
- case 'payment_intent.succeeded':
17
+ case 'payment_intent.succeeded': {
18
+ if (event.data?.object?.id) {
19
+ const record = await client.paymentIntents.retrieve(event.data.object.id);
20
+ event.data.object = {
21
+ ...event.data.object,
22
+ ...record,
23
+ };
24
+ }
18
25
  return handlePaymentIntentEvent(event, client);
26
+ }
19
27
 
20
28
  // case 'setup_intent.created':
21
29
  case 'setup_intent.canceled':
@@ -67,7 +67,7 @@ export async function getOneTimeProductInfo(invoiceId: string, paymentCurrency:
67
67
  include: [{ model: InvoiceItem, as: 'lines' }],
68
68
  });
69
69
  if (!doc) {
70
- throw new Error(`Invoice not found in ${invoiceId}`);
70
+ throw new Error(`Invoice not found: ${invoiceId}`);
71
71
  }
72
72
  const json = doc.toJSON();
73
73
  const products = (await Product.findAll()).map((x) => x.toJSON());
@@ -733,19 +733,19 @@ export async function ensureOverdraftProtectionInvoiceAndItems({
733
733
  const invoicePrice = price?.currency_options?.find((x: any) => x.currency_id === paymentIntent?.currency_id);
734
734
 
735
735
  if (!subscription.overdraft_protection?.enabled) {
736
- throw new Error('create overdraft protection invoice skipped due to overdraft protection not enabled');
736
+ throw new Error('Overdraft protection invoice creation skipped: overdraft protection is not enabled');
737
737
  }
738
738
 
739
739
  if (!invoicePrice) {
740
- throw new Error('overdraft protection invoice price not found');
740
+ throw new Error('Overdraft protection invoice price not found');
741
741
  }
742
742
  const currency = await PaymentCurrency.findByPk(invoicePrice.currency_id);
743
743
  if (!currency) {
744
- throw new Error('overdraft protection invoice currency not found');
744
+ throw new Error('Overdraft protection invoice currency not found');
745
745
  }
746
746
  const paymentMethod = await PaymentMethod.findByPk(currency?.payment_method_id);
747
747
  if (!paymentMethod) {
748
- throw new Error('overdraft protection invoice payment method not found');
748
+ throw new Error('Overdraft protection invoice payment method not found');
749
749
  }
750
750
 
751
751
  if (paymentMethod.type !== 'arcblock') {
@@ -754,7 +754,7 @@ export async function ensureOverdraftProtectionInvoiceAndItems({
754
754
 
755
755
  const { unused } = await isSubscriptionOverdraftProtectionEnabled(subscription, paymentIntent?.currency_id);
756
756
  if (new BN(unused).lt(new BN(invoicePrice.unit_amount))) {
757
- throw new Error('create overdraft protection invoice skipped due to insufficient overdraft protection');
757
+ throw new Error('Overdraft protection invoice creation skipped: insufficient overdraft protection funds');
758
758
  }
759
759
 
760
760
  const result = await createInvoiceWithItems({
@@ -1,5 +1,6 @@
1
1
  import { VendorAuth } from '@blocklet/payment-vendor';
2
2
 
3
+ import { joinURL } from 'ufo';
3
4
  import { ProductVendor } from '../../../store/models';
4
5
  import logger from '../../logger';
5
6
  import { api } from '../../util';
@@ -53,11 +54,14 @@ export class LauncherAdapter implements VendorAdapter {
53
54
  };
54
55
 
55
56
  const { headers, body } = VendorAuth.signRequestWithHeaders(orderData);
56
- const response = await fetch(`${launcherApiUrl}/api/vendor/deliveries`, {
57
- method: 'POST',
58
- headers,
59
- body,
60
- });
57
+ const response = await fetch(
58
+ joinURL(launcherApiUrl, vendorConfig.metadata?.mountPoint || '', '/api/vendor/deliveries'),
59
+ {
60
+ method: 'POST',
61
+ headers,
62
+ body,
63
+ }
64
+ );
61
65
 
62
66
  if (!response.ok) {
63
67
  const errorBody = await response.text();
@@ -125,11 +129,14 @@ export class LauncherAdapter implements VendorAdapter {
125
129
  const launcherApiUrl = vendorConfig.app_url;
126
130
  const { headers, body } = VendorAuth.signRequestWithHeaders(params);
127
131
 
128
- const response = await fetch(`${launcherApiUrl}/api/vendor/return`, {
129
- method: 'POST',
130
- headers,
131
- body,
132
- });
132
+ const response = await fetch(
133
+ joinURL(launcherApiUrl, vendorConfig.metadata?.mountPoint || '', '/api/vendor/return'),
134
+ {
135
+ method: 'POST',
136
+ headers,
137
+ body,
138
+ }
139
+ );
133
140
 
134
141
  if (!response.ok) {
135
142
  const errorBody = await response.text();
@@ -1,13 +1,12 @@
1
1
  import { BN } from '@ocap/util';
2
+ import { Customer, PaymentMethod } from '../../store/models';
2
3
  import { CheckoutSession } from '../../store/models/checkout-session';
3
- import { Product } from '../../store/models/product';
4
- import { ProductVendor } from '../../store/models/product-vendor';
5
- import { Payout } from '../../store/models/payout';
6
4
  import { PaymentIntent } from '../../store/models/payment-intent';
7
- import { Price } from '../../store/models/price';
8
- import { calculateVendorCommission, VendorAdapterFactory } from './adapters/factory';
9
- import { Customer } from '../../store/models';
5
+ import { Payout } from '../../store/models/payout';
6
+ import { ProductVendor } from '../../store/models/product-vendor';
7
+ import env from '../env';
10
8
  import logger from '../logger';
9
+ import { calculateVendorCommission, VendorAdapterFactory } from './adapters/factory';
11
10
 
12
11
  const DEFAULT_COMMISSION_RATE = 20;
13
12
 
@@ -80,11 +79,11 @@ export class VendorFulfillmentService {
80
79
  amount: orderInfo.amount_total,
81
80
  currency: orderInfo.currency_id,
82
81
 
83
- description: 'This is build a instance for Third Party',
82
+ description: `This is an app launched by ${env.appName || 'Third Party'}`,
84
83
  userInfo: {
85
84
  userDid: orderInfo.customer_did!,
86
85
  email: userEmail,
87
- description: 'This is build a instance for Third Party',
86
+ description: `This is an app launched by ${env.appName || 'Third Party'}`,
88
87
  },
89
88
  deliveryParams: {
90
89
  blockletMetaUrl: vendor.metadata?.blockletMetaUrl,
@@ -125,117 +124,6 @@ export class VendorFulfillmentService {
125
124
  }
126
125
  }
127
126
 
128
- // Calculate commission data (no fulfillment, only calculate commission)
129
- private static async calculateCommissionData(checkoutSessionId: string): Promise<VendorFulfillmentResult[]> {
130
- try {
131
- logger.info('Calculating commission data for checkout session', { checkoutSessionId });
132
-
133
- const checkoutSession = await CheckoutSession.findByPk(checkoutSessionId);
134
- if (!checkoutSession) {
135
- throw new Error(`CheckoutSession not found: ${checkoutSessionId}`);
136
- }
137
-
138
- const priceIds = checkoutSession.line_items.map((item: any) => item.price_id).filter(Boolean);
139
-
140
- if (priceIds.length === 0) {
141
- throw new Error(`No price IDs found in checkout session line items: ${checkoutSessionId}`);
142
- }
143
-
144
- // Find corresponding product_ids through price_ids
145
- const prices = await Price.findAll({
146
- where: { id: priceIds },
147
- attributes: ['id', 'product_id'],
148
- });
149
-
150
- // Create a map of price_id to line_item for amount calculation
151
- const priceToLineItemMap = new Map();
152
- checkoutSession.line_items.forEach((item: any) => {
153
- if (item.price_id) {
154
- priceToLineItemMap.set(item.price_id, item);
155
- }
156
- });
157
-
158
- const productIds = prices.map((price: any) => price.product_id).filter(Boolean);
159
-
160
- if (productIds.length === 0) {
161
- throw new Error(`No product IDs found from prices: ${checkoutSessionId}`);
162
- }
163
-
164
- // Get all products with vendor configurations
165
- const products = await Product.findAll({
166
- where: { id: productIds },
167
- attributes: ['id', 'vendor_config'],
168
- });
169
-
170
- const commissionResults: VendorFulfillmentResult[] = [];
171
-
172
- // Process each product separately
173
- for (const product of products) {
174
- if (!product.vendor_config || product.vendor_config.length === 0) {
175
- logger.debug('No vendor configuration found for product', {
176
- productId: product.id,
177
- checkoutSessionId,
178
- });
179
- // eslint-disable-next-line no-continue
180
- continue;
181
- }
182
-
183
- // Find all prices for this product
184
- const productPrices = prices.filter((price: any) => price.product_id === product.id);
185
-
186
- // Calculate total amount for this product across all line items
187
- let productTotalAmount = '0';
188
- for (const price of productPrices) {
189
- const lineItem = priceToLineItemMap.get(price.id);
190
- if (lineItem && lineItem.amount_total) {
191
- productTotalAmount = new BN(productTotalAmount).add(new BN(lineItem.amount_total || '0')).toString();
192
- }
193
- }
194
-
195
- // Calculate commission for each vendor of this product
196
- for (const vendorConfig of product.vendor_config) {
197
- const commissionAmount = calculateVendorCommission(
198
- productTotalAmount, // Use product-specific amount instead of total order amount
199
- vendorConfig.commission_rate || 0,
200
- vendorConfig.commission_type || 'percentage',
201
- vendorConfig.amount
202
- );
203
-
204
- commissionResults.push({
205
- vendorId: vendorConfig.vendor_key,
206
- vendorKey: vendorConfig.vendor_key,
207
- orderId: `commission_${checkoutSessionId}_${product.id}_${vendorConfig.vendor_id}`,
208
- status: 'completed',
209
- commissionAmount,
210
- commissionType: vendorConfig.commission_type || 'percentage',
211
- commissionRate: vendorConfig.commission_rate || 0,
212
- productId: product.id, // Add product ID for tracking
213
- });
214
- }
215
- }
216
-
217
- if (commissionResults.length === 0) {
218
- logger.warn('No vendor configurations found for any products', {
219
- productIds,
220
- checkoutSessionId,
221
- });
222
- }
223
-
224
- logger.info('Commission data calculated', {
225
- checkoutSessionId,
226
- commissionCount: commissionResults.length,
227
- });
228
-
229
- return commissionResults;
230
- } catch (error: any) {
231
- logger.error('Failed to calculate commission data', {
232
- checkoutSessionId,
233
- error: error.message,
234
- });
235
- throw error;
236
- }
237
- }
238
-
239
127
  static async createVendorPayouts(
240
128
  checkoutSessionId: string,
241
129
  fulfillmentResults?: VendorFulfillmentResult[]
@@ -257,13 +145,19 @@ export class VendorFulfillmentService {
257
145
  // If fulfillmentResults not provided, calculate commission info
258
146
  let commissionData = fulfillmentResults;
259
147
  if (!commissionData) {
260
- commissionData = await this.calculateCommissionData(checkoutSessionId);
148
+ commissionData = checkoutSession.vendor_info?.map((vendorInfo: any) => ({
149
+ vendorId: vendorInfo.vendor_id,
150
+ orderId: vendorInfo.order_id,
151
+ status: vendorInfo.status,
152
+ commissionAmount: vendorInfo.commissionAmount,
153
+ })) as VendorFulfillmentResult[];
261
154
  }
262
155
 
263
156
  const payoutPromises = commissionData
264
157
  .filter((result) => result.status !== 'failed' && new BN(result.commissionAmount).gt(new BN('0')))
265
158
  .map(async (result) => {
266
159
  const vendor = await ProductVendor.findByPk(result.vendorId);
160
+ const paymentMethod = await PaymentMethod.findByPk(paymentMethodId);
267
161
 
268
162
  const appPid = vendor?.app_pid;
269
163
  const destination = appPid || vendor?.metadata?.wallet_address || vendor?.metadata?.destination || '';
@@ -278,7 +172,7 @@ export class VendorFulfillmentService {
278
172
  customer_id: checkoutSession.customer_id || '',
279
173
  payment_intent_id: checkoutSession.payment_intent_id || '',
280
174
  payment_method_id: paymentMethodId,
281
- status: 'pending',
175
+ status: paymentMethod?.type === 'stripe' ? 'deferred' : 'pending',
282
176
  attempt_count: 0,
283
177
  attempted: false,
284
178
  vendor_info: {
@@ -54,45 +54,45 @@ export default flat({
54
54
 
55
55
  billingDiscrepancy: {
56
56
  title: '{productName} billing discrepancy',
57
- body: 'Detected billing discrepancy for {productName}, please check.',
57
+ body: 'A billing discrepancy has been detected for {productName}. Please review your billing details.',
58
58
  },
59
59
 
60
60
  sendTo: 'Sent to',
61
61
  mintNFT: {
62
62
  title: '{collection} NFT minted',
63
- message: 'A new {collection} NFT is minted and sent to your wallet, please check it out.',
63
+ message: 'A new {collection} NFT has been minted and sent to your wallet. Please check your wallet to view it.',
64
64
  },
65
65
 
66
66
  usageReportEmpty: {
67
67
  title: 'No usage report for {productName}',
68
- body: 'No usage report for {productName} detected, please check.',
68
+ body: 'No usage report has been detected for {productName}. Please verify your usage reporting configuration.',
69
69
  },
70
70
 
71
71
  meteringSubscriptionDetection: {
72
72
  title: '[{appName}] Metering subscription detection',
73
- body: 'During {startTimeStr} - {endTimeStr}, a total of {totalCount} subscriptions with usage-based billing were scanned. \nOut of these, {normalCount} were normal, while {abnormalCount} had anomalies, including {unreportedCount} unreported subscriptions and {discrepantCount} with billing discrepancies. \n\n Abnormal subscriptions:',
73
+ body: 'During {startTimeStr} - {endTimeStr}, a total of {totalCount} subscriptions with usage-based billing were scanned.\nOf these, {normalCount} were normal, while {abnormalCount} had anomalies, including {unreportedCount} unreported subscriptions and {discrepantCount} with billing discrepancies.\n\nAbnormal subscriptions:',
74
74
  healthyBody:
75
- 'During {startTimeStr} - {endTimeStr}, a total of {totalCount} subscriptions with usage-based billing were scanned, and all subscriptions were normal.',
75
+ 'During {startTimeStr} - {endTimeStr}, a total of {totalCount} subscriptions with usage-based billing were scanned, and all subscriptions were functioning normally.',
76
76
  view: 'Manage subscriptions',
77
77
  },
78
78
 
79
79
  subscriptionTrialStart: {
80
80
  title: 'Welcome to the start of your {productName} trial',
81
- body: 'Congratulations on your {productName} trial! The length of the trial is {trialDuration} and will end at {subscriptionTrialEnd}. Have fun with {productName}!',
81
+ body: 'Congratulations on starting your {productName} trial! Your trial period is {trialDuration} and will end on {subscriptionTrialEnd}. Enjoy exploring {productName}!',
82
82
  },
83
83
 
84
84
  subscriptionTrialWillEnd: {
85
85
  title: 'The {productName} trial will end soon',
86
86
  body: 'Your trial for {productName} will end in {willRenewDuration}. Please ensure your account balance is sufficient for automatic billing after the trial ends. Thank you for your support and trust!',
87
87
  unableToPayBody:
88
- 'Your trial for {productName} will end in {willRenewDuration}.Your current balance is {balance}, which is less than {price}, Please ensure your balance is sufficient for automatic billing. Thank you for your support and trust!',
88
+ 'Your trial for {productName} will end in {willRenewDuration}. Your current balance is {balance}, which is less than {price}. Please ensure your balance is sufficient for automatic billing. Thank you for your support and trust!',
89
89
  unableToPayReason:
90
- 'The estimated payment amount is {price}, but the current balance is insufficient ({balance}), please ensure that your account has enough balance to avoid payment failure.',
90
+ 'The estimated payment amount is {price}, but your current balance is insufficient ({balance}). Please ensure your account has enough balance to avoid payment failure.',
91
91
  },
92
92
 
93
93
  subscriptionSucceed: {
94
94
  title: "Congratulations! You've successfully subscribed to {productName}",
95
- body: 'Thank you for successfully subscribing to {productName} on {at}. We will be happy to provide you with excellent service, and we wish you a pleasant experience.',
95
+ body: 'Thank you for successfully subscribing to {productName} on {at}. We are happy to provide you with excellent service, and we wish you a pleasant experience.',
96
96
  },
97
97
 
98
98
  oneTimePaymentSucceeded: {
@@ -102,7 +102,7 @@ export default flat({
102
102
 
103
103
  subscriptionUpgraded: {
104
104
  title: 'Congratulations! Your subscription plan for {productName} has been successfully upgraded',
105
- body: 'Your subscription plan for {productName} has been successfully upgraded at {at}, thank you for your support, we wish you a pleasant experience!',
105
+ body: 'Your subscription plan for {productName} has been successfully upgraded on {at}. Thank you for your support, and we wish you a pleasant experience!',
106
106
  },
107
107
 
108
108
  subscriptionWillRenew: {
@@ -111,53 +111,53 @@ export default flat({
111
111
  unableToPayBody:
112
112
  'Your subscription to {productName} is scheduled for automatic payment on {at}({willRenewDuration} later). If you have any questions or need assistance, please feel free to contact us.',
113
113
  unableToPayReason:
114
- 'The estimated payment amount is {price}, but the current balance is insufficient ({balance}), please ensure that your account has enough balance to avoid payment failure.',
114
+ 'The estimated payment amount is {price}, but your current balance is insufficient ({balance}). Please ensure your account has enough balance to avoid payment failure.',
115
115
  renewAmount: 'Payment amount',
116
116
  estimatedAmountNote: 'Estimate {amount}, billed based on final usage',
117
117
  },
118
118
 
119
119
  subscriptionRenewed: {
120
120
  title: '{productName} payment successful',
121
- body: 'Payment for your subscription {productName} is successfully collected on {at}. Thanks for your continued support and trust, we wish you a pleasant journey when using this service!',
122
- noExpenseIncurred: 'No expenses incurred during the service ',
121
+ body: 'Payment for your {productName} subscription was successfully collected on {at}. Thanks for your continued support and trust. We wish you a pleasant experience using this service!',
122
+ noExpenseIncurred: 'No expenses incurred during the service period',
123
123
  },
124
124
 
125
125
  subscriptionRenewFailed: {
126
126
  title: '{productName} automatic payment failed',
127
- body: 'We are sorry to inform you that your {productName} failed to go through the automatic payment on {at}. If you have any questions, please contact us in time. Thank you!',
127
+ body: 'We are sorry to inform you that your {productName} automatic payment failed on {at}. If you have any questions, please contact us promptly. Thank you!',
128
128
  reason: {
129
- noDidWallet: 'You have not bound DID Wallet, please bind DID Wallet to ensure sufficient balance',
130
- noDelegation: 'Your DID Wallet has not been authorized, please update authorization',
129
+ noDidWallet: 'You have not connected a DID Wallet. Please connect your DID Wallet to ensure sufficient balance',
130
+ noDelegation: 'Your DID Wallet has not been authorized. Please update authorization',
131
131
  noTransferPermission:
132
- 'Your DID Wallet has not granted transfer permission to the application, please update authorization',
132
+ 'Your DID Wallet has not granted transfer permission to the application. Please update authorization',
133
133
  noTokenPermission:
134
- 'Your DID Wallet has not granted token transfer permission to the application, please update authorization',
134
+ 'Your DID Wallet has not granted token transfer permission to the application. Please update authorization',
135
135
  noTransferTo:
136
- 'Your DID Wallet has not granted the application automatic payment permission, please update authorization',
137
- noEnoughAllowance: 'The deduction amount exceeds the single transfer limit, please update authorization',
138
- noToken: 'Your account has no tokens, please add funds',
139
- noEnoughToken: 'Your account token balance is {balance}, insufficient for {price}, please add funds',
140
- noSupported: 'It is not supported to automatically pay with tokens, please check your package',
136
+ 'Your DID Wallet has not granted automatic payment permission to the application. Please update authorization',
137
+ noEnoughAllowance: 'The deduction amount exceeds the single transfer limit. Please update authorization',
138
+ noToken: 'Your account has no tokens. Please add funds',
139
+ noEnoughToken: 'Your account token balance is {balance}, which is insufficient for {price}. Please add funds',
140
+ noSupported: 'Automatic payment with tokens is not supported. Please check your package',
141
141
  txSendFailed: 'Failed to send automatic payment transaction',
142
142
  },
143
143
  },
144
144
 
145
145
  autoRechargeFailed: {
146
146
  title: 'Auto Top-Up payment failed',
147
- body: 'We are sorry to inform you that your {creditCurrencyName} auto top-up failed to go through the automatic payment on {at}. If you have any questions, please contact us in time. Thank you!',
147
+ body: 'We are sorry to inform you that your {creditCurrencyName} auto top-up automatic payment failed on {at}. If you have any questions, please contact us promptly. Thank you!',
148
148
  reason: {
149
- noDidWallet: 'You have not bound DID Wallet, please bind DID Wallet to ensure sufficient balance',
150
- noDelegation: 'Your DID Wallet has not been authorized, please update authorization',
149
+ noDidWallet: 'You have not connected a DID Wallet. Please connect your DID Wallet to ensure sufficient balance',
150
+ noDelegation: 'Your DID Wallet has not been authorized. Please update authorization',
151
151
  noTransferPermission:
152
- 'Your DID Wallet has not granted transfer permission to the application, please update authorization',
152
+ 'Your DID Wallet has not granted transfer permission to the application. Please update authorization',
153
153
  noTokenPermission:
154
- 'Your DID Wallet has not granted token transfer permission to the application, please update authorization',
154
+ 'Your DID Wallet has not granted token transfer permission to the application. Please update authorization',
155
155
  noTransferTo:
156
- 'Your DID Wallet has not granted the application automatic payment permission, please update authorization',
157
- noEnoughAllowance: 'The deduction amount exceeds the single transfer limit, please update authorization',
158
- noToken: 'Your account has no tokens, please add funds',
159
- noEnoughToken: 'Your account token balance is {balance}, insufficient for {price}, please add funds',
160
- noSupported: 'It is not supported to automatically pay with tokens, please check your package',
156
+ 'Your DID Wallet has not granted automatic payment permission to the application. Please update authorization',
157
+ noEnoughAllowance: 'The deduction amount exceeds the single transfer limit. Please update authorization',
158
+ noToken: 'Your account has no tokens. Please add funds',
159
+ noEnoughToken: 'Your account token balance is {balance}, which is insufficient for {price}. Please add funds',
160
+ noSupported: 'Automatic payment with tokens is not supported. Please check your package',
161
161
  txSendFailed: 'Failed to send automatic payment transaction',
162
162
  },
163
163
  },
@@ -187,7 +187,7 @@ export default flat({
187
187
 
188
188
  customerRewardSucceeded: {
189
189
  title: 'Thanks for your reward of {amount}',
190
- body: 'Thanks for your reward on {at}, the amount of reward is {amount}. Your support is our driving force, thanks for your generous support!',
190
+ body: 'Thank you for your reward on {at}. The reward amount is {amount}. Your support is our driving force. Thank you for your generous support!',
191
191
  received: '{address} has received {amount}',
192
192
  viewDetail: 'View Reference',
193
193
  },
@@ -204,14 +204,14 @@ export default flat({
204
204
  },
205
205
  sender: 'Payer',
206
206
  viewDetail: 'View Details',
207
- sended: '{address} has sent {amount}',
207
+ sent: '{address} has sent {amount}',
208
208
  },
209
209
  subscriptWillCanceled: {
210
- title: '{productName} subscription is about to be cancelled ',
210
+ title: '{productName} subscription is about to be cancelled',
211
211
  pastDue:
212
- 'Your subscription {productName} will be automatically unsubscribed by the system after {at} (after {willCancelDuration}) due to a long period of failure to automatically complete the automatic payment. Please handle the problem of automatic payment manually in time, so as not to affect the use. If you have any questions, please feel free to contact us.',
212
+ 'Your {productName} subscription will be automatically cancelled by the system after {at} ({willCancelDuration} later) due to repeated automatic payment failures. Please resolve the automatic payment issue manually to avoid service interruption. If you have any questions, please feel free to contact us.',
213
213
  body: 'Your subscription to {productName} will be automatically canceled on {at} ({willCancelDuration} later). If you have any questions, please feel free to contact us.',
214
- renewAmount: 'deduction amount ',
214
+ renewAmount: 'Deduction amount',
215
215
  cancelReason: 'Cancel reason',
216
216
  revokeStake: 'Revoke stake',
217
217
  adminCanceled: 'Admin canceled',
@@ -373,11 +373,7 @@ export const handlePaymentSucceed = async (
373
373
  );
374
374
  }
375
375
 
376
- // Trigger vendor commission queue
377
- events.emit('vendor.commission.queued', `vendor-commission-${paymentIntent.id}`, {
378
- paymentIntentId: paymentIntent.id,
379
- retryOnError: true,
380
- });
376
+ // Trigger vendor commission queue by invoice.paid
381
377
 
382
378
  let invoice;
383
379
  if (paymentIntent.invoice_id) {
@@ -167,25 +167,27 @@ export const startVendorCommissionQueue = async () => {
167
167
  });
168
168
  };
169
169
 
170
- events.on('vendor.commission.queued', async (id, job, args = {}) => {
170
+ events.on('invoice.paid', async (invoice) => {
171
171
  try {
172
- const { ...extraArgs } = args;
173
- const exist = await vendorCommissionQueue.get(id);
172
+ const paymentIntent = await PaymentIntent.findOne({ where: { invoice_id: invoice.id } });
174
173
 
175
- if (!exist) {
176
- logger.info('Vendor commission job added successfully', { id });
177
- vendorCommissionQueue.push({
178
- id,
179
- job,
180
- ...extraArgs,
181
- });
182
- } else {
174
+ if (!paymentIntent) {
175
+ logger.warn('PaymentIntent not found', { id: invoice.id });
176
+ return;
177
+ }
178
+
179
+ const id = `vendor-commission-${paymentIntent.id}`;
180
+ const exist = await vendorCommissionQueue.get(id);
181
+ if (exist) {
183
182
  logger.info('Vendor commission job already exists, skipping', { id });
183
+ return;
184
184
  }
185
- } catch (error: any) {
186
- logger.error('Failed to handle vendor commission queue event', {
185
+
186
+ vendorCommissionQueue.push({
187
187
  id,
188
- error,
188
+ job: { paymentIntentId: paymentIntent.id, retryOnError: true },
189
189
  });
190
+ } catch (error) {
191
+ logger.error('Failed to trigger vendor commission queue', { invoiceId: invoice.id, error });
190
192
  }
191
193
  });
@@ -273,14 +273,14 @@ async function updateSingleVendorInfo(
273
273
  vendor_id: current[index]?.vendor_id || vendorId,
274
274
  order_id: update.order_id || current[index]?.order_id || '',
275
275
  status: update.status || current[index]?.status || 'pending',
276
- amount: update.amount || current[index]?.amount || '0',
276
+ amount: update.commissionAmount || current[index]?.amount || '0',
277
277
  } as VendorInfo;
278
278
  } else {
279
279
  current.push({
280
280
  vendor_id: vendorId,
281
281
  order_id: update.order_id || '',
282
282
  status: update.status || 'pending',
283
- amount: update.amount || '0',
283
+ amount: update.commissionAmount || '0',
284
284
  ...update,
285
285
  } as VendorInfo);
286
286
  }
@@ -1,6 +1,6 @@
1
1
  import { joinURL } from 'ufo';
2
+ import { VendorAuth } from '@blocklet/payment-vendor';
2
3
  import createQueue from '../../libs/queue';
3
- import { getBlockletJson } from '../../libs/util';
4
4
  import { CheckoutSession } from '../../store/models/checkout-session';
5
5
  import { ProductVendor } from '../../store/models';
6
6
  import { fulfillmentCoordinatorQueue } from './fulfillment-coordinator';
@@ -99,12 +99,17 @@ export const handleVendorStatusCheck = async (job: VendorStatusCheckJob) => {
99
99
  logger.info('found vendor url', { url, productVendor });
100
100
 
101
101
  if (url) {
102
- const vendorMeta = await getBlockletJson(url);
103
- const mountPoint = vendorMeta.componentMountPoints?.find((x: any) => x.appId === vendorId)?.mountPoint;
104
- const serverStatusUrl = joinURL(url, mountPoint, '/api/vendor/status', vendor.order_id);
105
-
106
- const result = await fetch(serverStatusUrl);
102
+ const serverStatusUrl = joinURL(
103
+ url,
104
+ productVendor?.metadata?.mountPoint,
105
+ '/api/vendor/status',
106
+ vendor.order_id
107
+ );
108
+ const { headers } = VendorAuth.signRequestWithHeaders({});
109
+
110
+ const result = await fetch(serverStatusUrl, { headers });
107
111
  const data = await result.json();
112
+
108
113
  vendor.app_url = data?.appUrl || '';
109
114
  }
110
115
  }
@@ -22,6 +22,9 @@ const createVendorSchema = Joi.object({
22
22
  app_url: Joi.string().uri().max(512).required(),
23
23
  app_pid: Joi.string().max(255).allow('').optional(),
24
24
  app_logo: Joi.string().max(512).allow('').optional(),
25
+ vendor_did: Joi.string()
26
+ .pattern(/^(did:abt:)?[1-9A-HJ-NP-Za-km-z]{37}$/)
27
+ .required(),
25
28
  status: Joi.string().valid('active', 'inactive').default('active'),
26
29
  metadata: MetadataSchema,
27
30
  }).unknown(false);
@@ -33,6 +36,9 @@ const updateVendorSchema = Joi.object({
33
36
  app_url: Joi.string().uri().max(512).optional(),
34
37
  app_pid: Joi.string().max(255).allow('').optional(),
35
38
  app_logo: Joi.string().max(512).allow('').optional(),
39
+ vendor_did: Joi.string()
40
+ .pattern(/^(did:abt:)?[1-9A-HJ-NP-Za-km-z]{37}$/)
41
+ .required(),
36
42
  status: Joi.string().valid('active', 'inactive').optional(),
37
43
  metadata: MetadataSchema,
38
44
  }).unknown(true);
@@ -139,6 +145,7 @@ async function createVendor(req: any, res: any) {
139
145
  name,
140
146
  description,
141
147
  app_url: appUrl,
148
+ vendor_did: vendorDid,
142
149
  metadata,
143
150
  app_pid: appPid,
144
151
  app_logo: appLogo,
@@ -158,6 +165,7 @@ async function createVendor(req: any, res: any) {
158
165
  name,
159
166
  description,
160
167
  app_url: appUrl,
168
+ vendor_did: vendorDid?.replace('did:abt:', '').trim(),
161
169
  status: status || 'active',
162
170
  app_pid: appPid,
163
171
  app_logo: appLogo,
@@ -195,6 +203,7 @@ async function updateVendor(req: any, res: any) {
195
203
  name,
196
204
  description,
197
205
  app_url: appUrl,
206
+ vendor_did: vendorDid,
198
207
  status,
199
208
  metadata,
200
209
  app_pid: appPid,
@@ -214,6 +223,7 @@ async function updateVendor(req: any, res: any) {
214
223
  name,
215
224
  description,
216
225
  app_url: appUrl,
226
+ vendor_did: vendorDid,
217
227
  status,
218
228
  metadata,
219
229
  app_pid: appPid,
@@ -276,7 +286,15 @@ async function getVendorStatusByVendorId(vendorId: string, orderId: string, isDe
276
286
  }
277
287
 
278
288
  const vendor = await ProductVendor.findByPk(vendorId);
279
- const url = vendor?.app_url ? joinURL(vendor.app_url, '/api/vendor/', isDetail ? 'orders' : 'status', orderId) : null;
289
+ const url = vendor?.app_url
290
+ ? joinURL(
291
+ vendor.app_url,
292
+ vendor.metadata?.mountPoint || '',
293
+ '/api/vendor/',
294
+ isDetail ? 'orders' : 'status',
295
+ orderId
296
+ )
297
+ : null;
280
298
 
281
299
  const { headers } = VendorAuth.signRequestWithHeaders({});
282
300
  const name = vendor?.name;
@@ -427,6 +445,7 @@ async function redirectToVendor(req: any, res: any) {
427
445
 
428
446
  const router = Router();
429
447
 
448
+ // FIXME: Authentication not yet added, awaiting implementation @Pengfei
430
449
  router.get('/order/:sessionId/status', validateParams(sessionIdParamSchema), getVendorFulfillmentStatus);
431
450
  router.get('/order/:sessionId/detail', validateParams(sessionIdParamSchema), getVendorFulfillmentDetail);
432
451
 
@@ -0,0 +1,20 @@
1
+ import { safeApplyColumnChanges, type Migration } from '../migrate';
2
+
3
+ export const up: Migration = async ({ context }) => {
4
+ // Add vendor_did column to product_vendors table
5
+ await safeApplyColumnChanges(context, {
6
+ product_vendors: [
7
+ {
8
+ name: 'vendor_did',
9
+ field: {
10
+ type: 'STRING',
11
+ allowNull: true,
12
+ },
13
+ },
14
+ ],
15
+ });
16
+ };
17
+
18
+ export const down: Migration = async ({ context }) => {
19
+ await context.removeColumn('product_vendors', 'vendor_did');
20
+ };
@@ -38,7 +38,7 @@ export class Payout extends Model<InferAttributes<Payout>, InferCreationAttribut
38
38
  commission_amount: string;
39
39
  };
40
40
 
41
- declare status: LiteralUnion<'pending' | 'paid' | 'failed' | 'canceled' | 'in_transit', string>;
41
+ declare status: LiteralUnion<'pending' | 'paid' | 'failed' | 'canceled' | 'in_transit' | 'deferred', string>;
42
42
 
43
43
  // retry logic
44
44
  declare failure_message?: string;
@@ -118,7 +118,7 @@ export class Payout extends Model<InferAttributes<Payout>, InferCreationAttribut
118
118
  allowNull: true,
119
119
  },
120
120
  status: {
121
- type: DataTypes.ENUM('paid', 'pending', 'failed', 'canceled', 'in_transit'),
121
+ type: DataTypes.ENUM('paid', 'pending', 'failed', 'canceled', 'in_transit', 'deferred'),
122
122
  allowNull: false,
123
123
  },
124
124
  failure_message: {
@@ -15,6 +15,7 @@ export class ProductVendor extends Model<InferAttributes<ProductVendor>, InferCr
15
15
  declare app_url: string;
16
16
  declare app_pid?: string;
17
17
  declare app_logo?: string;
18
+ declare vendor_did?: string;
18
19
 
19
20
  declare status: 'active' | 'inactive';
20
21
  declare metadata: Record<string, any>;
@@ -60,6 +61,10 @@ export class ProductVendor extends Model<InferAttributes<ProductVendor>, InferCr
60
61
  type: DataTypes.STRING(512),
61
62
  allowNull: true,
62
63
  },
64
+ vendor_did: {
65
+ type: DataTypes.STRING(255),
66
+ allowNull: true,
67
+ },
63
68
  status: {
64
69
  type: DataTypes.ENUM('active', 'inactive'),
65
70
  allowNull: false,
@@ -60,6 +60,7 @@ export class Product extends Model<InferAttributes<Product>, InferCreationAttrib
60
60
  commission_rate?: number;
61
61
  commission_type?: 'percentage' | 'fixed_amount';
62
62
  amount?: string;
63
+ commissionAmount?: string;
63
64
  custom_params?: Record<string, any>;
64
65
  }>;
65
66
 
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.20.9
17
+ version: 1.20.11
18
18
  logo: logo.png
19
19
  files:
20
20
  - dist
@@ -1,63 +1,63 @@
1
- # 多供应商发货系统完整实现方案
1
+ # Multi-Vendor Fulfillment System Complete Implementation
2
2
 
3
- ## 📋 项目概述
3
+ ## 📋 Project Overview
4
4
 
5
- 本文档总结了 Payment Kit 中多供应商发货系统的完整实现,包括架构设计、流程逻辑、技术实现和问题解决方案。
5
+ This document summarizes the complete implementation of the multi-vendor fulfillment system in Payment Kit, including architecture design, process logic, technical implementation, and problem-solving solutions.
6
6
 
7
- ## 🎯 系统目标
7
+ ## 🎯 System Goals
8
8
 
9
- ### 核心需求
9
+ ### Core Requirements
10
10
 
11
- 1. **多供应商支持**: 一个订单可以包含多个供应商的商品
12
- 2. **异步发货**: 不同供应商可以独立、并行发货
13
- 3. **状态协调**: 统一管理所有供应商的发货状态
14
- 4. **容错机制**: 处理发货失败、重试、超时等异常情况
15
- 5. **分成处理**: 所有供应商发货完成后,自动进行佣金分成
16
- 6. **退款保障**: 任何供应商失败即全额退款,确保用户权益
11
+ 1. **Multi-vendor Support**: A single order can contain products from multiple vendors
12
+ 2. **Asynchronous Fulfillment**: Different vendors can fulfill orders independently and in parallel
13
+ 3. **State Coordination**: Unified management of fulfillment status across all vendors
14
+ 4. **Fault Tolerance**: Handle fulfillment failures, retries, timeouts, and other exceptional situations
15
+ 5. **Commission Processing**: Automatic commission distribution after all vendors complete fulfillment
16
+ 6. **Refund Protection**: Full refund if any vendor fails, ensuring user rights
17
17
 
18
- ### 业务规则
18
+ ### Business Rules
19
19
 
20
- - **全部成功**: 所有供应商发货成功进行佣金分成
21
- - **任何失败**: 有任何供应商失败、超时或超过重试次数**对已成功的供应商发起退货请求**全额退款给用户
22
- - **超时时间**: 5分钟无响应视为超时
23
- - **重试次数**: 最多3次重试
24
- - **退货机制**: 对已完成发货的供应商发起退货请求
20
+ - **All Success**: All vendors fulfill successfully Process commission distribution
21
+ - **Any Failure**: If any vendor fails, times out, or exceeds retry limit **Initiate return requests for successful vendors** Full refund to user
22
+ - **Timeout Period**: 5 minutes of no response is considered timeout
23
+ - **Retry Limit**: Maximum 3 retries
24
+ - **Return Mechanism**: Initiate return requests for vendors that have completed fulfillment
25
25
 
26
- ## 🏗️ 系统架构
26
+ ## 🏗️ System Architecture
27
27
 
28
- ### 架构模式: 协调器模式 (Coordinator Pattern)
28
+ ### Architecture Pattern: Coordinator Pattern
29
29
 
30
- #### 🔄 **核心流程 (文字版)**
30
+ #### 🔄 **Core Process Flow (Text Version)**
31
31
 
32
32
  ```
33
33
  1. Payment Success
34
34
 
35
35
  2. vendor-commission.ts
36
- ├─ 检查供应商配置
37
- ├─ 无供应商直接冷钱包转账
38
- └─ 有供应商调用协调器
36
+ ├─ Check vendor configuration
37
+ ├─ No vendors Direct cold wallet transfer
38
+ └─ Has vendors Call coordinator
39
39
 
40
40
  3. vendor-fulfillment-coordinator.ts
41
- ├─ 启动发货流程
42
- ├─ 初始化 vendor_info 状态
43
- ├─ 为每个供应商创建发货任务
44
- └─ 被动等待通知
41
+ ├─ Start fulfillment process
42
+ ├─ Initialize vendor_info status
43
+ ├─ Create fulfillment tasks for each vendor
44
+ └─ Wait passively for notifications
45
45
 
46
- 4. vendor-fulfillment.ts (并行执行)
47
- ├─ 供应商A发货通知协调器
48
- ├─ 供应商B发货通知协调器
49
- ├─ 供应商C发货通知协调器
50
- └─ 重试机制 (最多3)
46
+ 4. vendor-fulfillment.ts (parallel execution)
47
+ ├─ Vendor A fulfills Notify coordinator
48
+ ├─ Vendor B fulfills Notify coordinator
49
+ ├─ Vendor C fulfills Notify coordinator
50
+ └─ Retry mechanism (max 3 times)
51
51
 
52
52
  5. vendor-fulfillment-coordinator.ts
53
- ├─ 接收所有通知
54
- ├─ 更新 vendor_info 状态 (事务+锁)
55
- ├─ 检查整体状态
56
- ├─ 全部成功分成 + 冷钱包
57
- └─ 任何失败 → 🔄 对已成功供应商发起退货请求全额退款
53
+ ├─ Receive all notifications
54
+ ├─ Update vendor_info status (transaction + lock)
55
+ ├─ Check overall status
56
+ ├─ All success Commission distribution + cold wallet
57
+ └─ Any failure → 🔄 Initiate return requests for successful vendors Full refund
58
58
  ```
59
59
 
60
- #### 📊 **详细架构图**
60
+ #### 📊 **Detailed Architecture Diagram**
61
61
 
62
62
  ```mermaid
63
63
  graph TD
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "payment-kit",
3
- "version": "1.20.9",
3
+ "version": "1.20.11",
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,8 +56,8 @@
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-react": "1.20.9",
60
- "@blocklet/payment-vendor": "1.20.9",
59
+ "@blocklet/payment-react": "1.20.11",
60
+ "@blocklet/payment-vendor": "1.20.11",
61
61
  "@blocklet/sdk": "^1.16.52-beta-20250912-112002-e3499e9c",
62
62
  "@blocklet/ui-react": "^3.1.40",
63
63
  "@blocklet/uploader": "^0.2.10",
@@ -126,7 +126,7 @@
126
126
  "devDependencies": {
127
127
  "@abtnode/types": "^1.16.52-beta-20250912-112002-e3499e9c",
128
128
  "@arcblock/eslint-config-ts": "^0.3.3",
129
- "@blocklet/payment-types": "1.20.9",
129
+ "@blocklet/payment-types": "1.20.11",
130
130
  "@types/cookie-parser": "^1.4.9",
131
131
  "@types/cors": "^2.8.19",
132
132
  "@types/debug": "^4.1.12",
@@ -173,5 +173,5 @@
173
173
  "parser": "typescript"
174
174
  }
175
175
  },
176
- "gitHead": "e89c0a9b6a3f3b7f7cb3cc0845b1775c1f587f6e"
176
+ "gitHead": "b69becabf1721f6238cf24004537bee1e4ade860"
177
177
  }
@@ -11,11 +11,11 @@ export default flat({
11
11
  add: 'Add more metadata',
12
12
  edit: 'Edit metadata',
13
13
  empty: 'No metadata',
14
- emptyTip: "You haven't added any metadata yet, you can add it",
14
+ emptyTip: "You haven't added any metadata yet. You can add it now",
15
15
  formMode: 'Switch to form mode',
16
16
  jsonMode: 'Switch to JSON mode',
17
17
  jsonPlaceholder: 'Enter JSON data...',
18
- invalidJson: 'Invalid JSON format, please check your input',
18
+ invalidJson: 'Invalid JSON format. Please check your input',
19
19
  formatJson: 'Format JSON',
20
20
  },
21
21
  price: 'Price',
@@ -26,7 +26,7 @@ export default flat({
26
26
  minLength: 'Min {len} characters',
27
27
  invalidCharacters: 'Invalid characters',
28
28
  latinOnly:
29
- 'At least one letter and cannot include Chinese characters and special characters such as <, >、"、’ or \\',
29
+ 'Must contain at least one letter and cannot include Chinese characters or special characters such as <, >, ", \' or \\',
30
30
  loading: 'Loading...',
31
31
  rechargeTime: 'Recharge Time',
32
32
  submit: 'Submit',
@@ -41,8 +41,8 @@ export default flat({
41
41
  quickStarts: 'Quick Starts',
42
42
  copy: 'Copy',
43
43
  copied: 'Copied',
44
- copySuccess: 'Copy Success',
45
- copyFailed: 'Copy Failed',
44
+ copySuccess: 'Copied successfully',
45
+ copyFailed: 'Copy failed',
46
46
  copyTip: 'Please copy manually',
47
47
  save: 'Save',
48
48
  cancel: 'Cancel',
@@ -254,7 +254,7 @@ export default flat({
254
254
  'Credit products are used to provide consumable credits to users, supporting both one-time purchase and package pricing models.',
255
255
  creditAmount: {
256
256
  label: 'Credit Amount',
257
- placeholder: 'Amount of credits users get when purchasing this price',
257
+ placeholder: 'Amount of credits users receive when purchasing this price',
258
258
  help: 'Leave empty for pay-per-unit pricing, enter a number for fixed package deals',
259
259
  description: 'Purchase quantity determines the amount of Credits received',
260
260
  },
@@ -367,14 +367,14 @@ export default flat({
367
367
  archiveTip: 'Archiving will hide this product from new purchases. Are you sure you want to archive this product?',
368
368
  unarchive: 'Unarchive product',
369
369
  unarchiveTip:
370
- 'Unarchiving will enable this product from new purchases. Are you sure you want to unarchive this product?',
370
+ 'Unarchiving will enable this product for new purchases. Are you sure you want to unarchive this product?',
371
371
  remove: 'Remove product',
372
372
  removeTip: 'Removing will hide this product from new purchases. Are you sure you want to remove this product?',
373
373
  archived: 'This product has been archived',
374
374
  archivedTip:
375
375
  "This product can't be added to new invoices, subscriptions, payment links, or pricing tables. Any existing subscriptions with this product remain active until canceled and any existing payment links or pricing tables are deactivated.",
376
376
  locked: 'This product is locked because at least one of its prices is used by a subscription or a payment.',
377
- currencyNotAligned: 'all prices must have the same currency settings',
377
+ currencyNotAligned: 'All prices must have the same currency settings',
378
378
  image: {
379
379
  label: 'Image',
380
380
  add: 'Add image',
@@ -431,7 +431,7 @@ export default flat({
431
431
  vendorConfig: {
432
432
  title: 'Vendor Configuration',
433
433
  add: 'Add Vendor',
434
- empty: 'No vendors configured. Click "Add Vendor" to configure vendor services.',
434
+ empty: 'No vendors configured. Click "Add Vendor" to configure vendor services',
435
435
  vendor: 'Vendor',
436
436
  vendorRequired: 'Vendor is required',
437
437
  productCode: 'Product Code',
@@ -508,7 +508,7 @@ export default flat({
508
508
  packageDesc: 'Price by number of units',
509
509
  graduated: 'Graduated Pricing',
510
510
  volume: 'Volume pricing',
511
- custom: 'Customer choose price',
511
+ custom: 'Customer chooses price',
512
512
  usageBased: 'Usage-based',
513
513
  creditMetered: 'Credit metered',
514
514
  },
@@ -520,7 +520,7 @@ export default flat({
520
520
  selectMeter: 'Select meter',
521
521
  pricingNote: 'For metered products, pricing is automatically based on usage tracked by the selected meter.',
522
522
  description:
523
- 'Credit metered billing requires a corresponding Credit consumption. If the Credit is consumed, the corresponding service will stop.',
523
+ 'Credit metered billing requires corresponding credit consumption. If credits are consumed, the corresponding service will stop.',
524
524
  },
525
525
  credit: {
526
526
  saveAsBasePrice: 'Set as Top-up Package',
@@ -557,7 +557,7 @@ export default flat({
557
557
  tip: '',
558
558
  },
559
559
  quantity: {
560
- tip: 'Quantity must be equal or greater than 0',
560
+ tip: 'Quantity must be equal to or greater than 0',
561
561
  },
562
562
  quantityAvailable: {
563
563
  label: 'Available quantity',
@@ -597,10 +597,10 @@ export default flat({
597
597
  afterPay: 'After payment',
598
598
  products: 'Products',
599
599
  addProduct: 'Add another product',
600
- requireBillingAddress: 'Collect customers billing addresses',
601
- requirePhoneNumber: 'Collect customers phone numbers',
600
+ requireBillingAddress: 'Collect customer billing addresses',
601
+ requirePhoneNumber: 'Collect customer phone numbers',
602
602
  allowPromotionCodes: 'Allow promotion codes',
603
- requireCrossSell: 'Require cross sell products selected if eligible',
603
+ requireCrossSell: 'Require cross-sell products to be selected if eligible',
604
604
  includeFreeTrial: 'Include a free trial',
605
605
  noStakeRequired: 'No stake required',
606
606
  showProductFeatures: 'Show product features',
@@ -684,7 +684,7 @@ export default flat({
684
684
  expiredUncapturedCharge: 'Expired uncaptured charge',
685
685
  amountRange: 'Refund amount must be between {min} and {max} {symbol}',
686
686
  amountHelper: 'Refund amount must be less than or equal to {max} {symbol}',
687
- required: 'please fill in the refund information',
687
+ required: 'Please fill in the refund information',
688
688
  empty: 'The current order has been fully refunded',
689
689
  },
690
690
  },
@@ -718,11 +718,11 @@ export default flat({
718
718
  },
719
719
  name: {
720
720
  label: 'Name',
721
- tip: 'Consumer facing',
721
+ tip: 'Customer-facing',
722
722
  },
723
723
  description: {
724
724
  label: 'Description',
725
- tip: 'Not consumer facing',
725
+ tip: 'Not customer-facing',
726
726
  },
727
727
  stripe: {
728
728
  dashboard: {
@@ -749,11 +749,11 @@ export default flat({
749
749
  },
750
750
  api_host: {
751
751
  label: 'API Host',
752
- tip: 'The graphql endpoint to send transaction to',
752
+ tip: 'The GraphQL endpoint to send transactions to',
753
753
  },
754
754
  explorer_host: {
755
755
  label: 'Explorer Host',
756
- tip: 'The webapp endpoint to view transaction details',
756
+ tip: 'The web app endpoint to view transaction details',
757
757
  },
758
758
  },
759
759
  ethereum: {
@@ -763,11 +763,11 @@ export default flat({
763
763
  },
764
764
  api_host: {
765
765
  label: 'RPC Endpoint',
766
- tip: 'The RPC endpoint to send transaction to',
766
+ tip: 'The RPC endpoint to send transactions to',
767
767
  },
768
768
  explorer_host: {
769
769
  label: 'Explorer Host',
770
- tip: 'The webapp endpoint to view transaction details',
770
+ tip: 'The web app endpoint to view transaction details',
771
771
  },
772
772
  native_symbol: {
773
773
  label: 'Native Symbol',
@@ -775,7 +775,7 @@ export default flat({
775
775
  },
776
776
  confirmation: {
777
777
  label: 'Confirmation Count',
778
- tip: 'How many blocks since transaction execution',
778
+ tip: 'Number of blocks required since transaction execution',
779
779
  },
780
780
  },
781
781
  evm: {
@@ -792,11 +792,11 @@ export default flat({
792
792
  },
793
793
  api_host: {
794
794
  label: 'RPC Endpoint',
795
- tip: 'The RPC endpoint to send transaction to',
795
+ tip: 'The RPC endpoint to send transactions to',
796
796
  },
797
797
  explorer_host: {
798
798
  label: 'Explorer Host',
799
- tip: 'The webapp endpoint to view transaction details',
799
+ tip: 'The web app endpoint to view transaction details',
800
800
  },
801
801
  native_symbol: {
802
802
  label: 'Native Symbol',
@@ -804,7 +804,7 @@ export default flat({
804
804
  },
805
805
  confirmation: {
806
806
  label: 'Confirmation Count',
807
- tip: 'How many blocks since transaction execution',
807
+ tip: 'Number of blocks required since transaction execution',
808
808
  },
809
809
  },
810
810
  },
@@ -905,6 +905,9 @@ export default flat({
905
905
  appUrlRequired: 'App URL is required',
906
906
  appUrlInvalid: 'Please enter a valid URL starting with http:// or https://',
907
907
  appUrlHelp: 'The base URL of the vendor application',
908
+ vendorDid: 'Vendor DID',
909
+ vendorDidInvalid: 'Please enter a valid DID',
910
+ vendorDidHelp: 'Optional DID address for the vendor',
908
911
  webhookPath: 'Webhook Path',
909
912
  webhookPathInvalid: 'Please enter a valid path starting with /',
910
913
  webhookPathHelp: 'Optional webhook callback path (e.g., /webhooks/status)',
@@ -881,8 +881,11 @@ export default flat({
881
881
  displayNameHelp: '此数据会展示到付款成功后的安装界面',
882
882
  appUrl: '应用地址',
883
883
  appUrlRequired: '应用地址是必填项',
884
- appUrlInvalid: '请输入以http://或https://开头的有效URL',
885
- appUrlHelp: '供应商应用的基础URL',
884
+ appUrlInvalid: '请输入有效的 URL,以 http:// 或 https:// 开头',
885
+ appUrlHelp: '供应商应用的基础 URL 地址',
886
+ vendorDid: '供应商 DID',
887
+ vendorDidInvalid: '请输入有效的 DID',
888
+ vendorDidHelp: '供应商的可选 DID 地址',
886
889
  webhookPath: 'Webhook路径',
887
890
  webhookPathInvalid: '请输入以/开头的有效路径',
888
891
  webhookPathHelp: '可选的webhook回调路径(如:/webhooks/status)',
@@ -30,6 +30,7 @@ interface Vendor {
30
30
  name: string;
31
31
  description: string;
32
32
  app_url: string;
33
+ vendor_did?: string;
33
34
  status: 'active' | 'inactive';
34
35
  metadata: Record<string, any>;
35
36
  created_at: string;
@@ -49,6 +50,7 @@ interface VendorFormData {
49
50
  name: string;
50
51
  description: string;
51
52
  app_url: string;
53
+ vendor_did?: string;
52
54
  status: 'active' | 'inactive';
53
55
  metadata: Array<{ key: string; value: string }>;
54
56
  app_pid?: string;
@@ -82,6 +84,7 @@ export default function VendorCreate({
82
84
  name: '',
83
85
  description: '',
84
86
  app_url: '',
87
+ vendor_did: '',
85
88
  status: 'inactive' as const,
86
89
  metadata: [{ key: 'blockletMetaUrl', value: '' }],
87
90
  app_pid: '',
@@ -114,6 +117,16 @@ export default function VendorCreate({
114
117
  }
115
118
  };
116
119
 
120
+ const validateDid = (did: string | undefined) => {
121
+ if (!did) return true; // DID 是可选的
122
+ // DID 格式验证
123
+ const didPattern = /^(did:abt:)?[1-9A-HJ-NP-Za-km-z]{37}$/;
124
+ if (!didPattern.test(did.trim())) {
125
+ return t('admin.vendor.vendorDidInvalid');
126
+ }
127
+ return true;
128
+ };
129
+
117
130
  const onSubmit = async (data: VendorFormData) => {
118
131
  try {
119
132
  setLoading(true);
@@ -144,6 +157,7 @@ export default function VendorCreate({
144
157
 
145
158
  const submitData = {
146
159
  ...restData,
160
+ vendor_did: restData.vendor_did?.replace('did:abt:', '').trim(),
147
161
  metadata: metadataObj,
148
162
  };
149
163
 
@@ -167,6 +181,10 @@ export default function VendorCreate({
167
181
  // 从响应中获取appPid和appLogo
168
182
  const blockletInfo = await response.json();
169
183
  if (blockletInfo) {
184
+ const component = blockletInfo.componentMountPoints?.find((x: any) => x.did === submitData.vendor_did);
185
+ if (component && !['', '/'].includes(component.mountPoint) && submitData.metadata) {
186
+ submitData.metadata.mountPoint = component.mountPoint;
187
+ }
170
188
  submitData.app_pid = blockletInfo.pid || blockletInfo.appPid;
171
189
  submitData.app_logo = blockletInfo.logo || blockletInfo.appLogo;
172
190
  }
@@ -307,6 +325,24 @@ export default function VendorCreate({
307
325
  )}
308
326
  />
309
327
 
328
+ <Controller
329
+ name="vendor_did"
330
+ control={control}
331
+ rules={{
332
+ required: t('admin.vendor.vendorDidRequired'),
333
+ validate: validateDid,
334
+ }}
335
+ render={({ field }) => (
336
+ <TextField
337
+ {...field}
338
+ label={t('admin.vendor.vendorDid')}
339
+ fullWidth
340
+ error={!!errors.vendor_did}
341
+ helperText={errors.vendor_did?.message || t('admin.vendor.vendorDidHelp')}
342
+ />
343
+ )}
344
+ />
345
+
310
346
  <MetadataForm title={t('common.metadata.label')} />
311
347
 
312
348
  <Controller