payment-kit 1.18.34 → 1.18.36
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/subscription-trial-will-end.ts +1 -1
- package/api/src/index.ts +2 -0
- package/api/src/libs/did-space.ts +235 -0
- package/api/src/libs/util.ts +33 -1
- package/api/src/queues/payment.ts +7 -4
- package/api/src/queues/space.ts +661 -0
- package/api/src/queues/subscription.ts +1 -1
- package/api/src/routes/customers.ts +47 -0
- package/api/src/store/models/invoice.ts +12 -3
- package/api/tests/libs/util.spec.ts +215 -1
- package/blocklet.yml +2 -1
- package/package.json +10 -9
- package/scripts/sdk.js +58 -2
- package/src/components/customer/link.tsx +32 -57
- package/src/components/invoice/list.tsx +1 -1
- package/src/components/payouts/list.tsx +8 -2
- package/src/libs/util.ts +0 -13
- package/src/pages/admin/customers/customers/detail.tsx +2 -2
- package/src/pages/admin/customers/customers/index.tsx +1 -2
- package/src/pages/admin/payments/payouts/detail.tsx +1 -1
- package/src/pages/customer/recharge/account.tsx +2 -1
- package/src/pages/customer/recharge/subscription.tsx +2 -1
- package/src/pages/customer/subscription/embed.tsx +1 -0
|
@@ -25,7 +25,7 @@ export class SubscriptionTrialWillEndSchedule extends BaseSubscriptionScheduleNo
|
|
|
25
25
|
},
|
|
26
26
|
status: 'trialing',
|
|
27
27
|
},
|
|
28
|
-
attributes: ['id', 'current_period_start', 'current_period_end', 'trial_end'],
|
|
28
|
+
attributes: ['id', 'current_period_start', 'current_period_end', 'trial_end', 'status'],
|
|
29
29
|
raw: true,
|
|
30
30
|
});
|
|
31
31
|
|
package/api/src/index.ts
CHANGED
|
@@ -43,6 +43,7 @@ import rechargeAccountHandlers from './routes/connect/recharge-account';
|
|
|
43
43
|
import { initialize } from './store/models';
|
|
44
44
|
import { sequelize } from './store/sequelize';
|
|
45
45
|
import { initUserHandler } from './integrations/blocklet/user';
|
|
46
|
+
import { startUploadBillingInfoListener } from './queues/space';
|
|
46
47
|
|
|
47
48
|
dotenv.config();
|
|
48
49
|
|
|
@@ -115,6 +116,7 @@ export const server = app.listen(port, (err?: any) => {
|
|
|
115
116
|
startCheckoutSessionQueue().then(() => logger.info('checkoutSession queue started'));
|
|
116
117
|
startNotificationQueue().then(() => logger.info('notification queue started'));
|
|
117
118
|
startRefundQueue().then(() => logger.info('refund queue started'));
|
|
119
|
+
startUploadBillingInfoListener();
|
|
118
120
|
|
|
119
121
|
if (process.env.BLOCKLET_MODE === 'production') {
|
|
120
122
|
ensureWebhookRegistered().catch(console.error);
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
// eslint-disable-next-line import/no-extraneous-dependencies
|
|
2
|
+
import { SpaceClient, GetObjectCommand, PutObjectCommand } from '@blocklet/did-space-js';
|
|
3
|
+
import { blocklet, wallet } from './auth';
|
|
4
|
+
import logger from './logger';
|
|
5
|
+
import env from './env';
|
|
6
|
+
import { streamToString } from './util';
|
|
7
|
+
|
|
8
|
+
// Get user's DID Space endpoint
|
|
9
|
+
export const getEndpointAndSpaceDid = async (userDid: string): Promise<{ endpoint: string; spaceDid: string }> => {
|
|
10
|
+
const { user } = await blocklet.getUser(userDid);
|
|
11
|
+
if (!user) {
|
|
12
|
+
throw new Error('User not found');
|
|
13
|
+
}
|
|
14
|
+
if (!user.didSpace.endpoint) {
|
|
15
|
+
throw new Error(`DID Space endpoint is not set for user ${userDid}`);
|
|
16
|
+
}
|
|
17
|
+
return {
|
|
18
|
+
endpoint: user.didSpace.endpoint,
|
|
19
|
+
spaceDid: user.didSpace.did,
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// Initialize DID Space client
|
|
24
|
+
export const getSpaceClient = async (userDid: string, endpoint?: string) => {
|
|
25
|
+
let spaceEndpoint = endpoint;
|
|
26
|
+
if (!spaceEndpoint) {
|
|
27
|
+
const result = await getEndpointAndSpaceDid(userDid);
|
|
28
|
+
spaceEndpoint = result.endpoint;
|
|
29
|
+
}
|
|
30
|
+
return new SpaceClient({
|
|
31
|
+
endpoint: spaceEndpoint,
|
|
32
|
+
wallet,
|
|
33
|
+
});
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// Universal billing information format
|
|
37
|
+
export interface BillingInfo {
|
|
38
|
+
tx_hash: string; // Transaction hash
|
|
39
|
+
timestamp: number; // Transaction timestamp
|
|
40
|
+
|
|
41
|
+
// Basic billing information
|
|
42
|
+
invoice_id: string; // Invoice ID
|
|
43
|
+
category: string; // Bill category
|
|
44
|
+
description?: string; // Bill description: subscription payment, recharge, refund, etc.
|
|
45
|
+
|
|
46
|
+
// Payment information
|
|
47
|
+
amount: string; // Payment amount
|
|
48
|
+
currency: {
|
|
49
|
+
// Payment medium information
|
|
50
|
+
symbol: string; // Currency symbol
|
|
51
|
+
decimal: number; // Decimal places
|
|
52
|
+
type: string; // Payment type (arcblock/ethereum)
|
|
53
|
+
chain_id: string; // Chain ID
|
|
54
|
+
explorer_host: string; // Explorer host
|
|
55
|
+
explorer_tx_url: string; // Explorer transaction URL
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// Related business information
|
|
59
|
+
related?: {
|
|
60
|
+
type: string; // Relation type (subscription/invoice)
|
|
61
|
+
id: string; // Related entity ID
|
|
62
|
+
name: string; // Related entity name
|
|
63
|
+
status?: string; // Related entity status
|
|
64
|
+
link?: string; // Link to the related entity
|
|
65
|
+
period_start?: number; // Period start time
|
|
66
|
+
period_end?: number; // Period end time
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
customer_did: string; // Customer DID
|
|
70
|
+
tags?: string[]; // User defined tags
|
|
71
|
+
app_pid: string; // App PID
|
|
72
|
+
link?: string; // Link to the related entity
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Merge billing information, preserving user customizations
|
|
76
|
+
const mergeBillingInfo = (existing: BillingInfo, updated: Partial<BillingInfo>): BillingInfo => {
|
|
77
|
+
return {
|
|
78
|
+
...(existing || {}),
|
|
79
|
+
...updated,
|
|
80
|
+
// Preserve user customizations if they exist
|
|
81
|
+
description: existing?.description || updated?.description,
|
|
82
|
+
category: existing?.category || updated?.category || 'other',
|
|
83
|
+
tags: Array.from(new Set([...(existing.tags || []), ...(updated.tags || [])])),
|
|
84
|
+
};
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
// Upload or update billing information to DID Space
|
|
88
|
+
export const uploadBillingInfo = async (
|
|
89
|
+
userDid: string,
|
|
90
|
+
billingInfo: BillingInfo,
|
|
91
|
+
endpoint?: string
|
|
92
|
+
): Promise<boolean> => {
|
|
93
|
+
try {
|
|
94
|
+
// Validate required fields
|
|
95
|
+
if (!billingInfo.tx_hash || !billingInfo.invoice_id || !billingInfo.amount) {
|
|
96
|
+
logger.error('Missing required fields in billingInfo');
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const client = await getSpaceClient(userDid, endpoint);
|
|
101
|
+
|
|
102
|
+
const key = `txs/${billingInfo.tx_hash}/metadata.json`;
|
|
103
|
+
|
|
104
|
+
let finalBillingInfo = {
|
|
105
|
+
...billingInfo,
|
|
106
|
+
app_pid: billingInfo?.app_pid || env.appPid,
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
// Check existing bill
|
|
110
|
+
const existingBill = await client.send(new GetObjectCommand({ key }));
|
|
111
|
+
if (existingBill.statusCode === 200 && existingBill.data) {
|
|
112
|
+
// Merge with existing data if force update
|
|
113
|
+
const existing = JSON.parse(await streamToString(existingBill.data)) as BillingInfo;
|
|
114
|
+
finalBillingInfo = mergeBillingInfo(existing, billingInfo);
|
|
115
|
+
logger.info('Updating existing billing:', { userDid, txHash: billingInfo.tx_hash });
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const payload = new PutObjectCommand({
|
|
119
|
+
key,
|
|
120
|
+
data: JSON.stringify(finalBillingInfo),
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
const result = await client.send(payload);
|
|
124
|
+
|
|
125
|
+
if (result.statusCode !== 200) {
|
|
126
|
+
logger.error('Upload billing info failed:', result);
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
logger.info(existingBill.statusCode === 200 ? 'Bill updated:' : 'Bill created:', {
|
|
131
|
+
userDid,
|
|
132
|
+
txHash: billingInfo.tx_hash,
|
|
133
|
+
});
|
|
134
|
+
return true;
|
|
135
|
+
} catch (error) {
|
|
136
|
+
logger.error('Upload billing info error:', error);
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
// Get specific billing information by transaction hash
|
|
142
|
+
export const getBillingInfo = async (
|
|
143
|
+
userDid: string,
|
|
144
|
+
txHash: string,
|
|
145
|
+
endpoint?: string
|
|
146
|
+
): Promise<BillingInfo | null> => {
|
|
147
|
+
try {
|
|
148
|
+
if (!txHash) {
|
|
149
|
+
logger.error('Transaction hash is required');
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const client = await getSpaceClient(userDid, endpoint);
|
|
154
|
+
const payload = new GetObjectCommand({
|
|
155
|
+
key: `txs/${txHash}/metadata.json`,
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
const result = await client.send(payload);
|
|
159
|
+
|
|
160
|
+
if (result.statusCode !== 200 || !result.data) {
|
|
161
|
+
logger.debug('Bill not found:', { userDid, txHash });
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const billingInfo = JSON.parse(await streamToString(result.data)) as BillingInfo;
|
|
166
|
+
|
|
167
|
+
// Validate parsed data
|
|
168
|
+
if (!billingInfo.tx_hash || !billingInfo.invoice_id) {
|
|
169
|
+
logger.error('Invalid billing data structure:', { userDid, txHash });
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return billingInfo;
|
|
174
|
+
} catch (error) {
|
|
175
|
+
logger.error('Get billing info error:', { userDid, txHash, error });
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
// Update specific fields of billing information
|
|
181
|
+
export const updateBillingInfo = async (
|
|
182
|
+
userDid: string,
|
|
183
|
+
txHash: string,
|
|
184
|
+
updates: Partial<BillingInfo>,
|
|
185
|
+
endpoint?: string
|
|
186
|
+
): Promise<boolean> => {
|
|
187
|
+
try {
|
|
188
|
+
const existing = await getBillingInfo(userDid, txHash, endpoint);
|
|
189
|
+
if (!existing) {
|
|
190
|
+
logger.error('Billing not found for update:', { userDid, txHash });
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const updated = mergeBillingInfo(existing, updates);
|
|
195
|
+
if (!updated.tx_hash || !updated.invoice_id || !updated.amount) {
|
|
196
|
+
logger.error('Invalid billing data structure:', { userDid, txHash });
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
const result = await uploadBillingInfo(userDid, updated, endpoint);
|
|
200
|
+
return result;
|
|
201
|
+
} catch (error) {
|
|
202
|
+
logger.error('Update billing info error:', { userDid, txHash, error });
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
// Batch upload/update billing information
|
|
208
|
+
export const uploadBillingInfoBatch = async (
|
|
209
|
+
userDid: string,
|
|
210
|
+
billingInfoList: BillingInfo[],
|
|
211
|
+
endpoint?: string
|
|
212
|
+
): Promise<boolean[]> => {
|
|
213
|
+
try {
|
|
214
|
+
if (!Array.isArray(billingInfoList) || billingInfoList.length === 0) {
|
|
215
|
+
logger.error('Invalid billingInfoList:', { userDid });
|
|
216
|
+
return [];
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Validate all bills before upload
|
|
220
|
+
const validBills = billingInfoList.filter((bill) => bill.tx_hash && bill.invoice_id && bill.amount);
|
|
221
|
+
|
|
222
|
+
if (validBills.length !== billingInfoList.length) {
|
|
223
|
+
logger.warn('Some bills are invalid and will be skipped');
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const results = await Promise.all(
|
|
227
|
+
validBills.map((billingInfo) => uploadBillingInfo(userDid, billingInfo, endpoint))
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
return results;
|
|
231
|
+
} catch (error) {
|
|
232
|
+
logger.error('Batch upload error:', { userDid, error });
|
|
233
|
+
return [];
|
|
234
|
+
}
|
|
235
|
+
};
|
package/api/src/libs/util.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import crypto from 'crypto';
|
|
2
|
-
|
|
2
|
+
import { Readable } from 'stream';
|
|
3
|
+
import { buffer } from 'node:stream/consumers';
|
|
3
4
|
import { getUrl } from '@blocklet/sdk/lib/component';
|
|
4
5
|
import env from '@blocklet/sdk/lib/env';
|
|
5
6
|
import { getWalletDid } from '@blocklet/sdk/lib/did';
|
|
@@ -93,6 +94,19 @@ export function createCodeGenerator(prefix: string, size: number = 24) {
|
|
|
93
94
|
return prefix ? () => `${prefix}_${generator()}` : generator;
|
|
94
95
|
}
|
|
95
96
|
|
|
97
|
+
export function stringToStream(str: string): Readable {
|
|
98
|
+
const stream = new Readable();
|
|
99
|
+
stream.push(str);
|
|
100
|
+
stream.push(null);
|
|
101
|
+
|
|
102
|
+
return stream;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export const streamToString = async (stream: any, encoding?: BufferEncoding): Promise<string> => {
|
|
106
|
+
const data = await buffer(stream);
|
|
107
|
+
return data.toString(encoding);
|
|
108
|
+
};
|
|
109
|
+
|
|
96
110
|
// FIXME: merge with old metadata
|
|
97
111
|
export function formatMetadata(metadata?: Record<string, any>): Record<string, any> {
|
|
98
112
|
if (!metadata) {
|
|
@@ -545,3 +559,21 @@ export function formatCurrencyInfo(
|
|
|
545
559
|
}
|
|
546
560
|
return paymentMethod && paymentMethod.type !== 'arcblock' ? `${amountStr} (${paymentMethod.name})` : amountStr;
|
|
547
561
|
}
|
|
562
|
+
|
|
563
|
+
export function getExplorerTxUrl({
|
|
564
|
+
explorerHost,
|
|
565
|
+
txHash,
|
|
566
|
+
type,
|
|
567
|
+
}: {
|
|
568
|
+
explorerHost: string;
|
|
569
|
+
txHash: string;
|
|
570
|
+
type: LiteralUnion<'ethereum' | 'base' | 'arcblock' | 'bitcoin', string>;
|
|
571
|
+
}) {
|
|
572
|
+
if (!explorerHost || !txHash) {
|
|
573
|
+
return '';
|
|
574
|
+
}
|
|
575
|
+
if (type === 'arcblock') {
|
|
576
|
+
return joinURL(explorerHost, '/txs', txHash);
|
|
577
|
+
}
|
|
578
|
+
return joinURL(explorerHost, '/tx', txHash);
|
|
579
|
+
}
|
|
@@ -175,10 +175,13 @@ export const handlePaymentSucceed = async (
|
|
|
175
175
|
);
|
|
176
176
|
}
|
|
177
177
|
|
|
178
|
-
depositVaultQueue.
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
178
|
+
const exist = await depositVaultQueue.get(`deposit-vault-${paymentIntent.currency_id}`);
|
|
179
|
+
if (!exist) {
|
|
180
|
+
depositVaultQueue.push({
|
|
181
|
+
id: `deposit-vault-${paymentIntent.currency_id}`,
|
|
182
|
+
job: { currencyId: paymentIntent.currency_id },
|
|
183
|
+
});
|
|
184
|
+
}
|
|
182
185
|
|
|
183
186
|
let invoice;
|
|
184
187
|
if (paymentIntent.invoice_id) {
|