payment-kit 1.20.5 → 1.20.6

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.
Files changed (39) hide show
  1. package/api/src/crons/index.ts +11 -3
  2. package/api/src/index.ts +18 -14
  3. package/api/src/libs/adapters/launcher-adapter.ts +177 -0
  4. package/api/src/libs/env.ts +7 -0
  5. package/api/src/libs/url.ts +77 -0
  6. package/api/src/libs/vendor-adapter-factory.ts +22 -0
  7. package/api/src/libs/vendor-adapter.ts +109 -0
  8. package/api/src/libs/vendor-fulfillment.ts +321 -0
  9. package/api/src/queues/payment.ts +14 -10
  10. package/api/src/queues/payout.ts +1 -0
  11. package/api/src/queues/vendor/vendor-commission.ts +192 -0
  12. package/api/src/queues/vendor/vendor-fulfillment-coordinator.ts +627 -0
  13. package/api/src/queues/vendor/vendor-fulfillment.ts +97 -0
  14. package/api/src/queues/vendor/vendor-status-check.ts +179 -0
  15. package/api/src/routes/checkout-sessions.ts +3 -0
  16. package/api/src/routes/index.ts +2 -0
  17. package/api/src/routes/products.ts +72 -1
  18. package/api/src/routes/vendor.ts +526 -0
  19. package/api/src/store/migrations/20250820-add-product-vendor.ts +102 -0
  20. package/api/src/store/migrations/20250822-add-vendor-config-to-products.ts +56 -0
  21. package/api/src/store/models/checkout-session.ts +84 -18
  22. package/api/src/store/models/index.ts +3 -0
  23. package/api/src/store/models/payout.ts +11 -0
  24. package/api/src/store/models/product-vendor.ts +118 -0
  25. package/api/src/store/models/product.ts +15 -0
  26. package/blocklet.yml +8 -2
  27. package/doc/vendor_fulfillment_system.md +931 -0
  28. package/package.json +5 -4
  29. package/src/components/collapse.tsx +1 -0
  30. package/src/components/product/edit.tsx +9 -0
  31. package/src/components/product/form.tsx +11 -0
  32. package/src/components/product/vendor-config.tsx +249 -0
  33. package/src/components/vendor/actions.tsx +145 -0
  34. package/src/locales/en.tsx +89 -0
  35. package/src/locales/zh.tsx +89 -0
  36. package/src/pages/admin/products/index.tsx +11 -1
  37. package/src/pages/admin/products/products/detail.tsx +79 -2
  38. package/src/pages/admin/products/vendors/create.tsx +418 -0
  39. package/src/pages/admin/products/vendors/index.tsx +313 -0
@@ -18,17 +18,19 @@ import {
18
18
  stripePaymentCronTime,
19
19
  stripeSubscriptionCronTime,
20
20
  subscriptionCronTime,
21
+ vendorStatusCheckCronTime,
21
22
  } from '../libs/env';
22
23
  import logger from '../libs/logger';
24
+ import { startCreditConsumeQueue } from '../queues/credit-consume';
25
+ import { startDepositVaultQueue } from '../queues/payment';
23
26
  import { startSubscriptionQueue } from '../queues/subscription';
27
+ import { startVendorStatusCheckSchedule } from '../queues/vendor/vendor-status-check';
24
28
  import { CheckoutSession } from '../store/models';
29
+ import { createMeteringSubscriptionDetection } from './metering-subscription-detection';
25
30
  import { createPaymentStat } from './payment-stat';
26
31
  import { SubscriptionTrialWillEndSchedule } from './subscription-trial-will-end';
27
32
  import { SubscriptionWillCanceledSchedule } from './subscription-will-canceled';
28
33
  import { SubscriptionWillRenewSchedule } from './subscription-will-renew';
29
- import { createMeteringSubscriptionDetection } from './metering-subscription-detection';
30
- import { startDepositVaultQueue } from '../queues/payment';
31
- import { startCreditConsumeQueue } from '../queues/credit-consume';
32
34
 
