payment-kit 1.20.12 → 1.20.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/api/src/crons/index.ts +8 -0
- package/api/src/libs/env.ts +1 -0
- package/api/src/libs/vendor-util/adapters/launcher-adapter.ts +1 -1
- package/api/src/libs/vendor-util/adapters/types.ts +1 -0
- package/api/src/libs/vendor-util/fulfillment.ts +1 -1
- package/api/src/queues/vendors/fulfillment-coordinator.ts +1 -29
- package/api/src/queues/vendors/return-processor.ts +184 -0
- package/api/src/queues/vendors/return-scanner.ts +119 -0
- package/api/src/queues/vendors/status-check.ts +1 -1
- package/api/src/routes/vendor.ts +89 -2
- package/api/src/store/migrations/20250918-add-vendor-extends.ts +20 -0
- package/api/src/store/models/checkout-session.ts +5 -2
- package/api/src/store/models/product-vendor.ts +6 -0
- package/blocklet.yml +1 -1
- package/package.json +5 -5
package/api/src/crons/index.ts
CHANGED
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
stripeSubscriptionCronTime,
|
|
20
20
|
subscriptionCronTime,
|
|
21
21
|
vendorStatusCheckCronTime,
|
|
22
|
+
vendorReturnScanCronTime,
|
|
22
23
|
} from '../libs/env';
|
|
23
24
|
import logger from '../libs/logger';
|
|
24
25
|
import { startCreditConsumeQueue } from '../queues/credit-consume';
|
|
@@ -31,6 +32,7 @@ import { createPaymentStat } from './payment-stat';
|
|
|
31
32
|
import { SubscriptionTrialWillEndSchedule } from './subscription-trial-will-end';
|
|
32
33
|
import { SubscriptionWillCanceledSchedule } from './subscription-will-canceled';
|
|
33
34
|
import { SubscriptionWillRenewSchedule } from './subscription-will-renew';
|
|
35
|
+
import { scheduleVendorReturnScan } from '../queues/vendors/return-scanner';
|
|
34
36
|
|
|
35
37
|
function init() {
|
|
36
38
|
Cron.init({
|
|
@@ -123,6 +125,12 @@ function init() {
|
|
|
123
125
|
fn: () => startVendorStatusCheckSchedule(),
|
|
124
126
|
options: { runOnInit: false },
|
|
125
127
|
},
|
|
128
|
+
{
|
|
129
|
+
name: 'vendor.return.scan',
|
|
130
|
+
time: vendorReturnScanCronTime,
|
|
131
|
+
fn: () => scheduleVendorReturnScan(),
|
|
132
|
+
options: { runOnInit: false },
|
|
133
|
+
},
|
|
126
134
|
],
|
|
127
135
|
onError: (error: Error, name: string) => {
|
|
128
136
|
logger.error('run job failed', { name, error });
|
package/api/src/libs/env.ts
CHANGED
|
@@ -15,6 +15,7 @@ export const meteringSubscriptionDetectionCronTime: string =
|
|
|
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
17
|
export const vendorStatusCheckCronTime: string = process.env.VENDOR_STATUS_CHECK_CRON_TIME || '0 */10 * * * *'; // 默认每 10 min 执行一次
|
|
18
|
+
export const vendorReturnScanCronTime: string = process.env.VENDOR_RETURN_SCAN_CRON_TIME || '0 */10 * * * *'; // 默认每 10 min 执行一次
|
|
18
19
|
export const vendorTimeoutMinutes: number = process.env.VENDOR_TIMEOUT_MINUTES
|
|
19
20
|
? +process.env.VENDOR_TIMEOUT_MINUTES
|
|
20
21
|
: 10; // 默认 10 分钟超时
|
|
@@ -73,6 +73,7 @@ export interface ReturnRequestParams {
|
|
|
73
73
|
export interface ReturnRequestResult {
|
|
74
74
|
status: 'requested' | 'accepted' | 'rejected' | 'failed';
|
|
75
75
|
message?: string;
|
|
76
|
+
success?: boolean;
|
|
76
77
|
}
|
|
77
78
|
|
|
78
79
|
export interface CheckOrderStatusParams extends Record<string, any> {}
|
|
@@ -13,35 +13,7 @@ import { Refund } from '../../store/models/refund';
|
|
|
13
13
|
import { sequelize } from '../../store/sequelize';
|
|
14
14
|
import { depositVaultQueue } from '../payment';
|
|
15
15
|
|
|
16
|
-
type VendorInfo =
|
|
17
|
-
vendor_id: string;
|
|
18
|
-
vendor_key: string;
|
|
19
|
-
order_id: string;
|
|
20
|
-
status:
|
|
21
|
-
| 'pending'
|
|
22
|
-
| 'processing'
|
|
23
|
-
| 'completed'
|
|
24
|
-
| 'failed'
|
|
25
|
-
| 'cancelled'
|
|
26
|
-
| 'max_retries_exceeded'
|
|
27
|
-
| 'return_requested'
|
|
28
|
-
| 'sent';
|
|
29
|
-
service_url?: string;
|
|
30
|
-
error_message?: string;
|
|
31
|
-
amount: string;
|
|
32
|
-
|
|
33
|
-
attempts?: number;
|
|
34
|
-
lastAttemptAt?: string;
|
|
35
|
-
completedAt?: string;
|
|
36
|
-
commissionAmount?: string;
|
|
37
|
-
|
|
38
|
-
returnRequest?: {
|
|
39
|
-
reason: string;
|
|
40
|
-
requestedAt: string;
|
|
41
|
-
status: 'pending' | 'accepted' | 'rejected';
|
|
42
|
-
returnDetails?: string;
|
|
43
|
-
};
|
|
44
|
-
};
|
|
16
|
+
export type VendorInfo = NonNullable<CheckoutSession['vendor_info']>[number];
|
|
45
17
|
|
|
46
18
|
interface CoordinatorJob {
|
|
47
19
|
checkoutSessionId: string;
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import logger from '../../libs/logger';
|
|
2
|
+
import createQueue from '../../libs/queue';
|
|
3
|
+
import { VendorFulfillmentService } from '../../libs/vendor-util/fulfillment';
|
|
4
|
+
import { CheckoutSession } from '../../store/models';
|
|
5
|
+
import { VendorInfo } from './fulfillment-coordinator';
|
|
6
|
+
|
|
7
|
+
type ReturnProcessorJob = {
|
|
8
|
+
checkoutSessionId: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export const vendorReturnProcessorQueue = createQueue<ReturnProcessorJob>({
|
|
12
|
+
name: 'vendor-return-processor',
|
|
13
|
+
onJob: handleReturnProcessorJob,
|
|
14
|
+
options: {
|
|
15
|
+
concurrency: 1,
|
|
16
|
+
maxRetries: 2,
|
|
17
|
+
retryDelay: 30 * 1000,
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
async function handleReturnProcessorJob(job: ReturnProcessorJob): Promise<void> {
|
|
22
|
+
const { checkoutSessionId } = job;
|
|
23
|
+
|
|
24
|
+
logger.info('Starting vendor return processor job', {
|
|
25
|
+
checkoutSessionId,
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
const checkoutSession = await CheckoutSession.findByPk(checkoutSessionId);
|
|
30
|
+
|
|
31
|
+
if (!checkoutSession) {
|
|
32
|
+
logger.warn('CheckoutSession not found', { checkoutSessionId });
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const vendorInfoList = checkoutSession.vendor_info as VendorInfo[];
|
|
37
|
+
let hasChanges = false;
|
|
38
|
+
|
|
39
|
+
let i = -1;
|
|
40
|
+
for (const vendor of vendorInfoList) {
|
|
41
|
+
i++;
|
|
42
|
+
// Only process vendors with 'completed' status
|
|
43
|
+
if (vendor.status !== 'completed') {
|
|
44
|
+
logger.info('Skipping vendor return because status is not completed', {
|
|
45
|
+
checkoutSessionId,
|
|
46
|
+
vendorId: vendor.vendor_id,
|
|
47
|
+
orderId: vendor.order_id,
|
|
48
|
+
status: vendor.status,
|
|
49
|
+
});
|
|
50
|
+
// eslint-disable-next-line no-continue
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
logger.info('Processing vendor return', {
|
|
56
|
+
checkoutSessionId,
|
|
57
|
+
vendorId: vendor.vendor_id,
|
|
58
|
+
orderId: vendor.order_id,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// eslint-disable-next-line no-await-in-loop
|
|
62
|
+
const returnResult = await callVendorReturn(vendor, checkoutSession);
|
|
63
|
+
|
|
64
|
+
if (returnResult.success) {
|
|
65
|
+
// Return successful, update status to 'returned'
|
|
66
|
+
vendorInfoList[i] = {
|
|
67
|
+
...vendor,
|
|
68
|
+
status: 'returned',
|
|
69
|
+
lastAttemptAt: new Date().toISOString(),
|
|
70
|
+
};
|
|
71
|
+
hasChanges = true;
|
|
72
|
+
|
|
73
|
+
logger.info('Vendor return successful', {
|
|
74
|
+
checkoutSessionId,
|
|
75
|
+
vendorId: vendor.vendor_id,
|
|
76
|
+
orderId: vendor.order_id,
|
|
77
|
+
});
|
|
78
|
+
} else {
|
|
79
|
+
// Return failed, keep 'completed' status for next scan retry
|
|
80
|
+
vendorInfoList[i] = {
|
|
81
|
+
...vendor,
|
|
82
|
+
lastAttemptAt: new Date().toISOString(),
|
|
83
|
+
error_message: returnResult.message || 'Return request failed',
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
logger.warn('Vendor return failed', {
|
|
87
|
+
checkoutSessionId,
|
|
88
|
+
vendorId: vendor.vendor_id,
|
|
89
|
+
orderId: vendor.order_id,
|
|
90
|
+
error: returnResult.message,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
} catch (error: any) {
|
|
94
|
+
logger.error('Error processing vendor return', {
|
|
95
|
+
checkoutSessionId,
|
|
96
|
+
vendorId: vendor.vendor_id,
|
|
97
|
+
orderId: vendor.order_id,
|
|
98
|
+
error: error.message,
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// Record error but keep status unchanged for retry
|
|
102
|
+
vendorInfoList[i] = {
|
|
103
|
+
...vendor,
|
|
104
|
+
lastAttemptAt: new Date().toISOString(),
|
|
105
|
+
error_message: error.message,
|
|
106
|
+
};
|
|
107
|
+
hasChanges = true;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Update vendor_info if there are changes
|
|
112
|
+
if (hasChanges) {
|
|
113
|
+
await checkoutSession.update({ vendor_info: vendorInfoList });
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Check if all vendors have been returned
|
|
117
|
+
const allReturned = vendorInfoList.every((vendor) => vendor.status === 'returned');
|
|
118
|
+
|
|
119
|
+
if (allReturned && checkoutSession.fulfillment_status !== 'returned') {
|
|
120
|
+
await checkoutSession.update({ fulfillment_status: 'returned' });
|
|
121
|
+
|
|
122
|
+
logger.info('All vendors returned, updated fulfillment status to returned', {
|
|
123
|
+
checkoutSessionId,
|
|
124
|
+
totalVendors: vendorInfoList.length,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
logger.info('Vendor return processor job completed', {
|
|
129
|
+
checkoutSessionId,
|
|
130
|
+
totalVendors: vendorInfoList.length,
|
|
131
|
+
allReturned,
|
|
132
|
+
hasChanges,
|
|
133
|
+
});
|
|
134
|
+
} catch (error: any) {
|
|
135
|
+
logger.error('Vendor return processor job failed', {
|
|
136
|
+
checkoutSessionId,
|
|
137
|
+
error,
|
|
138
|
+
});
|
|
139
|
+
throw error;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function callVendorReturn(
|
|
144
|
+
vendor: VendorInfo,
|
|
145
|
+
checkoutSession: CheckoutSession
|
|
146
|
+
): Promise<{ success: boolean; message?: string }> {
|
|
147
|
+
try {
|
|
148
|
+
const vendorAdapter = await VendorFulfillmentService.getVendorAdapter(vendor.vendor_key);
|
|
149
|
+
|
|
150
|
+
if (!vendorAdapter) {
|
|
151
|
+
return {
|
|
152
|
+
success: false,
|
|
153
|
+
message: `No adapter found for vendor: ${vendor.vendor_id}`,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const returnResult = await vendorAdapter.requestReturn({
|
|
158
|
+
orderId: vendor.order_id,
|
|
159
|
+
reason: 'Subscription canceled',
|
|
160
|
+
paymentIntentId: checkoutSession.payment_intent_id || '',
|
|
161
|
+
customParams: {
|
|
162
|
+
checkoutSessionId: checkoutSession.id,
|
|
163
|
+
subscriptionId: checkoutSession.subscription_id,
|
|
164
|
+
vendorKey: vendor.vendor_key,
|
|
165
|
+
},
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
success: returnResult.success || false,
|
|
170
|
+
message: returnResult.message,
|
|
171
|
+
};
|
|
172
|
+
} catch (error: any) {
|
|
173
|
+
logger.error('Failed to call vendor return API', {
|
|
174
|
+
vendorId: vendor.vendor_id,
|
|
175
|
+
orderId: vendor.order_id,
|
|
176
|
+
error: error.message,
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
success: false,
|
|
181
|
+
message: error.message,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { Op } from 'sequelize';
|
|
2
|
+
import logger from '../../libs/logger';
|
|
3
|
+
import createQueue from '../../libs/queue';
|
|
4
|
+
import { CheckoutSession, Subscription } from '../../store/models';
|
|
5
|
+
import { vendorReturnProcessorQueue } from './return-processor';
|
|
6
|
+
import { VendorInfo } from './fulfillment-coordinator';
|
|
7
|
+
|
|
8
|
+
export const vendorReturnScannerQueue = createQueue({
|
|
9
|
+
name: 'vendor-return-scanner',
|
|
10
|
+
onJob: handleReturnScannerJob,
|
|
11
|
+
options: {
|
|
12
|
+
concurrency: 1,
|
|
13
|
+
maxRetries: 3,
|
|
14
|
+
retryDelay: 60 * 1000,
|
|
15
|
+
},
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
async function handleReturnScannerJob(): Promise<void> {
|
|
19
|
+
try {
|
|
20
|
+
const sessionsNeedingReturn = await findSessionsNeedingVendorReturn();
|
|
21
|
+
if (sessionsNeedingReturn.length === 0) {
|
|
22
|
+
logger.info('No checkout sessions needing vendor return');
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
logger.info('Found checkout sessions needing vendor return', {
|
|
27
|
+
count: sessionsNeedingReturn.length,
|
|
28
|
+
sessionIds: sessionsNeedingReturn.map((s) => s.id),
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
for (const session of sessionsNeedingReturn) {
|
|
32
|
+
const id = `vendor-return-process-${session.id}`;
|
|
33
|
+
// eslint-disable-next-line no-await-in-loop
|
|
34
|
+
const exists = await vendorReturnProcessorQueue.get(id);
|
|
35
|
+
if (!exists) {
|
|
36
|
+
vendorReturnProcessorQueue.push({
|
|
37
|
+
id,
|
|
38
|
+
job: {
|
|
39
|
+
checkoutSessionId: session.id,
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
} catch (error: any) {
|
|
45
|
+
logger.error('Vendor return scanner job failed', {
|
|
46
|
+
error,
|
|
47
|
+
});
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function findSessionsNeedingVendorReturn(): Promise<CheckoutSession[]> {
|
|
53
|
+
try {
|
|
54
|
+
// First, find canceled subscriptions
|
|
55
|
+
const canceledSubscriptions = await Subscription.findAll({
|
|
56
|
+
where: { status: 'canceled' },
|
|
57
|
+
attributes: ['id'],
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const canceledSubscriptionIds = canceledSubscriptions.map((sub) => sub.id);
|
|
61
|
+
|
|
62
|
+
// Find checkout sessions with completed fulfillment and canceled subscriptions
|
|
63
|
+
const readyToReturnSessions = await CheckoutSession.findAll({
|
|
64
|
+
where: {
|
|
65
|
+
fulfillment_status: 'completed',
|
|
66
|
+
subscription_id: { [Op.in]: canceledSubscriptionIds },
|
|
67
|
+
},
|
|
68
|
+
order: [['updated_at', 'DESC']],
|
|
69
|
+
limit: 100,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// Find checkout sessions already in returning status
|
|
73
|
+
const returningSessions = await CheckoutSession.findAll({
|
|
74
|
+
where: {
|
|
75
|
+
fulfillment_status: 'returning',
|
|
76
|
+
subscription_id: { [Op.ne]: null as any },
|
|
77
|
+
},
|
|
78
|
+
order: [['updated_at', 'DESC']],
|
|
79
|
+
limit: 100,
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// Update canceled sessions to returning status
|
|
83
|
+
if (readyToReturnSessions.length > 0) {
|
|
84
|
+
await CheckoutSession.update(
|
|
85
|
+
{
|
|
86
|
+
fulfillment_status: 'returning',
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
where: {
|
|
90
|
+
id: { [Op.in]: readyToReturnSessions.map((s) => s.id) },
|
|
91
|
+
},
|
|
92
|
+
}
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const sessions = [...readyToReturnSessions, ...returningSessions];
|
|
97
|
+
|
|
98
|
+
// Filter sessions that have vendors needing return
|
|
99
|
+
const filteredSessions = sessions.filter((session) => {
|
|
100
|
+
const vendorInfoList = session.vendor_info as VendorInfo[];
|
|
101
|
+
|
|
102
|
+
if (!vendorInfoList || vendorInfoList.length === 0) {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
const hasVendorNeedingReturn = vendorInfoList.some((vendor) => vendor.status === 'completed');
|
|
106
|
+
return hasVendorNeedingReturn;
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
return filteredSessions;
|
|
110
|
+
} catch (error: any) {
|
|
111
|
+
logger.error('Failed to find sessions needing vendor return', { error });
|
|
112
|
+
throw error;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function scheduleVendorReturnScan(): void {
|
|
117
|
+
const scanId = `scan-${Date.now()}`;
|
|
118
|
+
vendorReturnScannerQueue.push({ id: scanId, job: {} });
|
|
119
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { joinURL } from 'ufo';
|
|
2
|
-
import { VendorAuth } from '@blocklet/payment-vendor';
|
|
2
|
+
import { Auth as VendorAuth } from '@blocklet/payment-vendor';
|
|
3
3
|
import createQueue from '../../libs/queue';
|
|
4
4
|
import { CheckoutSession } from '../../store/models/checkout-session';
|
|
5
5
|
import { ProductVendor } from '../../store/models';
|
package/api/src/routes/vendor.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import { getUrl } from '@blocklet/sdk/lib/component';
|
|
1
2
|
import { Router } from 'express';
|
|
2
3
|
import Joi from 'joi';
|
|
3
4
|
|
|
4
|
-
import { VendorAuth } from '@blocklet/payment-vendor';
|
|
5
|
+
import { Auth as VendorAuth, middleware } from '@blocklet/payment-vendor';
|
|
5
6
|
import { joinURL } from 'ufo';
|
|
6
7
|
import { MetadataSchema } from '../libs/api';
|
|
7
8
|
import { wallet } from '../libs/auth';
|
|
@@ -9,7 +10,8 @@ import dayjs from '../libs/dayjs';
|
|
|
9
10
|
import logger from '../libs/logger';
|
|
10
11
|
import { authenticate } from '../libs/security';
|
|
11
12
|
import { formatToShortUrl } from '../libs/url';
|
|
12
|
-
import {
|
|
13
|
+
import { getBlockletJson } from '../libs/util';
|
|
14
|
+
import { CheckoutSession, Invoice, Subscription } from '../store/models';
|
|
13
15
|
import { ProductVendor } from '../store/models/product-vendor';
|
|
14
16
|
|
|
15
17
|
const authAdmin = authenticate<CheckoutSession>({ component: true, roles: ['owner', 'admin'] });
|
|
@@ -159,6 +161,8 @@ async function createVendor(req: any, res: any) {
|
|
|
159
161
|
return res.status(400).json({ error: 'Vendor key already exists' });
|
|
160
162
|
}
|
|
161
163
|
|
|
164
|
+
const blockletJson = await getBlockletJson(appUrl);
|
|
165
|
+
|
|
162
166
|
const vendor = await ProductVendor.create({
|
|
163
167
|
vendor_key: vendorKey,
|
|
164
168
|
vendor_type: vendorType || 'launcher',
|
|
@@ -170,6 +174,10 @@ async function createVendor(req: any, res: any) {
|
|
|
170
174
|
app_pid: appPid,
|
|
171
175
|
app_logo: appLogo,
|
|
172
176
|
metadata: metadata || {},
|
|
177
|
+
extends: {
|
|
178
|
+
appId: blockletJson?.appId,
|
|
179
|
+
appPk: blockletJson?.appPk,
|
|
180
|
+
},
|
|
173
181
|
created_by: req.user?.did || 'admin',
|
|
174
182
|
});
|
|
175
183
|
|
|
@@ -210,6 +218,8 @@ async function updateVendor(req: any, res: any) {
|
|
|
210
218
|
app_logo: appLogo,
|
|
211
219
|
} = value;
|
|
212
220
|
|
|
221
|
+
const blockletJson = await getBlockletJson(appUrl);
|
|
222
|
+
|
|
213
223
|
if (req.body.vendorKey && req.body.vendorKey !== vendor.vendor_key) {
|
|
214
224
|
const existingVendor = await ProductVendor.findOne({
|
|
215
225
|
where: { vendor_key: req.body.vendorKey },
|
|
@@ -229,6 +239,10 @@ async function updateVendor(req: any, res: any) {
|
|
|
229
239
|
app_pid: appPid,
|
|
230
240
|
app_logo: appLogo,
|
|
231
241
|
vendor_key: req.body.vendor_key,
|
|
242
|
+
extends: {
|
|
243
|
+
appId: blockletJson?.appId,
|
|
244
|
+
appPk: blockletJson?.appPk,
|
|
245
|
+
},
|
|
232
246
|
};
|
|
233
247
|
|
|
234
248
|
await vendor.update(Object.fromEntries(Object.entries(updates).filter(([, v]) => v !== undefined)));
|
|
@@ -362,9 +376,23 @@ async function getVendorStatus(sessionId: string, isDetail = false) {
|
|
|
362
376
|
return getVendorStatusByVendorId(item.vendor_id, item.order_id, isDetail);
|
|
363
377
|
});
|
|
364
378
|
|
|
379
|
+
const subscriptionId = doc.subscription_id;
|
|
380
|
+
let shortSubscriptionUrl = '';
|
|
381
|
+
|
|
382
|
+
if (isDetail && subscriptionId) {
|
|
383
|
+
const subscriptionUrl = getUrl(`/customer/subscription/${subscriptionId}`);
|
|
384
|
+
|
|
385
|
+
shortSubscriptionUrl = await formatToShortUrl({
|
|
386
|
+
url: subscriptionUrl,
|
|
387
|
+
maxVisits: 5,
|
|
388
|
+
validUntil: dayjs().add(20, 'minutes').format('YYYY-MM-DDTHH:mm:ss+00:00'),
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
|
|
365
392
|
return {
|
|
366
393
|
payment_status: doc.payment_status,
|
|
367
394
|
session_status: doc.status,
|
|
395
|
+
subscriptionUrl: shortSubscriptionUrl,
|
|
368
396
|
vendors: await Promise.all(vendors),
|
|
369
397
|
error: null,
|
|
370
398
|
};
|
|
@@ -443,12 +471,71 @@ async function redirectToVendor(req: any, res: any) {
|
|
|
443
471
|
}
|
|
444
472
|
}
|
|
445
473
|
|
|
474
|
+
async function getVendorSubscription(req: any, res: any) {
|
|
475
|
+
const { sessionId } = req.params;
|
|
476
|
+
|
|
477
|
+
const checkoutSession = await CheckoutSession.findByPk(sessionId);
|
|
478
|
+
|
|
479
|
+
if (!checkoutSession) {
|
|
480
|
+
return res.status(404).json({ error: 'Checkout session not found' });
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
const subscription = await Subscription.findByPk(checkoutSession.subscription_id);
|
|
484
|
+
|
|
485
|
+
if (!subscription) {
|
|
486
|
+
return res.status(404).json({ error: 'Subscription not found' });
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
const invoices = await Invoice.findAll({
|
|
490
|
+
where: { subscription_id: subscription.id },
|
|
491
|
+
order: [['created_at', 'DESC']],
|
|
492
|
+
attributes: [
|
|
493
|
+
'id',
|
|
494
|
+
'amount_due',
|
|
495
|
+
'amount_paid',
|
|
496
|
+
'amount_remaining',
|
|
497
|
+
'status',
|
|
498
|
+
'currency_id',
|
|
499
|
+
'period_start',
|
|
500
|
+
'period_end',
|
|
501
|
+
'created_at',
|
|
502
|
+
'due_date',
|
|
503
|
+
'description',
|
|
504
|
+
'invoice_pdf',
|
|
505
|
+
],
|
|
506
|
+
limit: 20,
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
return res.json({
|
|
510
|
+
subscription: subscription.toJSON(),
|
|
511
|
+
billing_history: invoices,
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
async function handleSubscriptionRedirect(req: any, res: any) {
|
|
516
|
+
const { sessionId } = req.params;
|
|
517
|
+
|
|
518
|
+
const checkoutSession = await CheckoutSession.findByPk(sessionId);
|
|
519
|
+
if (!checkoutSession) {
|
|
520
|
+
return res.status(404).json({ error: 'Checkout session not found' });
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
return res.redirect(getUrl(`/customer/subscription/${checkoutSession.subscription_id}`));
|
|
524
|
+
}
|
|
525
|
+
|
|
446
526
|
const router = Router();
|
|
447
527
|
|
|
528
|
+
const ensureVendorAuth = middleware.ensureVendorAuth((vendorPk: string) =>
|
|
529
|
+
ProductVendor.findOne({ where: { 'extends.appPk': vendorPk } }).then((v) => v as any)
|
|
530
|
+
);
|
|
531
|
+
|
|
448
532
|
// FIXME: Authentication not yet added, awaiting implementation @Pengfei
|
|
449
533
|
router.get('/order/:sessionId/status', validateParams(sessionIdParamSchema), getVendorFulfillmentStatus);
|
|
450
534
|
router.get('/order/:sessionId/detail', validateParams(sessionIdParamSchema), getVendorFulfillmentDetail);
|
|
451
535
|
|
|
536
|
+
router.get('/subscription/:sessionId/redirect', handleSubscriptionRedirect);
|
|
537
|
+
router.get('/subscription/:sessionId', ensureVendorAuth, getVendorSubscription);
|
|
538
|
+
|
|
452
539
|
router.get(
|
|
453
540
|
'/open/:subscriptionId',
|
|
454
541
|
authAdmin,
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { safeApplyColumnChanges, type Migration } from '../migrate';
|
|
2
|
+
|
|
3
|
+
export const up: Migration = async ({ context }) => {
|
|
4
|
+
// Add extends column to product_vendors table
|
|
5
|
+
await safeApplyColumnChanges(context, {
|
|
6
|
+
product_vendors: [
|
|
7
|
+
{
|
|
8
|
+
name: 'extends',
|
|
9
|
+
field: {
|
|
10
|
+
type: 'JSON',
|
|
11
|
+
allowNull: true,
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
],
|
|
15
|
+
});
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const down: Migration = async ({ context }) => {
|
|
19
|
+
await context.removeColumn('product_vendors', 'extends');
|
|
20
|
+
};
|
|
@@ -212,7 +212,9 @@ export class CheckoutSession extends Model<InferAttributes<CheckoutSession>, Inf
|
|
|
212
212
|
| 'cancelled'
|
|
213
213
|
| 'max_retries_exceeded'
|
|
214
214
|
| 'return_requested'
|
|
215
|
-
| 'sent'
|
|
215
|
+
| 'sent'
|
|
216
|
+
| 'returning'
|
|
217
|
+
| 'returned',
|
|
216
218
|
string
|
|
217
219
|
>;
|
|
218
220
|
declare vendor_info?: Array<{
|
|
@@ -227,7 +229,8 @@ export class CheckoutSession extends Model<InferAttributes<CheckoutSession>, Inf
|
|
|
227
229
|
| 'cancelled'
|
|
228
230
|
| 'max_retries_exceeded'
|
|
229
231
|
| 'return_requested'
|
|
230
|
-
| 'sent'
|
|
232
|
+
| 'sent'
|
|
233
|
+
| 'returned';
|
|
231
234
|
service_url?: string;
|
|
232
235
|
app_url?: string;
|
|
233
236
|
error_message?: string;
|
|
@@ -19,6 +19,7 @@ export class ProductVendor extends Model<InferAttributes<ProductVendor>, InferCr
|
|
|
19
19
|
|
|
20
20
|
declare status: 'active' | 'inactive';
|
|
21
21
|
declare metadata: Record<string, any>;
|
|
22
|
+
declare extends: Record<string, any>;
|
|
22
23
|
|
|
23
24
|
declare created_by: string;
|
|
24
25
|
declare created_at: CreationOptional<Date>;
|
|
@@ -75,6 +76,11 @@ export class ProductVendor extends Model<InferAttributes<ProductVendor>, InferCr
|
|
|
75
76
|
allowNull: true,
|
|
76
77
|
defaultValue: {},
|
|
77
78
|
},
|
|
79
|
+
extends: {
|
|
80
|
+
type: DataTypes.JSON,
|
|
81
|
+
allowNull: true,
|
|
82
|
+
defaultValue: {},
|
|
83
|
+
},
|
|
78
84
|
created_by: {
|
|
79
85
|
type: DataTypes.STRING(30),
|
|
80
86
|
allowNull: true,
|
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.13",
|
|
4
4
|
"scripts": {
|
|
5
5
|
"dev": "blocklet dev --open",
|
|
6
6
|
"lint": "tsc --noEmit && eslint src api/src --ext .mjs,.js,.jsx,.ts,.tsx",
|
|
@@ -56,8 +56,8 @@
|
|
|
56
56
|
"@blocklet/error": "^0.2.5",
|
|
57
57
|
"@blocklet/js-sdk": "^1.16.52-beta-20250912-112002-e3499e9c",
|
|
58
58
|
"@blocklet/logger": "^1.16.52-beta-20250912-112002-e3499e9c",
|
|
59
|
-
"@blocklet/payment-react": "1.20.
|
|
60
|
-
"@blocklet/payment-vendor": "1.20.
|
|
59
|
+
"@blocklet/payment-react": "1.20.13",
|
|
60
|
+
"@blocklet/payment-vendor": "1.20.13",
|
|
61
61
|
"@blocklet/sdk": "^1.16.52-beta-20250912-112002-e3499e9c",
|
|
62
62
|
"@blocklet/ui-react": "^3.1.41",
|
|
63
63
|
"@blocklet/uploader": "^0.2.11",
|
|
@@ -126,7 +126,7 @@
|
|
|
126
126
|
"devDependencies": {
|
|
127
127
|
"@abtnode/types": "^1.16.52-beta-20250912-112002-e3499e9c",
|
|
128
128
|
"@arcblock/eslint-config-ts": "^0.3.3",
|
|
129
|
-
"@blocklet/payment-types": "1.20.
|
|
129
|
+
"@blocklet/payment-types": "1.20.13",
|
|
130
130
|
"@types/cookie-parser": "^1.4.9",
|
|
131
131
|
"@types/cors": "^2.8.19",
|
|
132
132
|
"@types/debug": "^4.1.12",
|
|
@@ -173,5 +173,5 @@
|
|
|
173
173
|
"parser": "typescript"
|
|
174
174
|
}
|
|
175
175
|
},
|
|
176
|
-
"gitHead": "
|
|
176
|
+
"gitHead": "0cbe918549f7d06561b5307fb947bfbbd2250984"
|
|
177
177
|
}
|