payment-kit 1.20.17 → 1.20.19
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 +87 -86
- package/blocklet.yml +1 -1
- package/package.json +7 -6
- package/src/components/vendor/actions.tsx +1 -1
- package/src/locales/en.tsx +1 -0
- package/src/locales/zh.tsx +2 -1
- package/src/pages/admin/products/vendors/create.tsx +1 -36
- package/src/pages/admin/products/vendors/index.tsx +1 -1
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
|
}
|