33
35
  function init() {
34
36
  Cron.init({
@@ -115,6 +117,12 @@ function init() {
115
117
  fn: () => startCreditConsumeQueue(),
116
118
  options: { runOnInit: true },
117
119
  },
120
+ {
121
+ name: 'vendor.status.check',
122
+ time: vendorStatusCheckCronTime,
123
+ fn: () => startVendorStatusCheckSchedule(),
124
+ options: { runOnInit: false },
125
+ },
118
126
  ],
119
127
  onError: (error: Error, name: string) => {
120
128
  logger.error('run job failed', { name, error });
package/api/src/index.ts CHANGED
@@ -8,48 +8,50 @@ import cors from 'cors';
8
8
  import dotenv from 'dotenv-flow';
9
9
  import express, { ErrorRequestHandler } from 'express';
10
10
  // eslint-disable-next-line import/no-extraneous-dependencies
11
- import { xss } from '@blocklet/xss';
12
- import { csrf } from '@blocklet/sdk/lib/middlewares';
13
11
  import { CustomError, formatError, getStatusFromError } from '@blocklet/error';
12
+ import { csrf } from '@blocklet/sdk/lib/middlewares';
13
+ import { xss } from '@blocklet/xss';
14
14
 
15
+ import { syncCurrencyLogo } from './crons/currency';
15
16
  import crons from './crons/index';
16
17
  import { ensureStakedForGas } from './integrations/arcblock/stake';
17
18
  import { initResourceHandler } from './integrations/blocklet/resource';
19
+ import { initUserHandler } from './integrations/blocklet/user';
18
20
  import { ensureWebhookRegistered } from './integrations/stripe/setup';
19
21
  import { handlers } from './libs/auth';
20
22
  import logger, { setupAccessLogger } from './libs/logger';
21
23
  import { contextMiddleware, ensureI18n } from './libs/middleware';
24
+ import { ensureCreateOverdraftProtectionPrices } from './libs/overdraft-protection';
22
25
  import { initEventBroadcast } from './libs/ws';
23
26
  import { startCheckoutSessionQueue } from './queues/checkout-session';
27
+ import { startCreditConsumeQueue } from './queues/credit-consume';
28
+ import { startCreditGrantQueue } from './queues/credit-grant';
24
29
  import { startEventQueue } from './queues/event';
25
30
  import { startInvoiceQueue } from './queues/invoice';
26
31
  import { startNotificationQueue } from './queues/notification';
27
32
  import { startPaymentQueue } from './queues/payment';
28
33
  import { startPayoutQueue } from './queues/payout';
29
34
  import { startRefundQueue } from './queues/refund';
35
+ import { startUploadBillingInfoListener } from './queues/space';
30
36
  import { startSubscriptionQueue } from './queues/subscription';
31
- import { startCreditConsumeQueue } from './queues/credit-consume';
32
- import { startCreditGrantQueue } from './queues/credit-grant';
37
+ import { startVendorCommissionQueue } from './queues/vendor/vendor-commission';
38
+ import { startVendorFulfillmentQueue } from './queues/vendor/vendor-fulfillment';
33
39
  import routes from './routes';
40
+ import autoRechargeAuthorizationHandlers from './routes/connect/auto-recharge-auth';
34
41
  import changePaymentHandlers from './routes/connect/change-payment';
35
42
  import changePlanHandlers from './routes/connect/change-plan';
36
43
  import collectHandlers from './routes/connect/collect';
37
44
  import collectBatchHandlers from './routes/connect/collect-batch';
38
- import rechargeHandlers from './routes/connect/recharge';
39
- import payHandlers from './routes/connect/pay';
40
- import setupHandlers from './routes/connect/setup';
41
- import subscribeHandlers from './routes/connect/subscribe';
42
45
  import delegationHandlers from './routes/connect/delegation';
43
46
  import overdraftProtectionHandlers from './routes/connect/overdraft-protection';
44
- import rechargeAccountHandlers from './routes/connect/recharge-account';
47
+ import payHandlers from './routes/connect/pay';
45
48
  import reStakeHandlers from './routes/connect/re-stake';
46
- import autoRechargeAuthorizationHandlers from './routes/connect/auto-recharge-auth';
49
+ import rechargeHandlers from './routes/connect/recharge';
50
+ import rechargeAccountHandlers from './routes/connect/recharge-account';
51
+ import setupHandlers from './routes/connect/setup';
52
+ import subscribeHandlers from './routes/connect/subscribe';
47
53
  import { initialize } from './store/models';
48
54
  import { sequelize } from './store/sequelize';
49
- import { initUserHandler } from './integrations/blocklet/user';
50
- import { startUploadBillingInfoListener } from './queues/space';
51
- import { syncCurrencyLogo } from './crons/currency';
52
- import { ensureCreateOverdraftProtectionPrices } from './libs/overdraft-protection';
53
55
 
54
56
  dotenv.config();
55
57
 
@@ -126,6 +128,8 @@ export const server = app.listen(port, (err?: any) => {
126
128
  startSubscriptionQueue().then(() => logger.info('subscription queue started'));
127
129
  startEventQueue().then(() => logger.info('event queue started'));
128
130
  startPayoutQueue().then(() => logger.info('payout queue started'));
131
+ startVendorCommissionQueue().then(() => logger.info('vendor commission queue started'));
132
+ startVendorFulfillmentQueue().then(() => logger.info('vendor fulfillment queue started'));
129
133
  startCheckoutSessionQueue().then(() => logger.info('checkoutSession queue started'));
130
134
  startNotificationQueue().then(() => logger.info('notification queue started'));
131
135
  startRefundQueue().then(() => logger.info('refund queue started'));
@@ -0,0 +1,177 @@
1
+ import { VendorAuth } from '@blocklet/payment-vendor';
2
+
3
+ import {
4
+ VendorAdapter,
5
+ VendorConfig,
6
+ FulfillOrderParams,
7
+ FulfillOrderResult,
8
+ ReturnRequestParams,
9
+ ReturnRequestResult,
10
+ CheckOrderStatusParams,
11
+ CheckOrderStatusResult,
12
+ } from '../vendor-adapter';
13
+ import logger from '../logger';
14
+ import { ProductVendor } from '../../store/models';
15
+ import { api } from '../util';
16
+
17
+ export class LauncherAdapter implements VendorAdapter {
18
+ private vendorConfig: VendorConfig | null = null;
19
+ private vendorKey: string;
20
+
21
+ constructor(vendorInfo: VendorConfig | string) {
22
+ if (typeof vendorInfo === 'string') {
23
+ this.vendorKey = vendorInfo;
24
+ this.vendorConfig = null;
25
+ } else {
26
+ this.vendorKey = vendorInfo.id;
27
+ this.vendorConfig = vendorInfo;
28
+ }
29
+ }
30
+
31
+ async getVendorConfig(): Promise<VendorConfig> {
32
+ if (this.vendorConfig === null) {
33
+ this.vendorConfig = await ProductVendor.findOne({ where: { vendor_key: this.vendorKey } });
34
+ if (!this.vendorConfig) {
35
+ throw new Error(`Vendor not found: ${this.vendorKey}`);
36
+ }
37
+ }
38
+ return this.vendorConfig;
39
+ }
40
+
41
+ async fulfillOrder(params: FulfillOrderParams): Promise<FulfillOrderResult> {
42
+ logger.info('Creating launcher order via real API', {
43
+ productCode: params.productCode,
44
+ customerId: params.customerId,
45
+ description: params.description,
46
+ });
47
+
48
+ const vendorConfig = await this.getVendorConfig();
49
+
50
+ try {
51
+ const launcherApiUrl = vendorConfig.app_url;
52
+ const orderData = {
53
+ description: params.description,
54
+ userInfo: params.userInfo,
55
+ deliveryParams: params.deliveryParams,
56
+ };
57
+
58
+ const { headers, body } = VendorAuth.signRequestWithHeaders(orderData);
59
+ const response = await fetch(`${launcherApiUrl}/api/vendor/deliveries`, {
60
+ method: 'POST',
61
+ headers,
62
+ body,
63
+ });
64
+
65
+ if (!response.ok) {
66
+ const errorBody = await response.text();
67
+ logger.error('Launcher API error', {
68
+ status: response.status,
69
+ statusText: response.statusText,
70
+ body: errorBody,
71
+ });
72
+ throw new Error(`Launcher API error: ${response.status} ${response.statusText}`);
73
+ }
74
+
75
+ const launcherResult = await response.json();
76
+
77
+ const result: FulfillOrderResult = {
78
+ orderId: launcherResult.orderId || launcherResult.vendorOrderId || `launcher_${Date.now()}`,
79
+ status: launcherResult.status || 'pending',
80
+ serviceUrl: launcherResult.serviceUrl || launcherResult.appUrl,
81
+ estimatedTime: launcherResult.estimatedTime || 300,
82
+ message: launcherResult.message || 'Launcher order created successfully',
83
+ vendorOrderId: launcherResult.vendorOrderId || launcherResult.orderId,
84
+ progress: launcherResult.progress || 0,
85
+ orderDetails: {
86
+ productCode: params.productCode,
87
+ customerId: params.customerId,
88
+ amount: params.amount,
89
+ currency: params.currency,
90
+ quantity: params.quantity,
91
+ paymentIntentId: params.paymentIntentId,
92
+ customParams: params.customParams,
93
+ },
94
+ installationInfo: {
95
+ appId: launcherResult.appId,
96
+ appUrl: launcherResult.appUrl,
97
+ adminUrl: launcherResult.adminUrl,
98
+ status: launcherResult.installationStatus || launcherResult.status,
99
+ estimatedCompletionTime: launcherResult.estimatedCompletionTime,
100
+ },
101
+ };
102
+
103
+ logger.info('Launcher order created successfully', {
104
+ orderId: result.orderId,
105
+ status: result.status,
106
+ serviceUrl: result.serviceUrl,
107
+ });
108
+
109
+ return result;
110
+ } catch (error: any) {
111
+ logger.error('Failed to create launcher order', {
112
+ error: error.message,
113
+ productCode: params.productCode,
114
+ customerId: params.customerId,
115
+ });
116
+
117
+ throw error;
118
+ }
119
+ }
120
+
121
+ async requestReturn(params: ReturnRequestParams): Promise<ReturnRequestResult> {
122
+ logger.debug('Requesting return for Launcher order', {
123
+ orderId: params.orderId,
124
+ reason: params.reason,
125
+ });
126
+
127
+ const vendorConfig = await this.getVendorConfig();
128
+ const launcherApiUrl = vendorConfig.app_url;
129
+ const { headers, body } = VendorAuth.signRequestWithHeaders(params);
130
+
131
+ const response = await fetch(`${launcherApiUrl}/api/vendor/return`, {
132
+ method: 'POST',
133
+ headers,
134
+ body,
135
+ });
136
+
137
+ if (!response.ok) {
138
+ const errorBody = await response.text();
139
+ logger.error('Launcher API error', {
140
+ status: response.status,
141
+ statusText: response.statusText,
142
+ body: errorBody,
143
+ });
144
+ throw new Error(`Launcher API error: ${response.status} ${response.statusText}`);
145
+ }
146
+
147
+ const launcherResult = await response.json();
148
+
149
+ return launcherResult;
150
+ }
151
+
152
+ async checkOrderStatus(params: CheckOrderStatusParams): Promise<CheckOrderStatusResult> {
153
+ const url = `${params.appUrl}/__blocklet__.js?type=json&noCache=1`;
154
+ try {
155
+ const blocklet = await api.get(url, {
156
+ headers: { 'Content-Type': 'application/json' },
157
+ });
158
+ const blockletInfo = blocklet.data;
159
+ let status: 'processing' | 'completed' | 'failed' = 'processing';
160
+ if (blockletInfo.status === 'running') {
161
+ status = 'completed';
162
+ } else if (blockletInfo.status === 'failed') {
163
+ status = 'failed';
164
+ }
165
+
166
+ return { status };
167
+ } catch (error: any) {
168
+ logger.error('Failed to check order status', {
169
+ url,
170
+ error: error.message,
171
+ stack: error.stack,
172
+ });
173
+
174
+ throw error;
175
+ }
176
+ }
177
+ }
@@ -14,6 +14,13 @@ export const meteringSubscriptionDetectionCronTime: string =
14
14
  process.env.METERING_SUBSCRIPTION_DETECTION_CRON_TIME || '0 0 10 * * *'; // 默认每天 10:00 执行
15
15
  export const depositVaultCronTime: string = process.env.DEPOSIT_VAULT_CRON_TIME || '0 */5 * * * *'; // 默认每 5 min 执行一次
16
16
  export const creditConsumptionCronTime: string = process.env.CREDIT_CONSUMPTION_CRON_TIME || '0 */10 * * * *'; // 默认每 10 min 执行一次
17
+ export const vendorStatusCheckCronTime: string = process.env.VENDOR_STATUS_CHECK_CRON_TIME || '0 */10 * * * *'; // 默认每 10 min 执行一次
18
+ export const vendorTimeoutMinutes: number = process.env.VENDOR_TIMEOUT_MINUTES
19
+ ? +process.env.VENDOR_TIMEOUT_MINUTES
20
+ : 10; // 默认 10 分钟超时
21
+
22
+ export const shortUrlApiKey: string = process.env.SHORT_URL_API_KEY || '';
23
+
17
24
  // sequelize 配置相关
18
25
  export const sequelizeOptionsPoolMin: number = process.env.SEQUELIZE_OPTIONS_POOL_MIN
19
26
  ? +process.env.SEQUELIZE_OPTIONS_POOL_MIN
@@ -0,0 +1,77 @@
1
+ import logger from './logger';
2
+ import { shortUrlApiKey } from './env';
3
+
4
+ interface ShortUrlResponse {
5
+ shortUrl: string;
6
+ longUrl: string;
7
+ shortCode: string;
8
+ dateCreated: string;
9
+ }
10
+
11
+ interface ShortUrlRequest {
12
+ longUrl: string;
13
+ validUntil?: string;
14
+ maxVisits?: number;
15
+ tags?: string[];
16
+ shortCodeLength?: number;
17
+ domain?: string;
18
+ findIfExists?: boolean;
19
+ validateUrl?: boolean;
20
+ forwardQuery?: boolean;
21
+ crawlable?: boolean;
22
+ }
23
+
24
+ export async function formatToShortUrl({
25
+ url,
26
+ validUntil,
27
+ maxVisits,
28
+ }: {
29
+ url: string;
30
+ validUntil?: string;
31
+ maxVisits?: number;
32
+ }): Promise<string> {
33
+ const apiKey = shortUrlApiKey;
34
+
35
+ if (!apiKey) {
36
+ return url;
37
+ }
38
+
39
+ try {
40
+ const requestPayload: ShortUrlRequest = {
41
+ longUrl: url,
42
+ validUntil,
43
+ maxVisits,
44
+ tags: [],
45
+ shortCodeLength: 8,
46
+ domain: 's.abtnet.io',
47
+ findIfExists: true,
48
+ validateUrl: true,
49
+ forwardQuery: true,
50
+ crawlable: true,
51
+ };
52
+
53
+ const response = await fetch('https://s.abtnet.io/rest/v3/short-urls', {
54
+ method: 'POST',
55
+ headers: {
56
+ 'Content-Type': 'application/json',
57
+ 'X-Api-Key': apiKey,
58
+ },
59
+ body: JSON.stringify(requestPayload),
60
+ });
61
+
62
+ if (!response.ok) {
63
+ logger.warn('Failed to create short URL, using original URL', {
64
+ status: response.status,
65
+ statusText: response.statusText,
66
+ url,
67
+ });
68
+ return url;
69
+ }
70
+
71
+ const data: ShortUrlResponse = await response.json();
72
+ return data.shortUrl;
73
+ } catch (error) {
74
+ logger.error('Error creating short URL, using original URL', { error, url });
75
+ return url;
76
+ }
77
+ }
@@ -0,0 +1,22 @@
1
+ import { VendorAdapter, VendorConfig } from './vendor-adapter';
2
+ import { LauncherAdapter } from './adapters/launcher-adapter.js';
3
+
4
+ export class VendorAdapterFactory {
5
+ static create(vendorConfig: string | VendorConfig): VendorAdapter {
6
+ const vendorKey = typeof vendorConfig === 'string' ? vendorConfig : vendorConfig.vendor_key;
7
+ switch (vendorKey) {
8
+ case 'launcher':
9
+ return new LauncherAdapter(vendorConfig);
10
+ default:
11
+ throw new Error(`Unsupported vendor: ${vendorConfig}`);
12
+ }
13
+ }
14
+
15
+ static getSupportedVendors(): string[] {
16
+ return ['launcher'];
17
+ }
18
+
19
+ static isSupported(vendorKey: string): boolean {
20
+ return this.getSupportedVendors().includes(vendorKey);
21
+ }
22
+ }
@@ -0,0 +1,109 @@
1
+ import { BN } from '@ocap/util';
2
+
3
+ export interface VendorConfig {
4
+ id: string;
5
+ vendor_key: string;
6
+ name: string;
7
+ description: string;
8
+ app_url: string;
9
+ webhook_path?: string;
10
+ default_commission_rate: number;
11
+ default_commission_type: 'percentage' | 'fixed_amount';
12
+ order_create_params: Record<string, any>;
13
+ status: 'active' | 'inactive';
14
+ metadata: Record<string, any>;
15
+ }
16
+
17
+ export interface FulfillOrderParams {
18
+ productCode: string;
19
+ customerId: string;
20
+ quantity: number;
21
+ paymentIntentId: string;
22
+ amount: string;
23
+ currency: string;
24
+
25
+ description: string;
26
+ userInfo: {
27
+ userDid: string;
28
+ email: string;
29
+ description?: string;
30
+ };
31
+ deliveryParams: {
32
+ blockletMetaUrl: string;
33
+ customParams?: Record<string, any>;
34
+ };
35
+
36
+ customParams?: Record<string, any>;
37
+ }
38
+
39
+ export interface OrderDetails {
40
+ productCode: string;
41
+ customerId: string;
42
+ amount: string;
43
+ currency: string;
44
+ quantity: number;
45
+ paymentIntentId: string;
46
+ customParams?: Record<string, any>;
47
+ }
48
+
49
+ export interface InstallationInfo {
50
+ appId: string;
51
+ appUrl: string;
52
+ adminUrl: string;
53
+ status: 'installing' | 'completed' | 'failed';
54
+ estimatedCompletionTime: string;
55
+ }
56
+
57
+ export interface FulfillOrderResult {
58
+ orderId: string;
59
+ status: 'pending' | 'processing' | 'completed' | 'failed';
60
+ serviceUrl?: string;
61
+ estimatedTime?: number;
62
+ message: string;
63
+ vendorOrderId?: string;
64
+ progress?: number;
65
+ orderDetails?: OrderDetails;
66
+ installationInfo?: InstallationInfo;
67
+ }
68
+
69
+ export interface ReturnRequestParams {
70
+ orderId: string;
71
+ vendorOrderId?: string;
72
+ reason: string;
73
+ paymentIntentId: string;
74
+ customParams?: Record<string, any>;
75
+ }
76
+
77
+ export interface ReturnRequestResult {
78
+ status: 'requested' | 'accepted' | 'rejected' | 'failed';
79
+ message?: string;
80
+ }
81
+
82
+ export interface CheckOrderStatusParams extends Record<string, any> {}
83
+
84
+ export interface CheckOrderStatusResult {
85
+ status: 'processing' | 'completed' | 'failed';
86
+ }
87
+
88
+ export interface VendorAdapter {
89
+ fulfillOrder(params: FulfillOrderParams): Promise<FulfillOrderResult>;
90
+ requestReturn(params: ReturnRequestParams): Promise<ReturnRequestResult>;
91
+ checkOrderStatus(params: CheckOrderStatusParams): Promise<CheckOrderStatusResult>;
92
+ }
93
+
94
+ export function calculateVendorCommission(
95
+ totalAmount: string,
96
+ commissionRate: number,
97
+ commissionType: 'percentage' | 'fixed_amount',
98
+ itemAmount?: string
99
+ ): string {
100
+ const amount = itemAmount || totalAmount || '0';
101
+
102
+ if (commissionType === 'percentage') {
103
+ return new BN(amount)
104
+ .mul(new BN(commissionRate || 0))
105
+ .div(new BN(100))
106
+ .toString();
107
+ }
108
+ return new BN(commissionRate).toString();
109
+ }