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.
@@ -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
+ };
@@ -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.push({
179
- id: `deposit-vault-${paymentIntent.currency_id}`,
180
- job: { currencyId: paymentIntent.currency_id },
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) {