payment-kit 1.20.16 → 1.20.18
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/.env +1 -0
- package/api/src/index.ts +2 -0
- package/api/src/libs/vendor-util/adapters/didnames-adapter.ts +412 -0
- package/api/src/libs/vendor-util/adapters/factory.ts +26 -1
- package/api/src/libs/vendor-util/adapters/launcher-adapter.ts +110 -22
- package/api/src/libs/vendor-util/adapters/types.ts +4 -0
- package/api/src/libs/vendor-util/adapters/util.ts +7 -0
- package/api/src/libs/vendor-util/fulfillment.ts +7 -2
- package/api/src/queues/vendors/fulfillment-coordinator.ts +193 -7
- package/api/src/routes/vendor.ts +59 -74
- package/blocklet.yml +1 -1
- package/package.json +7 -5
- package/src/locales/en.tsx +1 -0
- package/src/locales/zh.tsx +1 -0
- package/src/pages/admin/products/vendors/create.tsx +3 -2
package/.env
CHANGED
package/api/src/index.ts
CHANGED
|
@@ -37,6 +37,7 @@ import { startUploadBillingInfoListener } from './queues/space';
|
|
|
37
37
|
import { startSubscriptionQueue } from './queues/subscription';
|
|
38
38
|
import { startVendorCommissionQueue } from './queues/vendors/commission';
|
|
39
39
|
import { startVendorFulfillmentQueue } from './queues/vendors/fulfillment';
|
|
40
|
+
import { startCoordinatedFulfillmentQueue } from './queues/vendors/fulfillment-coordinator';
|
|
40
41
|
import routes from './routes';
|
|
41
42
|
import autoRechargeAuthorizationHandlers from './routes/connect/auto-recharge-auth';
|
|
42
43
|
import changePaymentHandlers from './routes/connect/change-payment';
|
|
@@ -131,6 +132,7 @@ export const server = app.listen(port, (err?: any) => {
|
|
|
131
132
|
startPayoutQueue().then(() => logger.info('payout queue started'));
|
|
132
133
|
startVendorCommissionQueue().then(() => logger.info('vendor commission queue started'));
|
|
133
134
|
startVendorFulfillmentQueue().then(() => logger.info('vendor fulfillment queue started'));
|
|
135
|
+
startCoordinatedFulfillmentQueue().then(() => logger.info('coordinated fulfillment queue started'));
|
|
134
136
|
startCheckoutSessionQueue().then(() => logger.info('checkoutSession queue started'));
|
|
135
137
|
startNotificationQueue().then(() => logger.info('notification queue started'));
|
|
136
138
|
startRefundQueue().then(() => logger.info('refund queue started'));
|
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
import { VendorAuth } from '@blocklet/payment-vendor';
|
|
2
|
+
import { toBase58 } from '@ocap/util';
|
|
3
|
+
import stableStringify from 'json-stable-stringify';
|
|
4
|
+
import { v4 as uuidV4 } from 'uuid';
|
|
5
|
+
|
|
6
|
+
import { ProductVendor } from '../../../store/models';
|
|
7
|
+
import { wallet } from '../../auth';
|
|
8
|
+
import logger from '../../logger';
|
|
9
|
+
import {
|
|
10
|
+
CheckOrderStatusParams,
|
|
11
|
+
CheckOrderStatusResult,
|
|
12
|
+
FulfillOrderParams,
|
|
13
|
+
FulfillOrderResult,
|
|
14
|
+
ReturnRequestParams,
|
|
15
|
+
ReturnRequestResult,
|
|
16
|
+
VendorAdapter,
|
|
17
|
+
VendorConfig,
|
|
18
|
+
} from './types';
|
|
19
|
+
import { formatVendorUrl } from './util';
|
|
20
|
+
|
|
21
|
+
export class DidnamesAdapter implements VendorAdapter {
|
|
22
|
+
private vendorConfig: VendorConfig | null = null;
|
|
23
|
+
private vendorKey: string;
|
|
24
|
+
|
|
25
|
+
constructor(vendorInfo: VendorConfig | string) {
|
|
26
|
+
if (typeof vendorInfo === 'string') {
|
|
27
|
+
this.vendorKey = vendorInfo;
|
|
28
|
+
this.vendorConfig = null;
|
|
29
|
+
} else {
|
|
30
|
+
this.vendorKey = vendorInfo.id;
|
|
31
|
+
this.vendorConfig = vendorInfo;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Generate random subdomain for documentation site
|
|
37
|
+
* Format: [prefix][separator][timestamp-suffix] or pure timestamp
|
|
38
|
+
* Configurable length, prefix, separator with timestamp-based uniqueness
|
|
39
|
+
*/
|
|
40
|
+
private generateRandomSubdomain(
|
|
41
|
+
options: {
|
|
42
|
+
totalLength?: number; // Total subdomain length (default: 9)
|
|
43
|
+
prefix?: string; // Prefix (default: 'doc')
|
|
44
|
+
usePrefix?: boolean; // Whether to use prefix (default: true)
|
|
45
|
+
separator?: string; // Separator between prefix and suffix (default: '-')
|
|
46
|
+
} = {}
|
|
47
|
+
): string {
|
|
48
|
+
const { totalLength = 8, prefix = 'doc', usePrefix = true, separator = '-' } = options;
|
|
49
|
+
|
|
50
|
+
if (usePrefix) {
|
|
51
|
+
// With prefix: 'doc' + separator + timestamp suffix
|
|
52
|
+
const prefixWithSeparatorLength = prefix.length + separator.length;
|
|
53
|
+
const suffixLength = totalLength - prefixWithSeparatorLength;
|
|
54
|
+
if (suffixLength <= 0) {
|
|
55
|
+
throw new Error(
|
|
56
|
+
`Total length (${totalLength}) must be greater than prefix + separator length (${prefixWithSeparatorLength})`
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const timestamp = Date.now();
|
|
61
|
+
const timeStr = timestamp.toString(36);
|
|
62
|
+
let suffix = timeStr.slice(-suffixLength);
|
|
63
|
+
|
|
64
|
+
// Pad with random chars if timestamp is shorter than needed
|
|
65
|
+
while (suffix.length < suffixLength) {
|
|
66
|
+
suffix = Math.floor(Math.random() * 36).toString(36) + suffix;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return `${prefix}${separator}${suffix}`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Pure timestamp-based without prefix
|
|
73
|
+
const timestamp = Date.now();
|
|
74
|
+
const timeStr = timestamp.toString(36);
|
|
75
|
+
let result = timeStr.slice(-totalLength);
|
|
76
|
+
|
|
77
|
+
// Pad with random chars if needed
|
|
78
|
+
while (result.length < totalLength) {
|
|
79
|
+
result = Math.floor(Math.random() * 36).toString(36) + result;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return result;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Generate bindDomainCap (binding capability) for domain authorization
|
|
87
|
+
*/
|
|
88
|
+
private generateBindCap({ domain, checkoutSessionId }: { domain: string; checkoutSessionId: string }): any {
|
|
89
|
+
const now = Math.floor(Date.now() / 1000);
|
|
90
|
+
const expireInMinutes = 30;
|
|
91
|
+
|
|
92
|
+
const cap = {
|
|
93
|
+
domain,
|
|
94
|
+
checkoutSessionId,
|
|
95
|
+
nbf: now, // not before
|
|
96
|
+
exp: now + expireInMinutes * 60,
|
|
97
|
+
nonce: uuidV4(),
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const signature = toBase58(wallet.sign(stableStringify(cap) || ''));
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
cap,
|
|
104
|
+
sig: signature,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async getVendorConfig(): Promise<VendorConfig> {
|
|
109
|
+
if (this.vendorConfig === null) {
|
|
110
|
+
this.vendorConfig = await ProductVendor.findOne({ where: { vendor_key: this.vendorKey } });
|
|
111
|
+
if (!this.vendorConfig) {
|
|
112
|
+
throw new Error(`Vendor not found: ${this.vendorKey}`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return this.vendorConfig;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async fulfillOrder(params: FulfillOrderParams): Promise<FulfillOrderResult> {
|
|
119
|
+
logger.info('Creating didnames order with domain binding', {
|
|
120
|
+
checkoutSessionId: params.checkoutSessionId,
|
|
121
|
+
productCode: params.productCode,
|
|
122
|
+
customerId: params.customerId,
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
const vendorConfig = await this.getVendorConfig();
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
const rootDomain = vendorConfig.metadata?.rootDomain;
|
|
129
|
+
if (!rootDomain) {
|
|
130
|
+
throw new Error('missing required metadata in didnames vendor: rootDomain');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
logger.info('didnames vendor rootDomain', { rootDomain });
|
|
134
|
+
|
|
135
|
+
const subdomain = this.generateRandomSubdomain({ totalLength: 8 });
|
|
136
|
+
const domain = `${subdomain}.${rootDomain}`;
|
|
137
|
+
|
|
138
|
+
const { checkoutSessionId } = params;
|
|
139
|
+
const bindDomainCap = this.generateBindCap({
|
|
140
|
+
domain,
|
|
141
|
+
checkoutSessionId,
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
params.deliveryParams.customParams = {
|
|
145
|
+
...params.deliveryParams.customParams,
|
|
146
|
+
years: 1,
|
|
147
|
+
whoisPrivacy: true,
|
|
148
|
+
subdomain,
|
|
149
|
+
rootDomain,
|
|
150
|
+
domain,
|
|
151
|
+
checkoutSessionId,
|
|
152
|
+
bindDomainCap,
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const orderData = {
|
|
156
|
+
checkoutSessionId: params.checkoutSessionId,
|
|
157
|
+
description: params.description,
|
|
158
|
+
userInfo: params.userInfo,
|
|
159
|
+
deliveryParams: params.deliveryParams,
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
const url = formatVendorUrl(vendorConfig, '/api/vendor/deliveries');
|
|
163
|
+
logger.info('submitting domain delivery to DID Names', {
|
|
164
|
+
subdomain,
|
|
165
|
+
url,
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
const { headers, body } = VendorAuth.signRequestWithHeaders(orderData);
|
|
169
|
+
|
|
170
|
+
const response = await fetch(url, {
|
|
171
|
+
method: 'POST',
|
|
172
|
+
headers,
|
|
173
|
+
body,
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
if (!response.ok) {
|
|
177
|
+
const errorBody = await response.text();
|
|
178
|
+
logger.error('DID Names API error', {
|
|
179
|
+
url,
|
|
180
|
+
status: response.status,
|
|
181
|
+
statusText: response.statusText,
|
|
182
|
+
body: errorBody,
|
|
183
|
+
});
|
|
184
|
+
throw new Error(`DID Names API error: ${response.status} ${response.statusText}`);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const didNamesResult = await response.json();
|
|
188
|
+
|
|
189
|
+
const result: FulfillOrderResult = {
|
|
190
|
+
orderId: didNamesResult.orderId,
|
|
191
|
+
status: didNamesResult.status || 'pending',
|
|
192
|
+
serviceUrl: `https://${subdomain}.${rootDomain}`, // User expects to access via custom domain
|
|
193
|
+
estimatedTime: didNamesResult.estimatedTime || 300,
|
|
194
|
+
message: didNamesResult.message || 'Domain binding order created successfully',
|
|
195
|
+
vendorOrderId: didNamesResult.orderId,
|
|
196
|
+
progress: didNamesResult.progress || 0,
|
|
197
|
+
orderDetails: {
|
|
198
|
+
productCode: params.productCode,
|
|
199
|
+
customerId: params.customerId,
|
|
200
|
+
amount: params.amount,
|
|
201
|
+
currency: params.currency,
|
|
202
|
+
quantity: params.quantity,
|
|
203
|
+
invoiceId: params.invoiceId,
|
|
204
|
+
customParams: {
|
|
205
|
+
...params.customParams,
|
|
206
|
+
subdomain,
|
|
207
|
+
rootDomain,
|
|
208
|
+
domain: didNamesResult.domain || domain,
|
|
209
|
+
nftDid: didNamesResult.nftDid,
|
|
210
|
+
sessionId: params.customParams?.checkoutSessionId,
|
|
211
|
+
bindDomainCap,
|
|
212
|
+
},
|
|
213
|
+
},
|
|
214
|
+
installationInfo: {
|
|
215
|
+
appId: didNamesResult.appId || `app_${subdomain.replace(/\./g, '_')}`,
|
|
216
|
+
appUrl: `https://${subdomain}.${rootDomain}`, // Custom domain URL
|
|
217
|
+
adminUrl: didNamesResult.adminUrl || `https://${subdomain}.${rootDomain}/admin`,
|
|
218
|
+
status: didNamesResult.installationStatus || didNamesResult.status || 'installing',
|
|
219
|
+
estimatedCompletionTime:
|
|
220
|
+
didNamesResult.estimatedCompletionTime || new Date(Date.now() + 300000).toISOString(),
|
|
221
|
+
},
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
logger.info('Didnames order created successfully', {
|
|
225
|
+
orderId: result.orderId,
|
|
226
|
+
status: result.status,
|
|
227
|
+
subdomain,
|
|
228
|
+
serviceUrl: result.serviceUrl,
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
return result;
|
|
232
|
+
} catch (error: any) {
|
|
233
|
+
logger.error('Failed to create didnames order', {
|
|
234
|
+
error,
|
|
235
|
+
productCode: params.productCode,
|
|
236
|
+
customerId: params.customerId,
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
throw error;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
async requestReturn(params: ReturnRequestParams): Promise<ReturnRequestResult> {
|
|
244
|
+
logger.info('Requesting return for Didnames order', {
|
|
245
|
+
orderId: params.orderId,
|
|
246
|
+
reason: params.reason,
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
const vendorConfig = await this.getVendorConfig();
|
|
250
|
+
|
|
251
|
+
try {
|
|
252
|
+
const returnRequest = {
|
|
253
|
+
orderId: params.orderId,
|
|
254
|
+
vendorOrderId: params.vendorOrderId,
|
|
255
|
+
reason: params.reason,
|
|
256
|
+
customParams: params.customParams,
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
const { headers, body } = VendorAuth.signRequestWithHeaders(returnRequest);
|
|
260
|
+
const url = formatVendorUrl(vendorConfig, '/api/vendor/return');
|
|
261
|
+
logger.info('submitting domain return to DID Names', {
|
|
262
|
+
url,
|
|
263
|
+
});
|
|
264
|
+
const response = await fetch(url, {
|
|
265
|
+
method: 'POST',
|
|
266
|
+
headers,
|
|
267
|
+
body,
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
if (!response.ok) {
|
|
271
|
+
const errorBody = await response.text();
|
|
272
|
+
logger.error('domain return to DID Names API error', {
|
|
273
|
+
url,
|
|
274
|
+
status: response.status,
|
|
275
|
+
statusText: response.statusText,
|
|
276
|
+
body: errorBody,
|
|
277
|
+
});
|
|
278
|
+
throw new Error(`DID Names API error: ${response.status} ${response.statusText}`);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const didNamesResult = await response.json();
|
|
282
|
+
|
|
283
|
+
logger.info('domain return to DID Names processed', {
|
|
284
|
+
url,
|
|
285
|
+
orderId: params.orderId,
|
|
286
|
+
status: didNamesResult.status,
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
return {
|
|
290
|
+
status: didNamesResult.status || 'requested',
|
|
291
|
+
message: didNamesResult.message || 'Domain unbinding requested',
|
|
292
|
+
};
|
|
293
|
+
} catch (error: any) {
|
|
294
|
+
logger.error('Failed to process return request', {
|
|
295
|
+
error: error.message,
|
|
296
|
+
orderId: params.orderId,
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
throw error;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
async checkOrderStatus(params: CheckOrderStatusParams): Promise<CheckOrderStatusResult> {
|
|
304
|
+
logger.info('Checking Didnames order status', {
|
|
305
|
+
orderId: params.orderId,
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
try {
|
|
309
|
+
const vendorConfig = await this.getVendorConfig();
|
|
310
|
+
const url = formatVendorUrl(vendorConfig, `/api/vendor/orders/${params.orderId}/status`);
|
|
311
|
+
|
|
312
|
+
const response = await fetch(url, {
|
|
313
|
+
method: 'GET',
|
|
314
|
+
headers: {
|
|
315
|
+
'Content-Type': 'application/json',
|
|
316
|
+
},
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
if (!response.ok) {
|
|
320
|
+
logger.warn('DID Names order status check failed', {
|
|
321
|
+
orderId: params.orderId,
|
|
322
|
+
status: response.status,
|
|
323
|
+
});
|
|
324
|
+
return { status: 'processing' };
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const orderStatus = await response.json();
|
|
328
|
+
let status: 'processing' | 'completed' | 'failed' = 'processing';
|
|
329
|
+
|
|
330
|
+
switch (orderStatus.status) {
|
|
331
|
+
case 'completed':
|
|
332
|
+
case 'active':
|
|
333
|
+
case 'failed':
|
|
334
|
+
case 'rejected':
|
|
335
|
+
case 'error':
|
|
336
|
+
status = 'failed';
|
|
337
|
+
break;
|
|
338
|
+
default:
|
|
339
|
+
status = 'processing';
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
logger.info('Didnames order status checked', {
|
|
343
|
+
orderId: params.orderId,
|
|
344
|
+
status,
|
|
345
|
+
didNamesStatus: orderStatus.status,
|
|
346
|
+
domain: params.domain,
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
return { status };
|
|
350
|
+
} catch (error: any) {
|
|
351
|
+
logger.error('Failed to check didnames order status', {
|
|
352
|
+
error: error.message,
|
|
353
|
+
orderId: params.orderId,
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
throw error;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
async getOrder(vendor: ProductVendor, orderId: string): Promise<any> {
|
|
361
|
+
const url = formatVendorUrl(vendor, `/api/vendor/orders/${orderId}`);
|
|
362
|
+
|
|
363
|
+
const { headers } = VendorAuth.signRequestWithHeaders({});
|
|
364
|
+
const response = await fetch(url, { method: 'GET', headers });
|
|
365
|
+
|
|
366
|
+
if (!response.ok) {
|
|
367
|
+
logger.error('Failed to get didnames order', {
|
|
368
|
+
url,
|
|
369
|
+
orderId,
|
|
370
|
+
status: response.status,
|
|
371
|
+
statusText: response.statusText,
|
|
372
|
+
});
|
|
373
|
+
throw new Error(`Failed to get order: ${response.status} ${response.statusText}`);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const data = await response.json();
|
|
377
|
+
return data;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
async getOrderStatus(vendor: ProductVendor, orderId: string): Promise<any> {
|
|
381
|
+
const url = formatVendorUrl(vendor, `/api/vendor/status/${orderId}`);
|
|
382
|
+
|
|
383
|
+
logger.info('getting didnames order status', {
|
|
384
|
+
url,
|
|
385
|
+
orderId,
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
const { headers } = VendorAuth.signRequestWithHeaders({});
|
|
389
|
+
const response = await fetch(url, { method: 'GET', headers });
|
|
390
|
+
|
|
391
|
+
logger.info('didnames order status response', {
|
|
392
|
+
url,
|
|
393
|
+
orderId,
|
|
394
|
+
status: response.status,
|
|
395
|
+
statusText: response.statusText,
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
if (!response.ok) {
|
|
399
|
+
logger.error('Failed to get didnames order status', {
|
|
400
|
+
url,
|
|
401
|
+
orderId,
|
|
402
|
+
status: response.status,
|
|
403
|
+
statusText: response.statusText,
|
|
404
|
+
});
|
|
405
|
+
throw new Error(`Failed to get order status: ${response.status} ${response.statusText}`);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const data = await response.json();
|
|
409
|
+
|
|
410
|
+
return data;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { BN } from '@ocap/util';
|
|
2
|
+
|
|
3
|
+
import { DidnamesAdapter } from './didnames-adapter';
|
|
2
4
|
import { LauncherAdapter } from './launcher-adapter';
|
|
3
5
|
import { VendorAdapter } from './types';
|
|
4
6
|
import { ProductVendor } from '../../../store/models';
|
|
@@ -26,16 +28,39 @@ export class VendorAdapterFactory {
|
|
|
26
28
|
if (!vendorConfig) {
|
|
27
29
|
throw new Error(`Vendor not found: ${vendorKey}`);
|
|
28
30
|
}
|
|
31
|
+
|
|
29
32
|
switch (vendorConfig.vendor_type) {
|
|
30
33
|
case 'launcher':
|
|
31
34
|
return new LauncherAdapter(vendorConfig);
|
|
35
|
+
case 'didnames':
|
|
36
|
+
return new DidnamesAdapter(vendorConfig);
|
|
32
37
|
default:
|
|
33
38
|
throw new Error(`Unsupported vendor: ${vendorConfig.vendor_type}`);
|
|
34
39
|
}
|
|
35
40
|
}
|
|
36
41
|
|
|
42
|
+
/**
|
|
43
|
+
* Create coordinated adapters for domain binding workflow
|
|
44
|
+
* @param vendorKeys Array of vendor keys in execution order
|
|
45
|
+
*/
|
|
46
|
+
static async createCoordinated(vendorKeys: string[]): Promise<{
|
|
47
|
+
didnamesAdapter?: DidnamesAdapter;
|
|
48
|
+
launcherAdapter?: LauncherAdapter;
|
|
49
|
+
}> {
|
|
50
|
+
const adapters: { [key: string]: VendorAdapter } = {};
|
|
51
|
+
|
|
52
|
+
for (const vendorKey of vendorKeys) {
|
|
53
|
+
adapters[vendorKey] = await this.create(vendorKey); // eslint-disable-line no-await-in-loop
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
didnamesAdapter: adapters.didnames as DidnamesAdapter,
|
|
58
|
+
launcherAdapter: adapters.launcher as LauncherAdapter,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
37
62
|
static getSupportedVendors(): string[] {
|
|
38
|
-
return ['launcher'];
|
|
63
|
+
return ['launcher', 'didnames'];
|
|
39
64
|
}
|
|
40
65
|
|
|
41
66
|
static isSupported(vendorKey: string): boolean {
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { Auth as VendorAuth } from '@blocklet/payment-vendor';
|
|
2
2
|
|
|
3
|
-
import { joinURL } from 'ufo';
|
|
4
3
|
import { ProductVendor } from '../../../store/models';
|
|
4
|
+
import dayjs from '../../dayjs';
|
|
5
5
|
import logger from '../../logger';
|
|
6
|
+
import { formatToShortUrl } from '../../url';
|
|
6
7
|
import { api } from '../../util';
|
|
7
8
|
import {
|
|
8
9
|
CheckOrderStatusParams,
|
|
@@ -14,6 +15,51 @@ import {
|
|
|
14
15
|
VendorAdapter,
|
|
15
16
|
VendorConfig,
|
|
16
17
|
} from './types';
|
|
18
|
+
import { formatVendorUrl } from './util';
|
|
19
|
+
|
|
20
|
+
const doRequestVendorData = (vendor: ProductVendor, orderId: string, url: string) => {
|
|
21
|
+
const { headers } = VendorAuth.signRequestWithHeaders({});
|
|
22
|
+
const name = vendor?.name;
|
|
23
|
+
const key = vendor?.vendor_key;
|
|
24
|
+
|
|
25
|
+
return fetch(url, { headers })
|
|
26
|
+
.then(async (r) => {
|
|
27
|
+
const data = await r.json();
|
|
28
|
+
if (r.status !== 200) {
|
|
29
|
+
logger.error('vendor status fetch failed', {
|
|
30
|
+
vendorId: vendor.id,
|
|
31
|
+
vendorKey: vendor.vendor_key,
|
|
32
|
+
orderId,
|
|
33
|
+
status: r.status,
|
|
34
|
+
url,
|
|
35
|
+
data,
|
|
36
|
+
});
|
|
37
|
+
throw new Error(
|
|
38
|
+
`vendor status fetch failed, vendor: ${vendor.vendor_key}, orderId: ${orderId}, status: ${r.status}, url: ${url}`
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
if (!data.dashboardUrl) {
|
|
42
|
+
return {
|
|
43
|
+
...data,
|
|
44
|
+
name,
|
|
45
|
+
key,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const validUntil = dayjs().add(20, 'minutes').format('YYYY-MM-DDTHH:mm:ss+00:00');
|
|
50
|
+
const maxVisits = 5;
|
|
51
|
+
const homeUrl = await formatToShortUrl({ url: data.homeUrl, maxVisits, validUntil });
|
|
52
|
+
const dashboardUrl = await formatToShortUrl({ url: data.dashboardUrl, maxVisits, validUntil });
|
|
53
|
+
return {
|
|
54
|
+
...data,
|
|
55
|
+
name,
|
|
56
|
+
key,
|
|
57
|
+
homeUrl,
|
|
58
|
+
dashboardUrl,
|
|
59
|
+
};
|
|
60
|
+
})
|
|
61
|
+
.catch((e) => ({ error: e.message }));
|
|
62
|
+
};
|
|
17
63
|
|
|
18
64
|
export class LauncherAdapter implements VendorAdapter {
|
|
19
65
|
private vendorConfig: VendorConfig | null = null;
|
|
@@ -40,12 +86,12 @@ export class LauncherAdapter implements VendorAdapter {
|
|
|
40
86
|
productCode: params.productCode,
|
|
41
87
|
customerId: params.customerId,
|
|
42
88
|
description: params.description,
|
|
89
|
+
bindCapInfo: params.deliveryParams?.customParams?.bindDomainCap ? 'present' : 'not present',
|
|
43
90
|
});
|
|
44
91
|
|
|
45
92
|
const vendorConfig = await this.getVendorConfig();
|
|
46
93
|
|
|
47
94
|
try {
|
|
48
|
-
const launcherApiUrl = vendorConfig.app_url;
|
|
49
95
|
const orderData = {
|
|
50
96
|
checkoutSessionId: params.checkoutSessionId,
|
|
51
97
|
description: params.description,
|
|
@@ -53,19 +99,42 @@ export class LauncherAdapter implements VendorAdapter {
|
|
|
53
99
|
deliveryParams: params.deliveryParams,
|
|
54
100
|
};
|
|
55
101
|
|
|
102
|
+
// Extract bindDomainCap related params if present
|
|
103
|
+
const customParams = params.deliveryParams?.customParams || {};
|
|
104
|
+
const { domain, sessionId, bindDomainCap, domainNftDid } = customParams;
|
|
105
|
+
|
|
106
|
+
if (bindDomainCap && domain && sessionId) {
|
|
107
|
+
logger.info('passing domain binding authorization to Blocklet Server', {
|
|
108
|
+
domain,
|
|
109
|
+
sessionId: `${sessionId.substring(0, 8)}...`, // Log partial sessionId for security
|
|
110
|
+
hasBindCap: true,
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// Ensure bindDomainCap data is passed through to Blocklet Server
|
|
114
|
+
orderData.deliveryParams = {
|
|
115
|
+
...orderData.deliveryParams,
|
|
116
|
+
customParams: {
|
|
117
|
+
...orderData.deliveryParams.customParams,
|
|
118
|
+
domain,
|
|
119
|
+
sessionId,
|
|
120
|
+
bindDomainCap,
|
|
121
|
+
domainNftDid,
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
56
126
|
const { headers, body } = VendorAuth.signRequestWithHeaders(orderData);
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
}
|
|
64
|
-
);
|
|
127
|
+
const url = formatVendorUrl(vendorConfig, '/api/vendor/deliveries');
|
|
128
|
+
const response = await fetch(url, {
|
|
129
|
+
method: 'POST',
|
|
130
|
+
headers,
|
|
131
|
+
body,
|
|
132
|
+
});
|
|
65
133
|
|
|
66
134
|
if (!response.ok) {
|
|
67
135
|
const errorBody = await response.text();
|
|
68
|
-
logger.error('
|
|
136
|
+
logger.error('call launcher API error', {
|
|
137
|
+
url,
|
|
69
138
|
status: response.status,
|
|
70
139
|
statusText: response.statusText,
|
|
71
140
|
body: errorBody,
|
|
@@ -90,7 +159,18 @@ export class LauncherAdapter implements VendorAdapter {
|
|
|
90
159
|
currency: params.currency,
|
|
91
160
|
quantity: params.quantity,
|
|
92
161
|
invoiceId: params.invoiceId,
|
|
93
|
-
customParams:
|
|
162
|
+
customParams: {
|
|
163
|
+
...params.customParams,
|
|
164
|
+
// Include domain binding info if available
|
|
165
|
+
...(customParams.bindDomainCap
|
|
166
|
+
? {
|
|
167
|
+
domain: customParams.domain,
|
|
168
|
+
sessionId: customParams.sessionId,
|
|
169
|
+
bindDomainCap: customParams.bindDomainCap,
|
|
170
|
+
domainNftDid: customParams.domainNftDid,
|
|
171
|
+
}
|
|
172
|
+
: {}),
|
|
173
|
+
},
|
|
94
174
|
},
|
|
95
175
|
installationInfo: {
|
|
96
176
|
appId: launcherResult.appId,
|
|
@@ -110,7 +190,7 @@ export class LauncherAdapter implements VendorAdapter {
|
|
|
110
190
|
return result;
|
|
111
191
|
} catch (error: any) {
|
|
112
192
|
logger.error('Failed to create launcher order', {
|
|
113
|
-
error
|
|
193
|
+
error,
|
|
114
194
|
productCode: params.productCode,
|
|
115
195
|
customerId: params.customerId,
|
|
116
196
|
});
|
|
@@ -126,17 +206,13 @@ export class LauncherAdapter implements VendorAdapter {
|
|
|
126
206
|
});
|
|
127
207
|
|
|
128
208
|
const vendorConfig = await this.getVendorConfig();
|
|
129
|
-
const launcherApiUrl = vendorConfig.app_url;
|
|
130
209
|
const { headers, body } = VendorAuth.signRequestWithHeaders(params);
|
|
131
210
|
|
|
132
|
-
const response = await fetch(
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
body,
|
|
138
|
-
}
|
|
139
|
-
);
|
|
211
|
+
const response = await fetch(formatVendorUrl(vendorConfig, '/api/vendor/return'), {
|
|
212
|
+
method: 'POST',
|
|
213
|
+
headers,
|
|
214
|
+
body,
|
|
215
|
+
});
|
|
140
216
|
|
|
141
217
|
if (!response.ok) {
|
|
142
218
|
const errorBody = await response.text();
|
|
@@ -178,4 +254,16 @@ export class LauncherAdapter implements VendorAdapter {
|
|
|
178
254
|
throw error;
|
|
179
255
|
}
|
|
180
256
|
}
|
|
257
|
+
|
|
258
|
+
getOrderStatus(vendor: ProductVendor, orderId: string): Promise<any> {
|
|
259
|
+
const url = formatVendorUrl(vendor, `/api/vendor/status/${orderId}`);
|
|
260
|
+
|
|
261
|
+
return doRequestVendorData(vendor, orderId, url);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
getOrder(vendor: ProductVendor, orderId: string): Promise<any> {
|
|
265
|
+
const url = formatVendorUrl(vendor, `/api/vendor/orders/${orderId}`);
|
|
266
|
+
|
|
267
|
+
return doRequestVendorData(vendor, orderId, url);
|
|
268
|
+
}
|
|
181
269
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { ProductVendor } from '../../../store/models';
|
|
2
|
+
|
|
1
3
|
export interface VendorConfig {
|
|
2
4
|
id: string;
|
|
3
5
|
vendor_key: string;
|
|
@@ -85,4 +87,6 @@ export interface VendorAdapter {
|
|
|
85
87
|
fulfillOrder(params: FulfillOrderParams): Promise<FulfillOrderResult>;
|
|
86
88
|
requestReturn(params: ReturnRequestParams): Promise<ReturnRequestResult>;
|
|
87
89
|
checkOrderStatus(params: CheckOrderStatusParams): Promise<CheckOrderStatusResult>;
|
|
90
|
+
getOrder(vendor: ProductVendor, orderId: string): Promise<any>;
|
|
91
|
+
getOrderStatus(vendor: ProductVendor, orderId: string): Promise<any>;
|
|
88
92
|
}
|
|
@@ -47,7 +47,8 @@ export class VendorFulfillmentService {
|
|
|
47
47
|
currency_id: string;
|
|
48
48
|
customer_did: string;
|
|
49
49
|
},
|
|
50
|
-
vendorConfig: any
|
|
50
|
+
vendorConfig: any,
|
|
51
|
+
sharedContext?: any // Pass bindDomainCap and other shared data between vendors
|
|
51
52
|
): Promise<VendorFulfillmentResult> {
|
|
52
53
|
try {
|
|
53
54
|
const vendor = await ProductVendor.findByPk(vendorConfig.vendor_id);
|
|
@@ -87,7 +88,11 @@ export class VendorFulfillmentService {
|
|
|
87
88
|
},
|
|
88
89
|
deliveryParams: {
|
|
89
90
|
blockletMetaUrl: vendor.metadata?.blockletMetaUrl,
|
|
90
|
-
customParams:
|
|
91
|
+
customParams: {
|
|
92
|
+
...vendorConfig.custom_params,
|
|
93
|
+
// Merge shared context (bindDomainCap from didnames-adapter)
|
|
94
|
+
...(sharedContext || {}),
|
|
95
|
+
},
|
|
91
96
|
},
|
|
92
97
|
});
|
|
93
98
|
|
|
@@ -24,11 +24,172 @@ interface CoordinatorJob {
|
|
|
24
24
|
|
|
25
25
|
const MAX_FULFILLMENT_TIMEOUT = 300000;
|
|
26
26
|
|
|
27
|
+
// Helper function to get order info for coordinated fulfillment
|
|
28
|
+
async function getCoordinatedOrderInfo(checkoutSessionId: string) {
|
|
29
|
+
const checkoutSession = await CheckoutSession.findByPk(checkoutSessionId);
|
|
30
|
+
|
|
31
|
+
if (!checkoutSession) {
|
|
32
|
+
throw new Error('CheckoutSession or Invoice not found');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
checkoutSessionId,
|
|
37
|
+
amount_total: checkoutSession.amount_total,
|
|
38
|
+
customer_id: checkoutSession.customer_id || '',
|
|
39
|
+
invoiceId: checkoutSession.invoice_id || '',
|
|
40
|
+
currency_id: checkoutSession.currency_id,
|
|
41
|
+
customer_did: checkoutSession.customer_did || '',
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function handleCoordinatedFulfillment(job: any): Promise<void> {
|
|
46
|
+
const { checkoutSessionId, invoiceId, didnamesVendorConfig, launcherVendorConfig, context } = job;
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
logger.info('Starting coordinated fulfillment', {
|
|
50
|
+
checkoutSessionId,
|
|
51
|
+
didnamesVendorId: didnamesVendorConfig.vendor_id,
|
|
52
|
+
launcherVendorId: launcherVendorConfig.vendor_id,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const orderInfo = await getCoordinatedOrderInfo(checkoutSessionId);
|
|
56
|
+
if (!orderInfo) {
|
|
57
|
+
throw new Error('Order info not found');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Step 1: Execute didnames fulfillment to generate bindDomainCap
|
|
61
|
+
logger.info('Step 1: Executing didnames fulfillment', { checkoutSessionId });
|
|
62
|
+
const didnamesResult = await VendorFulfillmentService.fulfillSingleVendorOrder(
|
|
63
|
+
orderInfo,
|
|
64
|
+
didnamesVendorConfig,
|
|
65
|
+
context
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
if (didnamesResult.status === 'failed') {
|
|
69
|
+
throw new Error(`Didnames fulfillment failed: ${didnamesResult.errorMessage}`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Extract bindDomainCap from didnames result
|
|
73
|
+
const bindCapData = didnamesResult.orderDetails?.customParams || {};
|
|
74
|
+
const { domain, sessionId, bindDomainCap, nftDid } = bindCapData;
|
|
75
|
+
|
|
76
|
+
if (!bindDomainCap || !domain || !sessionId) {
|
|
77
|
+
logger.warn('Missing bindDomainCap data from didnames adapter', {
|
|
78
|
+
checkoutSessionId,
|
|
79
|
+
hasBindCap: !!bindDomainCap,
|
|
80
|
+
hasDomain: !!domain,
|
|
81
|
+
hasSessionId: !!sessionId,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Update didnames vendor status
|
|
86
|
+
await updateVendorFulfillmentStatus(
|
|
87
|
+
checkoutSessionId,
|
|
88
|
+
invoiceId,
|
|
89
|
+
didnamesVendorConfig.vendor_id,
|
|
90
|
+
didnamesResult.status === 'completed' ? 'completed' : 'sent',
|
|
91
|
+
{
|
|
92
|
+
orderId: didnamesResult.orderId,
|
|
93
|
+
commissionAmount: didnamesResult.commissionAmount,
|
|
94
|
+
serviceUrl: didnamesResult.serviceUrl,
|
|
95
|
+
}
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
// Step 2: Execute launcher fulfillment with bindDomainCap
|
|
99
|
+
logger.info('Step 2: Executing launcher fulfillment with bindDomainCap', {
|
|
100
|
+
checkoutSessionId,
|
|
101
|
+
hasBindCapData: !!bindDomainCap,
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const launcherResult = await VendorFulfillmentService.fulfillSingleVendorOrder(orderInfo, launcherVendorConfig, {
|
|
105
|
+
...context,
|
|
106
|
+
domain,
|
|
107
|
+
sessionId,
|
|
108
|
+
bindDomainCap,
|
|
109
|
+
domainNftDid: nftDid,
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// Update launcher vendor status
|
|
113
|
+
await updateVendorFulfillmentStatus(
|
|
114
|
+
checkoutSessionId,
|
|
115
|
+
invoiceId,
|
|
116
|
+
launcherVendorConfig.vendor_id,
|
|
117
|
+
launcherResult.status === 'completed' ? 'completed' : 'sent',
|
|
118
|
+
{
|
|
119
|
+
orderId: launcherResult.orderId,
|
|
120
|
+
commissionAmount: launcherResult.commissionAmount,
|
|
121
|
+
serviceUrl: launcherResult.serviceUrl,
|
|
122
|
+
}
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
logger.info('Coordinated fulfillment completed successfully', {
|
|
126
|
+
checkoutSessionId,
|
|
127
|
+
didnamesStatus: didnamesResult.status,
|
|
128
|
+
launcherStatus: launcherResult.status,
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// Check if all vendors are completed to trigger commission
|
|
132
|
+
await triggerCommissionProcess(checkoutSessionId, invoiceId);
|
|
133
|
+
} catch (error: any) {
|
|
134
|
+
logger.error('Coordinated fulfillment failed', {
|
|
135
|
+
checkoutSessionId,
|
|
136
|
+
error: error.message,
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// Mark both vendors as failed
|
|
140
|
+
await updateVendorFulfillmentStatus(checkoutSessionId, invoiceId, didnamesVendorConfig.vendor_id, 'failed', {
|
|
141
|
+
lastError: error.message,
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
await updateVendorFulfillmentStatus(checkoutSessionId, invoiceId, launcherVendorConfig.vendor_id, 'failed', {
|
|
145
|
+
lastError: error.message,
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
throw error;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
27
152
|
export const fulfillmentCoordinatorQueue = createQueue({
|
|
28
153
|
name: 'fulfillment-coordinator',
|
|
29
154
|
onJob: handleFulfillmentCoordination,
|
|
30
155
|
});
|
|
31
156
|
|
|
157
|
+
export const coordinatedFulfillmentQueue = createQueue({
|
|
158
|
+
name: 'coordinated-fulfillment',
|
|
159
|
+
onJob: handleCoordinatedFulfillment,
|
|
160
|
+
options: {
|
|
161
|
+
concurrency: 1,
|
|
162
|
+
maxRetries: 3,
|
|
163
|
+
enableScheduledJob: true,
|
|
164
|
+
},
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
export const startCoordinatedFulfillmentQueue = () => {
|
|
168
|
+
logger.debug('startCoordinatedFulfillmentQueue');
|
|
169
|
+
return Promise.resolve();
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
// Add event listener for coordinated fulfillment
|
|
173
|
+
events.on('vendor.fulfillment.coordinated', async (id, job) => {
|
|
174
|
+
try {
|
|
175
|
+
const exist = await coordinatedFulfillmentQueue.get(id);
|
|
176
|
+
if (!exist) {
|
|
177
|
+
logger.info('Adding coordinated fulfillment job to queue', { id, checkoutSessionId: job.checkoutSessionId });
|
|
178
|
+
coordinatedFulfillmentQueue.push({
|
|
179
|
+
id,
|
|
180
|
+
job,
|
|
181
|
+
});
|
|
182
|
+
} else {
|
|
183
|
+
logger.info('Coordinated fulfillment job already exists, skipping', { id });
|
|
184
|
+
}
|
|
185
|
+
} catch (error: any) {
|
|
186
|
+
logger.error('Failed to handle coordinated fulfillment queue event', {
|
|
187
|
+
id,
|
|
188
|
+
error: error.message,
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
|
|
32
193
|
export async function startVendorFulfillment(checkoutSessionId: string, invoiceId: string): Promise<void> {
|
|
33
194
|
try {
|
|
34
195
|
logger.info('Starting vendor fulfillment process', {
|
|
@@ -60,17 +221,42 @@ export async function startVendorFulfillment(checkoutSessionId: string, invoiceI
|
|
|
60
221
|
|
|
61
222
|
await updateVendorInfo(checkoutSessionId, initialVendorInfo);
|
|
62
223
|
|
|
63
|
-
//
|
|
64
|
-
|
|
65
|
-
|
|
224
|
+
// Check if we need coordinated fulfillment (didnames + launcher)
|
|
225
|
+
const didnamesVendor = vendorConfigs.find((v) => v.vendor_type === 'didnames');
|
|
226
|
+
const launcherVendor = vendorConfigs.find((v) => v.vendor_type === 'launcher');
|
|
227
|
+
|
|
228
|
+
if (didnamesVendor && launcherVendor) {
|
|
229
|
+
// Coordinated fulfillment: didnames first, then launcher with bindDomainCap
|
|
230
|
+
logger.info('Starting coordinated domain binding fulfillment', {
|
|
231
|
+
checkoutSessionId,
|
|
232
|
+
didnamesVendorId: didnamesVendor.vendor_id,
|
|
233
|
+
launcherVendorId: launcherVendor.vendor_id,
|
|
234
|
+
});
|
|
66
235
|
|
|
67
|
-
|
|
236
|
+
const coordinatedJobId = `coordinated-fulfillment-${checkoutSessionId}`;
|
|
237
|
+
events.emit('vendor.fulfillment.coordinated', coordinatedJobId, {
|
|
68
238
|
checkoutSessionId,
|
|
69
239
|
invoiceId,
|
|
70
|
-
|
|
71
|
-
|
|
240
|
+
didnamesVendorConfig: didnamesVendor,
|
|
241
|
+
launcherVendorConfig: launcherVendor,
|
|
72
242
|
retryOnError: true,
|
|
73
243
|
});
|
|
244
|
+
} else {
|
|
245
|
+
// Regular parallel fulfillment for other vendors
|
|
246
|
+
for (const vendorConfig of vendorConfigs) {
|
|
247
|
+
const vendorFulfillmentJobId = `vendor-fulfillment-${checkoutSessionId}-${vendorConfig.vendor_id}`;
|
|
248
|
+
|
|
249
|
+
events.emit('vendor.fulfillment.queued', vendorFulfillmentJobId, {
|
|
250
|
+
checkoutSessionId,
|
|
251
|
+
invoiceId,
|
|
252
|
+
vendorId: vendorConfig.vendor_id,
|
|
253
|
+
vendorConfig,
|
|
254
|
+
retryOnError: true,
|
|
255
|
+
context: {
|
|
256
|
+
subdomain: `docsmith-${Date.now()}`,
|
|
257
|
+
},
|
|
258
|
+
});
|
|
259
|
+
}
|
|
74
260
|
}
|
|
75
261
|
|
|
76
262
|
logger.info('Vendor fulfillment process has been triggered', {
|
|
@@ -294,7 +480,7 @@ async function updateSingleVendorInfo(
|
|
|
294
480
|
logger.error('updateVendorInfo - Update failed', {
|
|
295
481
|
checkoutSessionId,
|
|
296
482
|
vendorId,
|
|
297
|
-
error
|
|
483
|
+
error,
|
|
298
484
|
});
|
|
299
485
|
throw error;
|
|
300
486
|
} finally {
|
package/api/src/routes/vendor.ts
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { getUrl } from '@blocklet/sdk/lib/component';
|
|
2
2
|
import { Router } from 'express';
|
|
3
3
|
import Joi from 'joi';
|
|
4
|
+
import { middleware } from '@blocklet/payment-vendor';
|
|
4
5
|
|
|
5
|
-
import { Auth as VendorAuth, middleware } from '@blocklet/payment-vendor';
|
|
6
|
-
import { joinURL } from 'ufo';
|
|
7
6
|
import { MetadataSchema } from '../libs/api';
|
|
8
7
|
import { wallet } from '../libs/auth';
|
|
9
8
|
import dayjs from '../libs/dayjs';
|
|
@@ -11,14 +10,16 @@ import logger from '../libs/logger';
|
|
|
11
10
|
import { authenticate } from '../libs/security';
|
|
12
11
|
import { formatToShortUrl } from '../libs/url';
|
|
13
12
|
import { getBlockletJson } from '../libs/util';
|
|
13
|
+
import { VendorFulfillmentService } from '../libs/vendor-util/fulfillment';
|
|
14
14
|
import { CheckoutSession, Invoice, Subscription } from '../store/models';
|
|
15
15
|
import { ProductVendor } from '../store/models/product-vendor';
|
|
16
16
|
|
|
17
17
|
const authAdmin = authenticate<CheckoutSession>({ component: true, roles: ['owner', 'admin'] });
|
|
18
|
+
const loginAuth = authenticate<CheckoutSession>({ component: true, ensureLogin: true, mine: true });
|
|
18
19
|
|
|
19
20
|
const createVendorSchema = Joi.object({
|
|
20
21
|
vendor_key: Joi.string().max(50).required(),
|
|
21
|
-
vendor_type: Joi.string().valid('launcher').default('launcher'),
|
|
22
|
+
vendor_type: Joi.string().valid('launcher', 'didnames').default('launcher'),
|
|
22
23
|
name: Joi.string().max(255).required(),
|
|
23
24
|
description: Joi.string().max(1000).allow('').optional(),
|
|
24
25
|
app_url: Joi.string().uri().max(512).required(),
|
|
@@ -32,7 +33,7 @@ const createVendorSchema = Joi.object({
|
|
|
32
33
|
}).unknown(false);
|
|
33
34
|
|
|
34
35
|
const updateVendorSchema = Joi.object({
|
|
35
|
-
vendor_type: Joi.string().valid('launcher').optional(),
|
|
36
|
+
vendor_type: Joi.string().valid('launcher', 'didnames').optional(),
|
|
36
37
|
name: Joi.string().max(255).optional(),
|
|
37
38
|
description: Joi.string().max(1000).allow('').optional(),
|
|
38
39
|
app_url: Joi.string().uri().max(512).optional(),
|
|
@@ -114,7 +115,7 @@ async function getAllVendors(_req: any, res: any) {
|
|
|
114
115
|
}
|
|
115
116
|
}
|
|
116
117
|
|
|
117
|
-
async function
|
|
118
|
+
async function getVendorInfo(req: any, res: any) {
|
|
118
119
|
try {
|
|
119
120
|
const vendor = await ProductVendor.findByPk(req.params.id);
|
|
120
121
|
if (!vendor) {
|
|
@@ -294,57 +295,40 @@ async function testVendorConnection(req: any, res: any) {
|
|
|
294
295
|
}
|
|
295
296
|
}
|
|
296
297
|
|
|
297
|
-
async
|
|
298
|
+
const getVendorById = async (vendorId: string, orderId: string) => {
|
|
298
299
|
if (!vendorId || !orderId) {
|
|
299
|
-
|
|
300
|
+
throw new Error(`vendorId or orderId is required, vendorId: ${vendorId}, orderId: ${orderId}`);
|
|
300
301
|
}
|
|
301
302
|
|
|
302
303
|
const vendor = await ProductVendor.findByPk(vendorId);
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
}
|
|
329
|
-
const validUntil = dayjs().add(20, 'minutes').format('YYYY-MM-DDTHH:mm:ss+00:00');
|
|
330
|
-
const maxVisits = 5;
|
|
331
|
-
|
|
332
|
-
const homeUrl = await formatToShortUrl({ url: data.homeUrl, maxVisits, validUntil });
|
|
333
|
-
const dashboardUrl = await formatToShortUrl({ url: data.dashboardUrl, maxVisits, validUntil });
|
|
334
|
-
|
|
335
|
-
return {
|
|
336
|
-
...data,
|
|
337
|
-
name,
|
|
338
|
-
key,
|
|
339
|
-
homeUrl,
|
|
340
|
-
dashboardUrl,
|
|
341
|
-
};
|
|
342
|
-
})
|
|
343
|
-
.catch((e) => ({ error: e.message }))
|
|
344
|
-
: null;
|
|
304
|
+
if (!vendor) {
|
|
305
|
+
throw new Error(`vendor not found, vendorId: ${vendorId}`);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const vendorAdapter = await VendorFulfillmentService.getVendorAdapter(vendor.vendor_key);
|
|
309
|
+
|
|
310
|
+
const data = await vendorAdapter.getOrder(vendor, orderId);
|
|
311
|
+
|
|
312
|
+
return data;
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
async function getVendorStatusById(vendorId: string, orderId: string) {
|
|
316
|
+
if (!vendorId || !orderId) {
|
|
317
|
+
throw new Error(`vendorId or orderId is required, vendorId: ${vendorId}, orderId: ${orderId}`);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const vendor = await ProductVendor.findByPk(vendorId);
|
|
321
|
+
|
|
322
|
+
if (!vendor) {
|
|
323
|
+
throw new Error(`vendor not found, vendorId: ${vendorId}`);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const vendorAdapter = await VendorFulfillmentService.getVendorAdapter(vendor.vendor_key);
|
|
327
|
+
|
|
328
|
+
return vendorAdapter.getOrderStatus(vendor, orderId);
|
|
345
329
|
}
|
|
346
330
|
|
|
347
|
-
async function
|
|
331
|
+
async function doRequestVendor(sessionId: string, func: (vendorId: string, orderId: string) => Promise<any>) {
|
|
348
332
|
const doc = await CheckoutSession.findByPk(sessionId);
|
|
349
333
|
|
|
350
334
|
if (!doc) {
|
|
@@ -376,35 +360,36 @@ async function getVendorStatus(sessionId: string, isDetail = false) {
|
|
|
376
360
|
}
|
|
377
361
|
|
|
378
362
|
const vendors = doc.vendor_info.map((item) => {
|
|
379
|
-
return
|
|
380
|
-
return {
|
|
381
|
-
error_message: item.error_message,
|
|
382
|
-
status: item.status,
|
|
383
|
-
...status,
|
|
384
|
-
};
|
|
385
|
-
});
|
|
363
|
+
return func(item.vendor_id, item.order_id);
|
|
386
364
|
});
|
|
387
365
|
|
|
388
|
-
|
|
389
|
-
|
|
366
|
+
return {
|
|
367
|
+
payment_status: doc.payment_status,
|
|
368
|
+
session_status: doc.status,
|
|
369
|
+
vendors: await Promise.all(vendors),
|
|
370
|
+
subscriptionId: doc.subscription_id,
|
|
371
|
+
error: null,
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
async function getVendorStatus(sessionId: string) {
|
|
376
|
+
const result: any = await doRequestVendor(sessionId, getVendorStatusById);
|
|
390
377
|
|
|
391
|
-
if (
|
|
392
|
-
const subscriptionUrl = getUrl(`/customer/subscription/${subscriptionId}`);
|
|
378
|
+
if (result.subscriptionId) {
|
|
379
|
+
const subscriptionUrl = getUrl(`/customer/subscription/${result.subscriptionId}`);
|
|
393
380
|
|
|
394
|
-
|
|
381
|
+
result.subscriptionUrl = await formatToShortUrl({
|
|
395
382
|
url: subscriptionUrl,
|
|
396
383
|
maxVisits: 5,
|
|
397
384
|
validUntil: dayjs().add(20, 'minutes').format('YYYY-MM-DDTHH:mm:ss+00:00'),
|
|
398
385
|
});
|
|
399
386
|
}
|
|
400
387
|
|
|
401
|
-
return
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
error: null,
|
|
407
|
-
};
|
|
388
|
+
return result;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function getVendor(sessionId: string) {
|
|
392
|
+
return doRequestVendor(sessionId, getVendorById);
|
|
408
393
|
}
|
|
409
394
|
|
|
410
395
|
async function getVendorFulfillmentStatus(req: any, res: any) {
|
|
@@ -426,7 +411,7 @@ async function getVendorFulfillmentDetail(req: any, res: any) {
|
|
|
426
411
|
const { sessionId } = req.params;
|
|
427
412
|
|
|
428
413
|
try {
|
|
429
|
-
const detail = await
|
|
414
|
+
const detail = await getVendor(sessionId);
|
|
430
415
|
if (detail.code) {
|
|
431
416
|
return res.status(detail.code).json({ error: detail.error });
|
|
432
417
|
}
|
|
@@ -457,7 +442,7 @@ async function redirectToVendor(req: any, res: any) {
|
|
|
457
442
|
return res.redirect('/404');
|
|
458
443
|
}
|
|
459
444
|
|
|
460
|
-
const detail = await
|
|
445
|
+
const detail = await getVendorById(vendorId, order.order_id || '');
|
|
461
446
|
if (!detail) {
|
|
462
447
|
logger.warn('Vendor status detail not found', {
|
|
463
448
|
subscriptionId,
|
|
@@ -539,8 +524,8 @@ const ensureVendorAuth = middleware.ensureVendorAuth((vendorPk: string) =>
|
|
|
539
524
|
);
|
|
540
525
|
|
|
541
526
|
// FIXME: Authentication not yet added, awaiting implementation @Pengfei
|
|
542
|
-
router.get('/order/:sessionId/status', validateParams(sessionIdParamSchema), getVendorFulfillmentStatus);
|
|
543
|
-
router.get('/order/:sessionId/detail', validateParams(sessionIdParamSchema), getVendorFulfillmentDetail);
|
|
527
|
+
router.get('/order/:sessionId/status', loginAuth, validateParams(sessionIdParamSchema), getVendorFulfillmentStatus);
|
|
528
|
+
router.get('/order/:sessionId/detail', loginAuth, validateParams(sessionIdParamSchema), getVendorFulfillmentDetail);
|
|
544
529
|
|
|
545
530
|
router.get('/subscription/:sessionId/redirect', handleSubscriptionRedirect);
|
|
546
531
|
router.get('/subscription/:sessionId', ensureVendorAuth, getVendorSubscription);
|
|
@@ -554,7 +539,7 @@ router.get(
|
|
|
554
539
|
);
|
|
555
540
|
|
|
556
541
|
router.get('/', getAllVendors);
|
|
557
|
-
router.get('/:id', authAdmin, validateParams(vendorIdParamSchema),
|
|
542
|
+
router.get('/:id', authAdmin, validateParams(vendorIdParamSchema), getVendorInfo);
|
|
558
543
|
router.post('/', authAdmin, createVendor);
|
|
559
544
|
router.put('/:id', authAdmin, validateParams(vendorIdParamSchema), updateVendor);
|
|
560
545
|
router.delete('/:id', authAdmin, validateParams(vendorIdParamSchema), deleteVendor);
|
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.18",
|
|
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,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-
|
|
60
|
-
"@blocklet/payment-
|
|
59
|
+
"@blocklet/payment-broker-client": "1.20.18",
|
|
60
|
+
"@blocklet/payment-react": "1.20.18",
|
|
61
|
+
"@blocklet/payment-vendor": "1.20.18",
|
|
61
62
|
"@blocklet/sdk": "^1.16.52-beta-20250912-112002-e3499e9c",
|
|
62
63
|
"@blocklet/ui-react": "^3.1.41",
|
|
63
64
|
"@blocklet/uploader": "^0.2.12",
|
|
@@ -120,13 +121,14 @@
|
|
|
120
121
|
"ufo": "^1.6.1",
|
|
121
122
|
"umzug": "^3.8.2",
|
|
122
123
|
"use-bus": "^2.5.2",
|
|
124
|
+
"uuid": "^13.0.0",
|
|
123
125
|
"validator": "^13.15.15",
|
|
124
126
|
"web3": "^4.16.0"
|
|
125
127
|
},
|
|
126
128
|
"devDependencies": {
|
|
127
129
|
"@abtnode/types": "^1.16.52-beta-20250912-112002-e3499e9c",
|
|
128
130
|
"@arcblock/eslint-config-ts": "^0.3.3",
|
|
129
|
-
"@blocklet/payment-types": "1.20.
|
|
131
|
+
"@blocklet/payment-types": "1.20.18",
|
|
130
132
|
"@types/cookie-parser": "^1.4.9",
|
|
131
133
|
"@types/cors": "^2.8.19",
|
|
132
134
|
"@types/debug": "^4.1.12",
|
|
@@ -173,5 +175,5 @@
|
|
|
173
175
|
"parser": "typescript"
|
|
174
176
|
}
|
|
175
177
|
},
|
|
176
|
-
"gitHead": "
|
|
178
|
+
"gitHead": "94e4020fb0d4121ebf0430de2db1fd642676c8ae"
|
|
177
179
|
}
|
package/src/locales/en.tsx
CHANGED
|
@@ -1126,6 +1126,7 @@ export default flat({
|
|
|
1126
1126
|
nameRequired: 'Vendor name is required',
|
|
1127
1127
|
vendorType: 'Vendor Type',
|
|
1128
1128
|
vendorTypeRequired: 'Vendor type is required',
|
|
1129
|
+
didnames: 'DID Names',
|
|
1129
1130
|
launcher: 'Launcher',
|
|
1130
1131
|
vendorKey: 'Vendor Key',
|
|
1131
1132
|
vendorKeyRequired: 'Vendor key is required',
|
package/src/locales/zh.tsx
CHANGED
|
@@ -80,13 +80,13 @@ export default function VendorCreate({
|
|
|
80
80
|
}
|
|
81
81
|
: {
|
|
82
82
|
vendor_key: '',
|
|
83
|
-
vendor_type: '
|
|
83
|
+
vendor_type: 'didnames',
|
|
84
84
|
name: '',
|
|
85
85
|
description: '',
|
|
86
86
|
app_url: '',
|
|
87
87
|
vendor_did: '',
|
|
88
88
|
status: 'inactive' as const,
|
|
89
|
-
metadata: [
|
|
89
|
+
metadata: [],
|
|
90
90
|
app_pid: '',
|
|
91
91
|
app_logo: '',
|
|
92
92
|
};
|
|
@@ -256,6 +256,7 @@ export default function VendorCreate({
|
|
|
256
256
|
<InputLabel>{t('admin.vendor.vendorType')}</InputLabel>
|
|
257
257
|
<Select {...field} label={t('admin.vendor.vendorType')}>
|
|
258
258
|
<MenuItem value="launcher">{t('admin.vendor.launcher')}</MenuItem>
|
|
259
|
+
<MenuItem value="didnames">{t('admin.vendor.didnames')}</MenuItem>
|
|
259
260
|
</Select>
|
|
260
261
|
</FormControl>
|
|
261
262
|
)}
|