payment-kit 1.20.9 → 1.20.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/api/src/integrations/stripe/handlers/index.ts +10 -2
- package/api/src/libs/vendor-util/adapters/launcher-adapter.ts +17 -10
- package/api/src/libs/vendor-util/fulfillment.ts +15 -121
- package/api/src/queues/payment.ts +1 -5
- package/api/src/queues/vendors/commission.ts +16 -14
- package/api/src/queues/vendors/fulfillment-coordinator.ts +2 -2
- package/api/src/queues/vendors/status-check.ts +11 -6
- package/api/src/routes/vendor.ts +20 -1
- package/api/src/store/migrations/20250916-add-vendor-did.ts +20 -0
- package/api/src/store/models/payout.ts +2 -2
- package/api/src/store/models/product-vendor.ts +5 -0
- package/api/src/store/models/product.ts +1 -0
- package/blocklet.yml +1 -1
- package/package.json +5 -5
- package/src/locales/en.tsx +3 -0
- package/src/locales/zh.tsx +5 -2
- package/src/pages/admin/products/vendors/create.tsx +36 -0
|
@@ -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':
|
|
@@ -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(
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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(
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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 {
|
|
8
|
-
import {
|
|
9
|
-
import
|
|
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:
|
|
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:
|
|
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 =
|
|
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: {
|
|
@@ -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('
|
|
170
|
+
events.on('invoice.paid', async (invoice) => {
|
|
171
171
|
try {
|
|
172
|
-
const {
|
|
173
|
-
const exist = await vendorCommissionQueue.get(id);
|
|
172
|
+
const paymentIntent = await PaymentIntent.findOne({ where: { invoice_id: invoice.id } });
|
|
174
173
|
|
|
175
|
-
if (!
|
|
176
|
-
logger.
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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
|
-
|
|
186
|
-
|
|
185
|
+
|
|
186
|
+
vendorCommissionQueue.push({
|
|
187
187
|
id,
|
|
188
|
-
|
|
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.
|
|
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.
|
|
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
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
}
|
package/api/src/routes/vendor.ts
CHANGED
|
@@ -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
|
|
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,
|
package/blocklet.yml
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "payment-kit",
|
|
3
|
-
"version": "1.20.
|
|
3
|
+
"version": "1.20.10",
|
|
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.
|
|
60
|
-
"@blocklet/payment-vendor": "1.20.
|
|
59
|
+
"@blocklet/payment-react": "1.20.10",
|
|
60
|
+
"@blocklet/payment-vendor": "1.20.10",
|
|
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.
|
|
129
|
+
"@blocklet/payment-types": "1.20.10",
|
|
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": "
|
|
176
|
+
"gitHead": "1659d63120ced92167ec8681e51db96801b910ec"
|
|
177
177
|
}
|
package/src/locales/en.tsx
CHANGED
|
@@ -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)',
|
package/src/locales/zh.tsx
CHANGED
|
@@ -881,8 +881,11 @@ export default flat({
|
|
|
881
881
|
displayNameHelp: '此数据会展示到付款成功后的安装界面',
|
|
882
882
|
appUrl: '应用地址',
|
|
883
883
|
appUrlRequired: '应用地址是必填项',
|
|
884
|
-
appUrlInvalid: '
|
|
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
|