@fairmint/canton-fairmint-sdk 0.0.2

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 (54) hide show
  1. package/README.md +64 -0
  2. package/dist/clients/index.d.ts +5 -0
  3. package/dist/clients/index.d.ts.map +1 -0
  4. package/dist/clients/index.js +21 -0
  5. package/dist/clients/index.js.map +1 -0
  6. package/dist/clients/json-api/index.d.ts +2 -0
  7. package/dist/clients/json-api/index.d.ts.map +1 -0
  8. package/dist/clients/json-api/index.js +18 -0
  9. package/dist/clients/json-api/index.js.map +1 -0
  10. package/dist/clients/json-api/sdkHelper.d.ts +27 -0
  11. package/dist/clients/json-api/sdkHelper.d.ts.map +1 -0
  12. package/dist/clients/json-api/sdkHelper.js +73 -0
  13. package/dist/clients/json-api/sdkHelper.js.map +1 -0
  14. package/dist/clients/postgres-db-api/cantonDbClient.d.ts +335 -0
  15. package/dist/clients/postgres-db-api/cantonDbClient.d.ts.map +1 -0
  16. package/dist/clients/postgres-db-api/cantonDbClient.js +2703 -0
  17. package/dist/clients/postgres-db-api/cantonDbClient.js.map +1 -0
  18. package/dist/clients/postgres-db-api/fairmintDbClient.d.ts +241 -0
  19. package/dist/clients/postgres-db-api/fairmintDbClient.d.ts.map +1 -0
  20. package/dist/clients/postgres-db-api/fairmintDbClient.js +3078 -0
  21. package/dist/clients/postgres-db-api/fairmintDbClient.js.map +1 -0
  22. package/dist/clients/postgres-db-api/index.d.ts +5 -0
  23. package/dist/clients/postgres-db-api/index.d.ts.map +1 -0
  24. package/dist/clients/postgres-db-api/index.js +25 -0
  25. package/dist/clients/postgres-db-api/index.js.map +1 -0
  26. package/dist/clients/postgres-db-api/postgresDbClient.d.ts +118 -0
  27. package/dist/clients/postgres-db-api/postgresDbClient.d.ts.map +1 -0
  28. package/dist/clients/postgres-db-api/postgresDbClient.js +212 -0
  29. package/dist/clients/postgres-db-api/postgresDbClient.js.map +1 -0
  30. package/dist/clients/postgres-db-api/types.d.ts +330 -0
  31. package/dist/clients/postgres-db-api/types.d.ts.map +1 -0
  32. package/dist/clients/postgres-db-api/types.js +30 -0
  33. package/dist/clients/postgres-db-api/types.js.map +1 -0
  34. package/dist/clients/shared/config.d.ts +19 -0
  35. package/dist/clients/shared/config.d.ts.map +1 -0
  36. package/dist/clients/shared/config.js +208 -0
  37. package/dist/clients/shared/config.js.map +1 -0
  38. package/dist/clients/shared/index.d.ts +3 -0
  39. package/dist/clients/shared/index.d.ts.map +1 -0
  40. package/dist/clients/shared/index.js +19 -0
  41. package/dist/clients/shared/index.js.map +1 -0
  42. package/dist/clients/shared/types.d.ts +29 -0
  43. package/dist/clients/shared/types.d.ts.map +1 -0
  44. package/dist/clients/shared/types.js +3 -0
  45. package/dist/clients/shared/types.js.map +1 -0
  46. package/dist/clients/validator-api/index.d.ts +4 -0
  47. package/dist/clients/validator-api/index.d.ts.map +1 -0
  48. package/dist/clients/validator-api/index.js +61 -0
  49. package/dist/clients/validator-api/index.js.map +1 -0
  50. package/dist/index.d.ts +7 -0
  51. package/dist/index.d.ts.map +1 -0
  52. package/dist/index.js +24 -0
  53. package/dist/index.js.map +1 -0
  54. package/package.json +60 -0
@@ -0,0 +1,3078 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FairmintDbClient = void 0;
4
+ const pg_1 = require("pg");
5
+ const config_1 = require("../shared/config");
6
+ const types_1 = require("./types");
7
+ class FairmintDbClient {
8
+ constructor(network) {
9
+ // Initialize the shared config which handles environment loading
10
+ this.config = new config_1.ProviderConfig();
11
+ // Use provided network or get from environment variable, defaulting to devnet
12
+ this.network = network;
13
+ const databaseUrl = this.getDatabaseUrl();
14
+ if (databaseUrl) {
15
+ // Extract database name from URL for debugging (without exposing credentials)
16
+ const dbNameMatch = databaseUrl.match(/\/\/([^:]+:[^@]+@)?[^\/]+\/([^?]+)/);
17
+ const _dbName = dbNameMatch ? dbNameMatch[2] : 'unknown';
18
+ }
19
+ if (!databaseUrl) {
20
+ throw new Error(`Fairmint database URL for ${this.network} is not configured. Please set POSTGRES_DB_URL_${this.network.toUpperCase()} environment variable.`);
21
+ }
22
+ this.pool = new pg_1.Pool({
23
+ connectionString: databaseUrl,
24
+ ssl: { rejectUnauthorized: false },
25
+ });
26
+ // Test the connection
27
+ this.pool.on('error', err => {
28
+ console.error('Unexpected error on idle client', err);
29
+ process.exit(-1);
30
+ });
31
+ }
32
+ getDatabaseUrl() {
33
+ return this.config.getDatabaseUrl(this.network);
34
+ }
35
+ getNetwork() {
36
+ return this.network;
37
+ }
38
+ async connect() {
39
+ return this.pool.connect();
40
+ }
41
+ async close() {
42
+ await this.pool.end();
43
+ }
44
+ // Canton Transfers CRUD operations
45
+ async insertCantonTransfer(transfer) {
46
+ const query = `
47
+ INSERT INTO canton_transfers (
48
+ api_tracking_id, api_expires_at,
49
+ transfer_sender_party_id, transfer_receiver_party_id, transfer_amount,
50
+ transfer_description, transfer_burned_amount, receiver_holding_cids,
51
+ sender_change_cids, tx_update_id, tx_record_time,
52
+ status, app_reward_coupon_id
53
+ ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)
54
+ RETURNING *
55
+ `;
56
+ const values = [
57
+ transfer.api_tracking_id,
58
+ transfer.api_expires_at,
59
+ transfer.transfer_sender_party_id,
60
+ transfer.transfer_receiver_party_id,
61
+ transfer.transfer_amount,
62
+ transfer.transfer_description,
63
+ transfer.transfer_burned_amount,
64
+ transfer.receiver_holding_cids,
65
+ transfer.sender_change_cids,
66
+ transfer.tx_update_id,
67
+ transfer.tx_record_time,
68
+ transfer.status,
69
+ transfer.app_reward_coupon_id,
70
+ ];
71
+ const result = await this.pool.query(query, values);
72
+ return this.mapTransferFromDb(result.rows[0]);
73
+ }
74
+ async getCantonTransfer(id) {
75
+ const query = 'SELECT * FROM canton_transfers WHERE id = $1';
76
+ const result = await this.pool.query(query, [id]);
77
+ return result.rows.length > 0
78
+ ? this.mapTransferFromDb(result.rows[0])
79
+ : null;
80
+ }
81
+ async getCantonTransferByTrackingId(apiTrackingId) {
82
+ const query = 'SELECT * FROM canton_transfers WHERE api_tracking_id = $1';
83
+ const result = await this.pool.query(query, [apiTrackingId]);
84
+ return result.rows.length > 0
85
+ ? this.mapTransferFromDb(result.rows[0])
86
+ : null;
87
+ }
88
+ async getSubmittedTransfers() {
89
+ const query = `
90
+ SELECT * FROM canton_transfers
91
+ WHERE status = $1
92
+ ORDER BY created_at ASC
93
+ `;
94
+ const result = await this.pool.query(query, [types_1.TransferStatus.SUBMITTED]);
95
+ return result.rows.map(row => this.mapTransferFromDb(row));
96
+ }
97
+ async updateCantonTransfer(id, updates) {
98
+ const setClauses = [];
99
+ const values = [];
100
+ let paramIndex = 1;
101
+ // Build dynamic update query
102
+ Object.entries(updates).forEach(([key, value]) => {
103
+ if (key !== 'id' && key !== 'created_at') {
104
+ setClauses.push(`${this.toSnakeCase(key)} = $${paramIndex}`);
105
+ values.push(value);
106
+ paramIndex++;
107
+ }
108
+ });
109
+ if (setClauses.length === 0) {
110
+ throw new Error('No valid fields to update');
111
+ }
112
+ values.push(id);
113
+ const query = `
114
+ UPDATE canton_transfers
115
+ SET ${setClauses.join(', ')}, updated_at = NOW()
116
+ WHERE id = $${paramIndex}
117
+ RETURNING *
118
+ `;
119
+ const result = await this.pool.query(query, values);
120
+ return result.rows.length > 0
121
+ ? this.mapTransferFromDb(result.rows[0])
122
+ : null;
123
+ }
124
+ // Canton App Reward Coupons CRUD operations
125
+ async insertCantonAppRewardCoupon(coupon) {
126
+ const query = `
127
+ INSERT INTO canton_app_reward_coupons (
128
+ status, tx_update_id, tx_record_time, contract_id, template_id, package_name,
129
+ dso_party_id, provider_party_id, featured, round_number, beneficiary_party_id, coupon_amount
130
+ ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
131
+ RETURNING *
132
+ `;
133
+ const values = [
134
+ coupon.status,
135
+ coupon.tx_update_id,
136
+ coupon.tx_record_time,
137
+ coupon.contract_id,
138
+ coupon.template_id,
139
+ coupon.package_name,
140
+ coupon.dso_party_id,
141
+ coupon.provider_party_id,
142
+ coupon.featured,
143
+ coupon.round_number,
144
+ coupon.beneficiary_party_id,
145
+ coupon.coupon_amount,
146
+ ];
147
+ const result = await this.pool.query(query, values);
148
+ return this.mapRewardCouponFromDb(result.rows[0]);
149
+ }
150
+ async batchInsertCantonAppRewardCoupons(coupons) {
151
+ if (coupons.length === 0) {
152
+ return { insertedCount: 0, skippedCount: 0 };
153
+ }
154
+ // Build the VALUES clause with placeholders
155
+ const valuesPlaceholders = coupons
156
+ .map((_, idx) => `($${idx * 12 + 1}, $${idx * 12 + 2}, $${idx * 12 + 3}, $${idx * 12 + 4}, $${idx * 12 + 5}, $${idx * 12 + 6}, $${idx * 12 + 7}, $${idx * 12 + 8}, $${idx * 12 + 9}, $${idx * 12 + 10}, $${idx * 12 + 11}, $${idx * 12 + 12})`)
157
+ .join(', ');
158
+ const query = `
159
+ INSERT INTO canton_app_reward_coupons (
160
+ status, tx_update_id, tx_record_time, contract_id, template_id, package_name,
161
+ dso_party_id, provider_party_id, featured, round_number, beneficiary_party_id, coupon_amount
162
+ ) VALUES ${valuesPlaceholders}
163
+ ON CONFLICT (contract_id) DO NOTHING
164
+ RETURNING *
165
+ `;
166
+ // Flatten all coupon values into a single array
167
+ const values = coupons.flatMap(coupon => [
168
+ coupon.status,
169
+ coupon.tx_update_id,
170
+ coupon.tx_record_time,
171
+ coupon.contract_id,
172
+ coupon.template_id,
173
+ coupon.package_name,
174
+ coupon.dso_party_id,
175
+ coupon.provider_party_id,
176
+ coupon.featured,
177
+ coupon.round_number,
178
+ coupon.beneficiary_party_id,
179
+ coupon.coupon_amount,
180
+ ]);
181
+ const result = await this.pool.query(query, values);
182
+ const insertedCount = result.rowCount ?? 0;
183
+ const skippedCount = coupons.length - insertedCount;
184
+ return { insertedCount, skippedCount };
185
+ }
186
+ async upsertCantonAppRewardCouponWithStatus(coupon) {
187
+ const query = `
188
+ INSERT INTO canton_app_reward_coupons (
189
+ status, tx_update_id, tx_record_time, contract_id, template_id, package_name,
190
+ dso_party_id, provider_party_id, featured, round_number, beneficiary_party_id, coupon_amount
191
+ ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
192
+ ON CONFLICT (contract_id) DO UPDATE
193
+ SET status = EXCLUDED.status, updated_at = NOW()
194
+ RETURNING *
195
+ `;
196
+ const values = [
197
+ coupon.status,
198
+ coupon.tx_update_id,
199
+ coupon.tx_record_time,
200
+ coupon.contract_id,
201
+ coupon.template_id,
202
+ coupon.package_name,
203
+ coupon.dso_party_id,
204
+ coupon.provider_party_id,
205
+ coupon.featured,
206
+ coupon.round_number,
207
+ coupon.beneficiary_party_id,
208
+ coupon.coupon_amount,
209
+ ];
210
+ const result = await this.pool.query(query, values);
211
+ return this.mapRewardCouponFromDb(result.rows[0]);
212
+ }
213
+ async getCantonAppRewardCoupon(id) {
214
+ const query = 'SELECT * FROM canton_app_reward_coupons WHERE id = $1';
215
+ const result = await this.pool.query(query, [id]);
216
+ return result.rows.length > 0
217
+ ? this.mapRewardCouponFromDb(result.rows[0])
218
+ : null;
219
+ }
220
+ async getCantonAppRewardCouponByContractId(contractId) {
221
+ const query = 'SELECT * FROM canton_app_reward_coupons WHERE contract_id = $1';
222
+ const result = await this.pool.query(query, [contractId]);
223
+ return result.rows.length > 0
224
+ ? this.mapRewardCouponFromDb(result.rows[0])
225
+ : null;
226
+ }
227
+ async getCantonAppRewardCouponsByContractIds(contractIds) {
228
+ if (contractIds.length === 0) {
229
+ return [];
230
+ }
231
+ const placeholders = contractIds
232
+ .map((_, index) => `$${index + 1}`)
233
+ .join(', ');
234
+ const query = `
235
+ SELECT * FROM canton_app_reward_coupons
236
+ WHERE contract_id IN (${placeholders})
237
+ `;
238
+ const result = await this.pool.query(query, contractIds);
239
+ return result.rows.map(row => this.mapRewardCouponFromDb(row));
240
+ }
241
+ async getCantonAppRewardCouponsByStatus(status, limit = 100, sortOrder = 'ASC', beneficiaryPartyId) {
242
+ let query = `
243
+ SELECT * FROM canton_app_reward_coupons
244
+ WHERE status = $1
245
+ `;
246
+ const params = [status];
247
+ if (beneficiaryPartyId) {
248
+ query += ` AND beneficiary_party_id = $${params.length + 1}`;
249
+ params.push(beneficiaryPartyId);
250
+ }
251
+ query += ` ORDER BY tx_record_time ${sortOrder} LIMIT $${params.length + 1}`;
252
+ params.push(limit);
253
+ const result = await this.pool.query(query, params);
254
+ return result.rows.map(row => this.mapRewardCouponFromDb(row));
255
+ }
256
+ async countCantonAppRewardCouponsByStatus(status) {
257
+ const query = `
258
+ SELECT COUNT(*) FROM canton_app_reward_coupons
259
+ WHERE status = $1
260
+ `;
261
+ const result = await this.pool.query(query, [status]);
262
+ return result.rows[0].count;
263
+ }
264
+ async getOldestCreatedCantonAppRewardCoupons(limit = 100) {
265
+ const query = `
266
+ SELECT * FROM canton_app_reward_coupons
267
+ WHERE status = 'created'
268
+ ORDER BY tx_record_time ASC
269
+ LIMIT $1
270
+ `;
271
+ const result = await this.pool.query(query, [limit]);
272
+ return result.rows.map(row => this.mapRewardCouponFromDb(row));
273
+ }
274
+ async getCantonAppRewardCouponsByDateRange(startDate, endDate, status, limit = 1000, sortOrder = 'ASC') {
275
+ let query = `
276
+ SELECT * FROM canton_app_reward_coupons
277
+ WHERE tx_archive_record_time >= $1 AND tx_archive_record_time <= $2
278
+ `;
279
+ const values = [startDate, endDate];
280
+ let paramIndex = 3;
281
+ if (status) {
282
+ query += ` AND status = $${paramIndex}`;
283
+ values.push(status);
284
+ paramIndex++;
285
+ }
286
+ query += ` ORDER BY tx_archive_record_time ${sortOrder} LIMIT $${paramIndex}`;
287
+ values.push(limit);
288
+ const result = await this.pool.query(query, values);
289
+ return result.rows.map(row => this.mapRewardCouponFromDb(row));
290
+ }
291
+ async updateCantonAppRewardCoupon(id, updates) {
292
+ const setClauses = [];
293
+ const values = [];
294
+ let paramIndex = 1;
295
+ // Build dynamic update query
296
+ Object.entries(updates).forEach(([key, value]) => {
297
+ if (key !== 'id' && key !== 'created_at') {
298
+ setClauses.push(`${this.toSnakeCase(key)} = $${paramIndex}`);
299
+ values.push(value);
300
+ paramIndex++;
301
+ }
302
+ });
303
+ if (setClauses.length === 0) {
304
+ throw new Error('No valid fields to update');
305
+ }
306
+ values.push(id);
307
+ const query = `
308
+ UPDATE canton_app_reward_coupons
309
+ SET ${setClauses.join(', ')}, updated_at = NOW()
310
+ WHERE id = $${paramIndex}
311
+ RETURNING *
312
+ `;
313
+ const result = await this.pool.query(query, values);
314
+ return result.rows.length > 0
315
+ ? this.mapRewardCouponFromDb(result.rows[0])
316
+ : null;
317
+ }
318
+ async batchUpdateCantonAppRewardCouponsByContractIds(contractIds, updates) {
319
+ if (contractIds.length === 0) {
320
+ return [];
321
+ }
322
+ const setClauses = [];
323
+ const values = [];
324
+ let paramIndex = 1;
325
+ // Build dynamic update query
326
+ Object.entries(updates).forEach(([key, value]) => {
327
+ if (key !== 'id' && key !== 'created_at') {
328
+ setClauses.push(`${this.toSnakeCase(key)} = $${paramIndex}`);
329
+ values.push(value);
330
+ paramIndex++;
331
+ }
332
+ });
333
+ if (setClauses.length === 0) {
334
+ throw new Error('No valid fields to update');
335
+ }
336
+ // Build the WHERE clause for contract IDs
337
+ const contractIdPlaceholders = contractIds
338
+ .map((_, index) => `$${paramIndex + index}`)
339
+ .join(', ');
340
+ values.push(...contractIds);
341
+ const query = `
342
+ UPDATE canton_app_reward_coupons
343
+ SET ${setClauses.join(', ')}, updated_at = NOW()
344
+ WHERE contract_id IN (${contractIdPlaceholders})
345
+ RETURNING *
346
+ `;
347
+ const result = await this.pool.query(query, values);
348
+ return result.rows.map(row => this.mapRewardCouponFromDb(row));
349
+ }
350
+ async getRewardCoupons(options = {}) {
351
+ const { limit = 100, order = 'oldest', redeemed = 'Both' } = options;
352
+ let query = `
353
+ SELECT
354
+ c.id as coupon_id,
355
+ c.status as coupon_status,
356
+ c.tx_update_id as coupon_tx_update_id,
357
+ c.tx_record_time as coupon_tx_record_time,
358
+ c.contract_id as coupon_contract_id,
359
+ c.template_id as coupon_template_id,
360
+ c.package_name as coupon_package_name,
361
+ c.dso_party_id as coupon_dso_party_id,
362
+ c.provider_party_id as coupon_provider_party_id,
363
+ c.featured as coupon_featured,
364
+ c.round_number as coupon_round_number,
365
+ c.beneficiary_party_id as coupon_beneficiary_party_id,
366
+ c.coupon_amount as coupon_coupon_amount,
367
+ c.tx_archive_update_id as coupon_tx_archive_update_id,
368
+ c.tx_archive_record_time as coupon_tx_archive_record_time,
369
+ c.app_reward_amount as coupon_app_reward_amount,
370
+ c.created_at as coupon_created_at,
371
+ c.updated_at as coupon_updated_at,
372
+ t.id as transfer_id,
373
+ t.status as transfer_status,
374
+ t.api_tracking_id as transfer_api_tracking_id,
375
+ t.api_expires_at as transfer_api_expires_at,
376
+ t.transfer_sender_party_id as transfer_transfer_sender_party_id,
377
+ t.transfer_receiver_party_id as transfer_transfer_receiver_party_id,
378
+ t.transfer_amount as transfer_transfer_amount,
379
+ t.transfer_description as transfer_transfer_description,
380
+ t.transfer_burned_amount as transfer_transfer_burned_amount,
381
+ t.receiver_holding_cids as transfer_receiver_holding_cids,
382
+ t.sender_change_cids as transfer_sender_change_cids,
383
+ t.tx_update_id as transfer_tx_update_id,
384
+ t.tx_record_time as transfer_tx_record_time,
385
+ t.app_reward_coupon_id as transfer_app_reward_coupon_id,
386
+ t.created_at as transfer_created_at,
387
+ t.updated_at as transfer_updated_at,
388
+ r.id as redemption_id,
389
+ r.tx_update_id as redemption_tx_update_id,
390
+ r.tx_record_time as redemption_tx_record_time,
391
+ r.tx_synchronizer_id as redemption_tx_synchronizer_id,
392
+ r.tx_effective_at as redemption_tx_effective_at,
393
+ r.total_app_rewards,
394
+ r.created_at as redemption_created_at,
395
+ r.updated_at as redemption_updated_at
396
+ FROM canton_app_reward_coupons c
397
+ LEFT JOIN canton_transfers t ON t.app_reward_coupon_id = c.id
398
+ `;
399
+ const conditions = [];
400
+ const values = [];
401
+ const paramIndex = 1;
402
+ // Filter by redemption status
403
+ if (redeemed === 'Unredeemed') {
404
+ conditions.push('r.id IS NULL');
405
+ }
406
+ else if (redeemed === 'Redeemed') {
407
+ conditions.push('r.id IS NOT NULL');
408
+ }
409
+ if (conditions.length > 0) {
410
+ query += ` WHERE ${conditions.join(' AND ')}`;
411
+ }
412
+ // Order by
413
+ query += ` ORDER BY c.tx_record_time ${order === 'oldest' ? 'ASC' : 'DESC'}`;
414
+ // Limit
415
+ query += ` LIMIT $${paramIndex}`;
416
+ values.push(limit);
417
+ const result = await this.pool.query(query, values);
418
+ return result.rows.map(row => this.mapRewardCouponWithTransferFromDb(row));
419
+ }
420
+ async getRewardCoupon(options) {
421
+ const { contract_id } = options;
422
+ const query = `
423
+ SELECT
424
+ c.id as coupon_id,
425
+ c.status as coupon_status,
426
+ c.tx_update_id as coupon_tx_update_id,
427
+ c.tx_record_time as coupon_tx_record_time,
428
+ c.contract_id as coupon_contract_id,
429
+ c.template_id as coupon_template_id,
430
+ c.package_name as coupon_package_name,
431
+ c.dso_party_id as coupon_dso_party_id,
432
+ c.provider_party_id as coupon_provider_party_id,
433
+ c.featured as coupon_featured,
434
+ c.round_number as coupon_round_number,
435
+ c.beneficiary_party_id as coupon_beneficiary_party_id,
436
+ c.coupon_amount as coupon_coupon_amount,
437
+ c.tx_archive_update_id as coupon_tx_archive_update_id,
438
+ c.tx_archive_record_time as coupon_tx_archive_record_time,
439
+ c.app_reward_amount as coupon_app_reward_amount,
440
+ c.created_at as coupon_created_at,
441
+ c.updated_at as coupon_updated_at,
442
+ t.id as transfer_id,
443
+ t.status as transfer_status,
444
+ t.api_tracking_id as transfer_api_tracking_id,
445
+ t.api_expires_at as transfer_api_expires_at,
446
+ t.transfer_sender_party_id as transfer_transfer_sender_party_id,
447
+ t.transfer_receiver_party_id as transfer_transfer_receiver_party_id,
448
+ t.transfer_amount as transfer_transfer_amount,
449
+ t.transfer_description as transfer_transfer_description,
450
+ t.transfer_burned_amount as transfer_transfer_burned_amount,
451
+ t.receiver_holding_cids as transfer_receiver_holding_cids,
452
+ t.sender_change_cids as transfer_sender_change_cids,
453
+ t.tx_update_id as transfer_tx_update_id,
454
+ t.tx_record_time as transfer_tx_record_time,
455
+ t.app_reward_coupon_id as transfer_app_reward_coupon_id,
456
+ t.created_at as transfer_created_at,
457
+ t.updated_at as transfer_updated_at,
458
+ r.id as redemption_id,
459
+ r.tx_update_id as redemption_tx_update_id,
460
+ r.tx_record_time as redemption_tx_record_time,
461
+ r.tx_synchronizer_id as redemption_tx_synchronizer_id,
462
+ r.tx_effective_at as redemption_tx_effective_at,
463
+ r.total_app_rewards,
464
+ r.created_at as redemption_created_at,
465
+ r.updated_at as redemption_updated_at
466
+ FROM canton_app_reward_coupons c
467
+ LEFT JOIN canton_transfers t ON t.app_reward_coupon_id = c.id
468
+ WHERE c.contract_id = $1
469
+ `;
470
+ const result = await this.pool.query(query, [contract_id]);
471
+ return result.rows.length > 0
472
+ ? this.mapRewardCouponWithTransferFromDb(result.rows[0])
473
+ : null;
474
+ }
475
+ async getMonthlyTransferTotal(monthStart, monthEnd) {
476
+ const query = `
477
+ SELECT COALESCE(SUM(transfer_amount), 0) as total_amount
478
+ FROM canton_transfers
479
+ WHERE tx_record_time >= $1
480
+ AND tx_record_time <= $2
481
+ AND status IN ($3, $4)
482
+ `;
483
+ const result = await this.pool.query(query, [
484
+ monthStart,
485
+ monthEnd,
486
+ types_1.TransferStatus.CONFIRMED,
487
+ types_1.TransferStatus.PENDING,
488
+ ]);
489
+ const totalAmount = parseFloat(result.rows[0].total_amount);
490
+ return totalAmount;
491
+ }
492
+ async getMonthlyAppRewardAmountTotal(monthStart, monthEnd) {
493
+ const query = `
494
+ SELECT COALESCE(SUM(app_reward_amount), 0) as total_amount
495
+ FROM canton_app_reward_coupons
496
+ WHERE created_at >= $1
497
+ AND created_at <= $2
498
+ AND app_reward_amount IS NOT NULL
499
+ `;
500
+ const result = await this.pool.query(query, [monthStart, monthEnd]);
501
+ const totalAmount = parseFloat(result.rows[0].total_amount);
502
+ return totalAmount;
503
+ }
504
+ async getTransferTimeSeriesData(timeRange, _metric = 'count', timePeriod, customStartDate) {
505
+ let interval;
506
+ let timeFilter;
507
+ let timeSeriesStart;
508
+ let timeSeriesEnd = 'NOW()';
509
+ let timeRangeInterval;
510
+ switch (timeRange) {
511
+ case '15m':
512
+ interval = 'minute';
513
+ timeFilter = "created_at >= NOW() - INTERVAL '15 minutes'";
514
+ timeRangeInterval = '15 minutes';
515
+ timeSeriesStart = "NOW() - INTERVAL '15 minutes'";
516
+ break;
517
+ case '1h':
518
+ interval = 'minute';
519
+ timeFilter = "created_at >= NOW() - INTERVAL '1 hour'";
520
+ timeRangeInterval = '1 hour';
521
+ timeSeriesStart = "NOW() - INTERVAL '1 hour'";
522
+ break;
523
+ case '6h':
524
+ interval = 'minute';
525
+ timeFilter = "created_at >= NOW() - INTERVAL '6 hours'";
526
+ timeRangeInterval = '6 hours';
527
+ timeSeriesStart = "NOW() - INTERVAL '6 hours'";
528
+ break;
529
+ case '1d':
530
+ interval = 'hour';
531
+ timeFilter = "created_at >= NOW() - INTERVAL '1 day'";
532
+ timeRangeInterval = '1 day';
533
+ timeSeriesStart = "NOW() - INTERVAL '1 day'";
534
+ break;
535
+ case '7d':
536
+ interval = 'hour';
537
+ timeFilter = "created_at >= NOW() - INTERVAL '7 days'";
538
+ timeRangeInterval = '7 days';
539
+ timeSeriesStart = "NOW() - INTERVAL '7 days'";
540
+ break;
541
+ case '30d':
542
+ interval = 'day';
543
+ timeFilter = "created_at >= NOW() - INTERVAL '30 days'";
544
+ timeRangeInterval = '30 days';
545
+ timeSeriesStart = "NOW() - INTERVAL '30 days'";
546
+ break;
547
+ case 'last-month':
548
+ interval = 'day';
549
+ timeFilter =
550
+ "created_at >= date_trunc('month', current_date - interval '1 month') AND created_at < date_trunc('month', current_date)";
551
+ timeRangeInterval = '1 month';
552
+ timeSeriesStart =
553
+ "date_trunc('month', current_date - interval '1 month')";
554
+ timeSeriesEnd = "date_trunc('month', current_date)";
555
+ break;
556
+ case 'all':
557
+ interval = 'day';
558
+ timeFilter = '1=1';
559
+ timeRangeInterval = '100 years';
560
+ timeSeriesStart = "'2023-01-01'::timestamp";
561
+ break;
562
+ default:
563
+ interval = 'hour';
564
+ timeFilter = "created_at >= NOW() - INTERVAL '1 day'";
565
+ timeRangeInterval = '1 day';
566
+ timeSeriesStart = "NOW() - INTERVAL '1 day'";
567
+ }
568
+ // If custom start date is provided, override the timeFilter and time series start/end
569
+ if (timePeriod === 'custom-start' && customStartDate) {
570
+ timeFilter = `created_at >= '${customStartDate}:00' AND created_at < ('${customStartDate}:00'::timestamp + INTERVAL '${timeRangeInterval}')`;
571
+ timeSeriesStart = `'${customStartDate}:00'::timestamp`;
572
+ timeSeriesEnd = `('${customStartDate}:00'::timestamp + INTERVAL '${timeRangeInterval}')`;
573
+ }
574
+ const query = `
575
+ WITH time_series AS (
576
+ SELECT generate_series(
577
+ date_trunc('${interval}', ${timeSeriesStart}),
578
+ date_trunc('${interval}', ${timeSeriesEnd}),
579
+ INTERVAL '1 ${interval}'
580
+ ) AS timestamp
581
+ ),
582
+ transfer_data AS (
583
+ SELECT
584
+ date_trunc('${interval}', created_at) AS timestamp,
585
+ COUNT(*) as count,
586
+ COALESCE(SUM(transfer_amount), 0) as amount
587
+ FROM canton_transfers
588
+ WHERE ${timeFilter}
589
+ AND status IN ('pending', 'submitted', 'confirmed')
590
+ GROUP BY date_trunc('${interval}', created_at)
591
+ )
592
+ SELECT
593
+ ts.timestamp::text,
594
+ COALESCE(td.count, 0) as count,
595
+ COALESCE(td.amount, 0) as amount
596
+ FROM time_series ts
597
+ LEFT JOIN transfer_data td ON ts.timestamp = td.timestamp
598
+ ORDER BY ts.timestamp ASC
599
+ `;
600
+ const result = await this.pool.query(query);
601
+ return result.rows.map(row => ({
602
+ timestamp: row.timestamp,
603
+ count: parseInt(row.count, 10),
604
+ amount: parseFloat(row.amount),
605
+ }));
606
+ }
607
+ async getRewardCouponTimeSeriesData(timeRange, _metric = 'count', timePeriod, customStartDate, partyFilter, couponStatus = 'good') {
608
+ let interval;
609
+ let timeFilter;
610
+ let timeSeriesStart;
611
+ let timeSeriesEnd = 'NOW()';
612
+ let timeRangeInterval;
613
+ switch (timeRange) {
614
+ case '15m':
615
+ interval = 'minute';
616
+ timeFilter = "tx_record_time >= NOW() - INTERVAL '15 minutes'";
617
+ timeRangeInterval = '15 minutes';
618
+ timeSeriesStart = "NOW() - INTERVAL '15 minutes'";
619
+ break;
620
+ case '1h':
621
+ interval = 'minute';
622
+ timeFilter = "tx_record_time >= NOW() - INTERVAL '1 hour'";
623
+ timeRangeInterval = '1 hour';
624
+ timeSeriesStart = "NOW() - INTERVAL '1 hour'";
625
+ break;
626
+ case '6h':
627
+ interval = 'minute';
628
+ timeFilter = "tx_record_time >= NOW() - INTERVAL '6 hours'";
629
+ timeRangeInterval = '6 hours';
630
+ timeSeriesStart = "NOW() - INTERVAL '6 hours'";
631
+ break;
632
+ case '1d':
633
+ interval = 'hour';
634
+ timeFilter = "tx_record_time >= NOW() - INTERVAL '1 day'";
635
+ timeRangeInterval = '1 day';
636
+ timeSeriesStart = "NOW() - INTERVAL '1 day'";
637
+ break;
638
+ case '7d':
639
+ interval = 'hour';
640
+ timeFilter = "tx_record_time >= NOW() - INTERVAL '7 days'";
641
+ timeRangeInterval = '7 days';
642
+ timeSeriesStart = "NOW() - INTERVAL '7 days'";
643
+ break;
644
+ case '30d':
645
+ interval = 'day';
646
+ timeFilter = "tx_record_time >= NOW() - INTERVAL '30 days'";
647
+ timeRangeInterval = '30 days';
648
+ timeSeriesStart = "NOW() - INTERVAL '30 days'";
649
+ break;
650
+ case 'last-month':
651
+ interval = 'day';
652
+ timeFilter =
653
+ "tx_record_time >= date_trunc('month', current_date - interval '1 month') AND tx_record_time < date_trunc('month', current_date)";
654
+ timeRangeInterval = '1 month';
655
+ timeSeriesStart =
656
+ "date_trunc('month', current_date - interval '1 month')";
657
+ timeSeriesEnd = "date_trunc('month', current_date)";
658
+ break;
659
+ case 'all':
660
+ interval = 'day'; // Default to daily for 'all' to avoid too many points
661
+ timeFilter = '1=1';
662
+ timeRangeInterval = '100 years'; // Just for custom start calc
663
+ timeSeriesStart = "'2023-01-01'::timestamp"; // Reasonable start date
664
+ break;
665
+ default:
666
+ interval = 'hour';
667
+ timeFilter = "tx_record_time >= NOW() - INTERVAL '1 day'";
668
+ timeRangeInterval = '1 day';
669
+ timeSeriesStart = "NOW() - INTERVAL '1 day'";
670
+ }
671
+ // If custom start date is provided, override the timeFilter and time series start/end
672
+ if (timePeriod === 'custom-start' && customStartDate) {
673
+ timeFilter = `tx_record_time >= '${customStartDate}:00' AND tx_record_time < ('${customStartDate}:00'::timestamp + INTERVAL '${timeRangeInterval}')`;
674
+ timeSeriesStart = `'${customStartDate}:00'::timestamp`;
675
+ timeSeriesEnd = `('${customStartDate}:00'::timestamp + INTERVAL '${timeRangeInterval}')`;
676
+ }
677
+ // Add party filter if provided
678
+ const partyCondition = partyFilter
679
+ ? ` AND beneficiary_party_id = '${partyFilter}'`
680
+ : '';
681
+ // Add status filter based on couponStatus
682
+ const statusCondition = couponStatus === 'good'
683
+ ? ` AND status IN ('created', 'archived')`
684
+ : ` AND status = 'expired'`;
685
+ const query = `
686
+ WITH time_series AS (
687
+ SELECT generate_series(
688
+ date_trunc('${interval}', ${timeSeriesStart}),
689
+ date_trunc('${interval}', ${timeSeriesEnd}),
690
+ INTERVAL '1 ${interval}'
691
+ ) AS timestamp
692
+ ),
693
+ coupon_data AS (
694
+ SELECT
695
+ date_trunc('${interval}', tx_record_time) AS timestamp,
696
+ COUNT(*) as count,
697
+ COALESCE(SUM(app_reward_amount), 0) as amount,
698
+ COALESCE(SUM(coupon_amount), 0) as coupon_amount
699
+ FROM canton_app_reward_coupons
700
+ WHERE ${timeFilter}${partyCondition}${statusCondition}
701
+ GROUP BY date_trunc('${interval}', tx_record_time)
702
+ )
703
+ SELECT
704
+ ts.timestamp::text,
705
+ COALESCE(cd.count, 0) as count,
706
+ COALESCE(cd.amount, 0) as amount,
707
+ COALESCE(cd.coupon_amount, 0) as coupon_amount
708
+ FROM time_series ts
709
+ LEFT JOIN coupon_data cd ON ts.timestamp = cd.timestamp
710
+ ORDER BY ts.timestamp ASC
711
+ `;
712
+ const result = await this.pool.query(query);
713
+ return result.rows.map(row => ({
714
+ timestamp: row.timestamp,
715
+ count: parseInt(row.count, 10),
716
+ amount: parseFloat(row.amount),
717
+ couponAmount: parseFloat(row.coupon_amount),
718
+ }));
719
+ }
720
+ async getRewardCouponRoundSeriesData(timeRange, _metric = 'count', timePeriod, customStartDate, partyFilter, couponStatus = 'good') {
721
+ let timeRangeInterval;
722
+ let timeFilter;
723
+ switch (timeRange) {
724
+ case '15m':
725
+ timeRangeInterval = '15 minutes';
726
+ timeFilter = "tx_record_time >= NOW() - INTERVAL '15 minutes'";
727
+ break;
728
+ case '1h':
729
+ timeRangeInterval = '1 hour';
730
+ timeFilter = "tx_record_time >= NOW() - INTERVAL '1 hour'";
731
+ break;
732
+ case '6h':
733
+ timeRangeInterval = '6 hours';
734
+ timeFilter = "tx_record_time >= NOW() - INTERVAL '6 hours'";
735
+ break;
736
+ case '1d':
737
+ timeRangeInterval = '1 day';
738
+ timeFilter = "tx_record_time >= NOW() - INTERVAL '1 day'";
739
+ break;
740
+ case '7d':
741
+ timeRangeInterval = '7 days';
742
+ timeFilter = "tx_record_time >= NOW() - INTERVAL '7 days'";
743
+ break;
744
+ case '30d':
745
+ timeRangeInterval = '30 days';
746
+ timeFilter = "tx_record_time >= NOW() - INTERVAL '30 days'";
747
+ break;
748
+ case 'last-month':
749
+ timeRangeInterval = '1 month';
750
+ timeFilter =
751
+ "tx_record_time >= date_trunc('month', current_date - interval '1 month') AND tx_record_time < date_trunc('month', current_date)";
752
+ break;
753
+ case 'all':
754
+ default:
755
+ timeRangeInterval = '1 day';
756
+ timeFilter = "tx_record_time >= NOW() - INTERVAL '1 day'";
757
+ }
758
+ if (timePeriod === 'custom-start' && customStartDate) {
759
+ timeFilter = `tx_record_time >= '${customStartDate}:00' AND tx_record_time < ('${customStartDate}:00'::timestamp + INTERVAL '${timeRangeInterval}')`;
760
+ }
761
+ // Add party filter if provided
762
+ const partyCondition = partyFilter
763
+ ? ` AND beneficiary_party_id = '${partyFilter}'`
764
+ : '';
765
+ // Add status filter based on couponStatus
766
+ const statusCondition = couponStatus === 'good'
767
+ ? ` AND status IN ('created', 'archived')`
768
+ : ` AND status = 'expired'`;
769
+ const query = `
770
+ WITH time_filtered AS (
771
+ SELECT round_number
772
+ FROM canton_app_reward_coupons
773
+ WHERE ${timeFilter}${partyCondition}${statusCondition}
774
+ ),
775
+ round_bounds AS (
776
+ SELECT
777
+ COALESCE(MIN(round_number), 0) AS min_round,
778
+ COALESCE(MAX(round_number), 0) AS max_round
779
+ FROM time_filtered
780
+ ),
781
+ rounds AS (
782
+ SELECT generate_series(
783
+ GREATEST((SELECT min_round FROM round_bounds), 0),
784
+ GREATEST((SELECT max_round FROM round_bounds), 0)
785
+ ) AS round
786
+ WHERE (SELECT max_round FROM round_bounds) > 0
787
+ ),
788
+ coupon_data AS (
789
+ SELECT
790
+ round_number AS round,
791
+ MIN(tx_record_time) AS timestamp,
792
+ COUNT(*) AS count,
793
+ COALESCE(SUM(app_reward_amount), 0) AS amount,
794
+ COALESCE(SUM(coupon_amount), 0) AS coupon_amount
795
+ FROM canton_app_reward_coupons
796
+ WHERE round_number IN (SELECT round FROM rounds)${partyCondition}${statusCondition}
797
+ GROUP BY round_number
798
+ )
799
+ SELECT
800
+ r.round,
801
+ cd.timestamp,
802
+ COALESCE(cd.count, 0) AS count,
803
+ COALESCE(cd.amount, 0) AS amount,
804
+ COALESCE(cd.coupon_amount, 0) AS coupon_amount
805
+ FROM rounds r
806
+ LEFT JOIN coupon_data cd ON cd.round = r.round
807
+ ORDER BY r.round ASC
808
+ `;
809
+ const result = await this.pool.query(query);
810
+ return result.rows.map(row => ({
811
+ timestamp: row.timestamp
812
+ ? new Date(row.timestamp).toISOString()
813
+ : new Date().toISOString(),
814
+ count: parseInt(row.count, 10),
815
+ amount: parseFloat(row.amount),
816
+ couponAmount: parseFloat(row.coupon_amount),
817
+ round: typeof row.round === 'number' ? row.round : parseInt(row.round, 10),
818
+ }));
819
+ }
820
+ async getLatestCouponAmounts() {
821
+ const query = `
822
+ WITH latest AS (
823
+ SELECT DISTINCT ON (featured)
824
+ featured,
825
+ coupon_amount
826
+ FROM canton_app_reward_coupons
827
+ WHERE coupon_amount IS NOT NULL
828
+ ORDER BY featured, tx_record_time DESC
829
+ )
830
+ SELECT
831
+ MAX(coupon_amount) FILTER (WHERE featured) AS featured_amount,
832
+ MAX(coupon_amount) FILTER (WHERE NOT featured) AS unfeatured_amount
833
+ FROM latest
834
+ `;
835
+ const result = await this.pool.query(query);
836
+ const row = result.rows[0] ?? {};
837
+ return {
838
+ featured: row.featured_amount !== null && row.featured_amount !== undefined
839
+ ? parseFloat(row.featured_amount)
840
+ : null,
841
+ unfeatured: row.unfeatured_amount !== null && row.unfeatured_amount !== undefined
842
+ ? parseFloat(row.unfeatured_amount)
843
+ : null,
844
+ };
845
+ }
846
+ async getLastAppRewardCouponTimestamp(partyFilter) {
847
+ const partyCondition = partyFilter
848
+ ? ` AND beneficiary_party_id = '${partyFilter}'`
849
+ : '';
850
+ const query = `
851
+ SELECT MAX(tx_record_time) AS last_timestamp
852
+ FROM canton_app_reward_coupons
853
+ WHERE status IN ('created', 'archived')
854
+ AND app_reward_amount IS NOT NULL${partyCondition}
855
+ `;
856
+ const result = await this.pool.query(query);
857
+ const timestamp = result.rows[0]?.last_timestamp;
858
+ return timestamp ? new Date(timestamp).toISOString() : null;
859
+ }
860
+ async getLifetimeAppRewards() {
861
+ const query = `
862
+ SELECT COALESCE(SUM(app_reward_amount), 0) AS total
863
+ FROM canton_app_reward_coupons
864
+ WHERE status = 'archived'
865
+ `;
866
+ const result = await this.pool.query(query);
867
+ return Number(result.rows[0]?.total ?? 0);
868
+ }
869
+ async getUniqueCouponBeneficiaryParties() {
870
+ const query = `
871
+ SELECT DISTINCT
872
+ beneficiary_party_id as party_id,
873
+ beneficiary_party_id as display_name
874
+ FROM canton_app_reward_coupons
875
+ WHERE beneficiary_party_id IS NOT NULL
876
+ ORDER BY beneficiary_party_id ASC
877
+ `;
878
+ const result = await this.pool.query(query);
879
+ return result.rows;
880
+ }
881
+ async getAppMarkerTimeSeriesData(timeRange, _metric = 'count', timePeriod, customStartDate, partyFilter, _statusFilter) {
882
+ let interval;
883
+ let timeFilter;
884
+ let timeSeriesStart;
885
+ let timeSeriesEnd = 'NOW()';
886
+ let timeRangeInterval;
887
+ switch (timeRange) {
888
+ case '15m':
889
+ interval = 'minute';
890
+ timeFilter = "tx_record_time >= NOW() - INTERVAL '15 minutes'";
891
+ timeRangeInterval = '15 minutes';
892
+ timeSeriesStart = "NOW() - INTERVAL '15 minutes'";
893
+ break;
894
+ case '1h':
895
+ interval = 'minute';
896
+ timeFilter = "tx_record_time >= NOW() - INTERVAL '1 hour'";
897
+ timeRangeInterval = '1 hour';
898
+ timeSeriesStart = "NOW() - INTERVAL '1 hour'";
899
+ break;
900
+ case '6h':
901
+ interval = 'minute';
902
+ timeFilter = "tx_record_time >= NOW() - INTERVAL '6 hours'";
903
+ timeRangeInterval = '6 hours';
904
+ timeSeriesStart = "NOW() - INTERVAL '6 hours'";
905
+ break;
906
+ case '1d':
907
+ interval = 'hour';
908
+ timeFilter = "tx_record_time >= NOW() - INTERVAL '1 day'";
909
+ timeRangeInterval = '1 day';
910
+ timeSeriesStart = "NOW() - INTERVAL '1 day'";
911
+ break;
912
+ case '7d':
913
+ interval = 'hour';
914
+ timeFilter = "tx_record_time >= NOW() - INTERVAL '7 days'";
915
+ timeRangeInterval = '7 days';
916
+ timeSeriesStart = "NOW() - INTERVAL '7 days'";
917
+ break;
918
+ case '30d':
919
+ interval = 'day';
920
+ timeFilter = "tx_record_time >= NOW() - INTERVAL '30 days'";
921
+ timeRangeInterval = '30 days';
922
+ timeSeriesStart = "NOW() - INTERVAL '30 days'";
923
+ break;
924
+ case 'last-month':
925
+ interval = 'day';
926
+ timeFilter =
927
+ "tx_record_time >= date_trunc('month', current_date - interval '1 month') AND tx_record_time < date_trunc('month', current_date)";
928
+ timeRangeInterval = '1 month';
929
+ timeSeriesStart =
930
+ "date_trunc('month', current_date - interval '1 month')";
931
+ timeSeriesEnd = "date_trunc('month', current_date)";
932
+ break;
933
+ case 'all':
934
+ interval = 'day';
935
+ timeFilter = '1=1';
936
+ timeRangeInterval = '100 years';
937
+ timeSeriesStart = "'2023-01-01'::timestamp";
938
+ break;
939
+ default:
940
+ interval = 'hour';
941
+ timeFilter = "tx_record_time >= NOW() - INTERVAL '1 day'";
942
+ timeRangeInterval = '1 day';
943
+ timeSeriesStart = "NOW() - INTERVAL '1 day'";
944
+ }
945
+ // If custom start date is provided, override the timeFilter and time series start/end
946
+ if (timePeriod === 'custom-start' && customStartDate) {
947
+ timeFilter = `tx_record_time >= '${customStartDate}:00' AND tx_record_time < ('${customStartDate}:00'::timestamp + INTERVAL '${timeRangeInterval}')`;
948
+ timeSeriesStart = `'${customStartDate}:00'::timestamp`;
949
+ timeSeriesEnd = `('${customStartDate}:00'::timestamp + INTERVAL '${timeRangeInterval}')`;
950
+ }
951
+ // Add party filter if provided
952
+ const partyCondition = partyFilter
953
+ ? ` AND beneficiary_party_id = '${partyFilter}'`
954
+ : '';
955
+ const query = `
956
+ WITH time_series AS (
957
+ SELECT generate_series(
958
+ date_trunc('${interval}', ${timeSeriesStart}),
959
+ date_trunc('${interval}', ${timeSeriesEnd}),
960
+ INTERVAL '1 ${interval}'
961
+ ) AS timestamp
962
+ ),
963
+ marker_data AS (
964
+ SELECT
965
+ date_trunc('${interval}', tx_record_time) AS timestamp,
966
+ COUNT(*) FILTER (WHERE status = 'archived') as archived_count,
967
+ COUNT(*) FILTER (WHERE status = 'created') as created_count,
968
+ COUNT(*) FILTER (WHERE status = 'archived') as archived_weight,
969
+ COUNT(*) FILTER (WHERE status = 'created') as created_weight
970
+ FROM canton_app_markers
971
+ WHERE ${timeFilter}${partyCondition}
972
+ GROUP BY date_trunc('${interval}', tx_record_time)
973
+ )
974
+ SELECT
975
+ ts.timestamp::text,
976
+ COALESCE(md.archived_count, 0) as archived_count,
977
+ COALESCE(md.created_count, 0) as created_count,
978
+ COALESCE(md.archived_count, 0) + COALESCE(md.created_count, 0) as count,
979
+ COALESCE(md.archived_weight, 0) as archived_weight,
980
+ COALESCE(md.created_weight, 0) as created_weight,
981
+ 0 as amount,
982
+ 0 as coupon_amount
983
+ FROM time_series ts
984
+ LEFT JOIN marker_data md ON ts.timestamp = md.timestamp
985
+ ORDER BY ts.timestamp ASC
986
+ `;
987
+ const result = await this.pool.query(query);
988
+ return result.rows.map(row => ({
989
+ timestamp: row.timestamp,
990
+ count: parseInt(row.count, 10),
991
+ archivedCount: parseInt(row.archived_count, 10),
992
+ createdCount: parseInt(row.created_count, 10),
993
+ archivedWeight: parseFloat(row.archived_weight),
994
+ createdWeight: parseFloat(row.created_weight),
995
+ amount: 0,
996
+ couponAmount: 0,
997
+ }));
998
+ }
999
+ async getAppMarkerRoundSeriesData(timeRange, _metric = 'count', timePeriod, customStartDate, partyFilter, _statusFilter) {
1000
+ let timeRangeInterval;
1001
+ switch (timeRange) {
1002
+ case '15m':
1003
+ timeRangeInterval = '15 minutes';
1004
+ break;
1005
+ case '1h':
1006
+ timeRangeInterval = '1 hour';
1007
+ break;
1008
+ case '6h':
1009
+ timeRangeInterval = '6 hours';
1010
+ break;
1011
+ case '1d':
1012
+ timeRangeInterval = '1 day';
1013
+ break;
1014
+ case '7d':
1015
+ timeRangeInterval = '7 days';
1016
+ break;
1017
+ case '30d':
1018
+ timeRangeInterval = '30 days';
1019
+ break;
1020
+ default:
1021
+ timeRangeInterval = '1 day';
1022
+ }
1023
+ if (timePeriod === 'custom-start' && customStartDate) {
1024
+ const _timeFilter = `tx_record_time >= '${customStartDate}:00' AND tx_record_time < ('${customStartDate}:00'::timestamp + INTERVAL '${timeRangeInterval}')`;
1025
+ void _timeFilter;
1026
+ }
1027
+ // Add party filter if provided
1028
+ const _partyCondition = partyFilter
1029
+ ? ` AND beneficiary_party_id = '${partyFilter}'`
1030
+ : '';
1031
+ void _partyCondition;
1032
+ // Note: Markers don't have round numbers, so we just return time-based groupings
1033
+ // This method shouldn't be used for markers - use getAppMarkerTimeSeriesData instead
1034
+ // Returning empty result set as markers don't have rounds
1035
+ const query = `
1036
+ SELECT
1037
+ 0::bigint as round,
1038
+ NOW()::text as timestamp,
1039
+ 0::bigint as archived_count,
1040
+ 0::bigint as created_count,
1041
+ 0::bigint as count,
1042
+ 0::numeric as archived_weight,
1043
+ 0::numeric as created_weight,
1044
+ 0::numeric as amount,
1045
+ 0::numeric as coupon_amount
1046
+ WHERE false
1047
+ `;
1048
+ const result = await this.pool.query(query);
1049
+ return result.rows.map(row => ({
1050
+ timestamp: row.timestamp
1051
+ ? new Date(row.timestamp).toISOString()
1052
+ : new Date().toISOString(),
1053
+ count: parseInt(row.count, 10),
1054
+ archivedCount: parseInt(row.archived_count, 10),
1055
+ createdCount: parseInt(row.created_count, 10),
1056
+ archivedWeight: parseFloat(row.archived_weight),
1057
+ createdWeight: parseFloat(row.created_weight),
1058
+ amount: 0,
1059
+ couponAmount: 0,
1060
+ round: typeof row.round === 'number' ? row.round : parseInt(row.round, 10),
1061
+ }));
1062
+ }
1063
+ async getUniqueMarkerBeneficiaryParties() {
1064
+ const query = `
1065
+ SELECT DISTINCT
1066
+ beneficiary_party_id as party_id,
1067
+ beneficiary_party_id as display_name
1068
+ FROM canton_app_markers
1069
+ WHERE beneficiary_party_id IS NOT NULL
1070
+ ORDER BY beneficiary_party_id ASC
1071
+ `;
1072
+ const result = await this.pool.query(query);
1073
+ return result.rows;
1074
+ }
1075
+ async getMonthlyPaymentTotalForParty(partyId, monthStart, monthEnd) {
1076
+ // Check for transfers using created_at since tx_record_time might be null
1077
+ const query = `
1078
+ SELECT COALESCE(SUM(transfer_amount), 0) as total_amount
1079
+ FROM canton_transfers
1080
+ WHERE transfer_sender_party_id = $1
1081
+ AND created_at >= $2
1082
+ AND created_at <= $3
1083
+ AND status = $4
1084
+ `;
1085
+ const result = await this.pool.query(query, [
1086
+ partyId,
1087
+ monthStart,
1088
+ monthEnd,
1089
+ types_1.TransferStatus.CONFIRMED,
1090
+ ]);
1091
+ return parseFloat(result.rows[0].total_amount);
1092
+ }
1093
+ getRemainingRoundsInMonth(roundDurationMinutes = 10) {
1094
+ if (roundDurationMinutes <= 0) {
1095
+ throw new Error('roundDurationMinutes must be greater than 0');
1096
+ }
1097
+ const now = new Date();
1098
+ const nextMonth = new Date(now.getFullYear(), now.getMonth() + 1, 1);
1099
+ const monthEnd = new Date(nextMonth.getTime() - 1); // Last moment of current month
1100
+ // Calculate how many intervals are remaining in the current month
1101
+ const roundDurationInMs = roundDurationMinutes * 60 * 1000;
1102
+ const remainingTimeInMs = monthEnd.getTime() - now.getTime();
1103
+ const remainingRounds = Math.max(0, Math.floor(remainingTimeInMs / roundDurationInMs));
1104
+ return remainingRounds;
1105
+ }
1106
+ async getMonthlyPaymentTotalsForParties(partyIds, monthStart, monthEnd) {
1107
+ if (partyIds.length === 0) {
1108
+ return new Map();
1109
+ }
1110
+ // Create placeholders for the IN clause
1111
+ const placeholders = partyIds.map((_, index) => `$${index + 4}`).join(',');
1112
+ const query = `
1113
+ SELECT transfer_sender_party_id, COALESCE(SUM(transfer_amount), 0) as total_amount
1114
+ FROM canton_transfers
1115
+ WHERE transfer_sender_party_id IN (${placeholders})
1116
+ AND created_at >= $1
1117
+ AND created_at <= $2
1118
+ AND status = $3
1119
+ GROUP BY transfer_sender_party_id
1120
+ `;
1121
+ const params = [
1122
+ monthStart,
1123
+ monthEnd,
1124
+ types_1.TransferStatus.CONFIRMED,
1125
+ ...partyIds,
1126
+ ];
1127
+ const result = await this.pool.query(query, params);
1128
+ const totals = new Map();
1129
+ result.rows.forEach(row => {
1130
+ totals.set(row.transfer_sender_party_id, parseFloat(row.total_amount));
1131
+ });
1132
+ // Ensure all party IDs are in the map (with 0 if no transfers found)
1133
+ partyIds.forEach(partyId => {
1134
+ if (!totals.has(partyId)) {
1135
+ totals.set(partyId, 0);
1136
+ }
1137
+ });
1138
+ return totals;
1139
+ }
1140
+ async getLifetimePaymentStatisticsForParties(partyIds) {
1141
+ if (partyIds.length === 0) {
1142
+ return new Map();
1143
+ }
1144
+ // Create placeholders for the IN clause
1145
+ const placeholders = partyIds.map((_, index) => `$${index + 1}`).join(',');
1146
+ const query = `
1147
+ SELECT
1148
+ transfer_sender_party_id,
1149
+ COUNT(*) as payment_count,
1150
+ COALESCE(SUM(transfer_amount), 0) as total_value_sent
1151
+ FROM canton_transfers
1152
+ WHERE transfer_sender_party_id IN (${placeholders})
1153
+ AND status = $${partyIds.length + 1}
1154
+ GROUP BY transfer_sender_party_id
1155
+ `;
1156
+ const params = [...partyIds, types_1.TransferStatus.CONFIRMED];
1157
+ const result = await this.pool.query(query, params);
1158
+ const statistics = new Map();
1159
+ result.rows.forEach(row => {
1160
+ const paymentCount = parseInt(row.payment_count, 10);
1161
+ const totalValueSent = parseFloat(row.total_value_sent);
1162
+ const totalFeesPaid = paymentCount * 0.6; // 0.6 * count as specified
1163
+ statistics.set(row.transfer_sender_party_id, {
1164
+ paymentCount,
1165
+ totalValueSent,
1166
+ totalFeesPaid,
1167
+ });
1168
+ });
1169
+ // Ensure all party IDs are in the map (with 0 if no transfers found)
1170
+ partyIds.forEach(partyId => {
1171
+ if (!statistics.has(partyId)) {
1172
+ statistics.set(partyId, {
1173
+ paymentCount: 0,
1174
+ totalValueSent: 0,
1175
+ totalFeesPaid: 0,
1176
+ });
1177
+ }
1178
+ });
1179
+ return statistics;
1180
+ }
1181
+ /**
1182
+ * Get portals that have enable_canton_rewards=true but don't have canton parties yet
1183
+ *
1184
+ * @returns Array of portal info that need canton parties created
1185
+ */
1186
+ async getPortalsNeedingCantonParties() {
1187
+ const query = `
1188
+ SELECT p.id, p.company->>'name' as company_name
1189
+ FROM portal p
1190
+ WHERE p.enable_canton_rewards = true
1191
+ AND NOT EXISTS (
1192
+ SELECT 1 FROM canton_parties cp
1193
+ WHERE cp.portal_id = p.id
1194
+ )
1195
+ ORDER BY p.created_at ASC
1196
+ `;
1197
+ const result = await this.pool.query(query);
1198
+ return result.rows.map(row => ({
1199
+ id: row.id,
1200
+ companyName: row.company_name ?? undefined,
1201
+ }));
1202
+ }
1203
+ // Transaction helper - runs a callback within a database transaction
1204
+ async runInTransaction(callback) {
1205
+ const client = await this.pool.connect();
1206
+ try {
1207
+ await client.query('BEGIN');
1208
+ const result = await callback(client);
1209
+ await client.query('COMMIT');
1210
+ return result;
1211
+ }
1212
+ catch (error) {
1213
+ await client.query('ROLLBACK');
1214
+ throw error;
1215
+ }
1216
+ finally {
1217
+ client.release();
1218
+ }
1219
+ }
1220
+ // Canton App Markers CRUD operations
1221
+ async insertCantonAppMarker(marker) {
1222
+ const query = `
1223
+ INSERT INTO canton_app_markers (
1224
+ status, contract_id, provider_party_id, beneficiary_party_id, tx_record_time, weight
1225
+ ) VALUES ($1, $2, $3, $4, $5, $6)
1226
+ RETURNING *
1227
+ `;
1228
+ const values = [
1229
+ marker.status,
1230
+ marker.contract_id,
1231
+ marker.provider_party_id,
1232
+ marker.beneficiary_party_id,
1233
+ marker.tx_record_time,
1234
+ marker.weight,
1235
+ ];
1236
+ const result = await this.pool.query(query, values);
1237
+ return this.mapAppMarkerFromDb(result.rows[0]);
1238
+ }
1239
+ async batchInsertCantonAppMarkers(markers) {
1240
+ if (markers.length === 0) {
1241
+ return { insertedCount: 0, skippedCount: 0 };
1242
+ }
1243
+ // Build the VALUES clause with placeholders
1244
+ const valuesPlaceholders = markers
1245
+ .map((_, idx) => `($${idx * 6 + 1}, $${idx * 6 + 2}, $${idx * 6 + 3}, $${idx * 6 + 4}, $${idx * 6 + 5}, $${idx * 6 + 6})`)
1246
+ .join(', ');
1247
+ const query = `
1248
+ INSERT INTO canton_app_markers (
1249
+ status, contract_id, provider_party_id, beneficiary_party_id, tx_record_time, weight
1250
+ ) VALUES ${valuesPlaceholders}
1251
+ ON CONFLICT (contract_id) DO NOTHING
1252
+ RETURNING *
1253
+ `;
1254
+ // Flatten all marker values into a single array
1255
+ const values = markers.flatMap(marker => [
1256
+ marker.status,
1257
+ marker.contract_id,
1258
+ marker.provider_party_id,
1259
+ marker.beneficiary_party_id,
1260
+ marker.tx_record_time,
1261
+ marker.weight,
1262
+ ]);
1263
+ const result = await this.pool.query(query, values);
1264
+ const insertedCount = result.rowCount ?? 0;
1265
+ const skippedCount = markers.length - insertedCount;
1266
+ return { insertedCount, skippedCount };
1267
+ }
1268
+ async upsertCantonAppMarkerAsArchived(marker) {
1269
+ const query = `
1270
+ INSERT INTO canton_app_markers (
1271
+ status, contract_id, provider_party_id, beneficiary_party_id, tx_record_time, weight
1272
+ ) VALUES ($1, $2, $3, $4, $5, $6)
1273
+ ON CONFLICT (contract_id) DO UPDATE
1274
+ SET status = EXCLUDED.status, updated_at = NOW()
1275
+ RETURNING *
1276
+ `;
1277
+ const values = [
1278
+ marker.status,
1279
+ marker.contract_id,
1280
+ marker.provider_party_id,
1281
+ marker.beneficiary_party_id,
1282
+ marker.tx_record_time,
1283
+ marker.weight,
1284
+ ];
1285
+ const result = await this.pool.query(query, values);
1286
+ return this.mapAppMarkerFromDb(result.rows[0]);
1287
+ }
1288
+ async getCantonAppMarker(id) {
1289
+ const query = 'SELECT * FROM canton_app_markers WHERE id = $1';
1290
+ const result = await this.pool.query(query, [id]);
1291
+ return result.rows.length > 0
1292
+ ? this.mapAppMarkerFromDb(result.rows[0])
1293
+ : null;
1294
+ }
1295
+ async getCantonAppMarkerByContractId(contractId) {
1296
+ const query = 'SELECT * FROM canton_app_markers WHERE contract_id = $1';
1297
+ const result = await this.pool.query(query, [contractId]);
1298
+ return result.rows.length > 0
1299
+ ? this.mapAppMarkerFromDb(result.rows[0])
1300
+ : null;
1301
+ }
1302
+ async getCantonAppMarkersByContractIds(contractIds) {
1303
+ if (contractIds.length === 0) {
1304
+ return [];
1305
+ }
1306
+ const placeholders = contractIds
1307
+ .map((_, index) => `$${index + 1}`)
1308
+ .join(', ');
1309
+ const query = `
1310
+ SELECT * FROM canton_app_markers
1311
+ WHERE contract_id IN (${placeholders})
1312
+ `;
1313
+ const result = await this.pool.query(query, contractIds);
1314
+ return result.rows.map(row => this.mapAppMarkerFromDb(row));
1315
+ }
1316
+ async batchUpdateCantonAppMarkersByContractIds(contractIds, updates) {
1317
+ if (contractIds.length === 0) {
1318
+ return [];
1319
+ }
1320
+ const setClauses = [];
1321
+ const values = [];
1322
+ let paramIndex = 1;
1323
+ // Build dynamic update query
1324
+ Object.entries(updates).forEach(([key, value]) => {
1325
+ if (key !== 'id' && key !== 'created_at') {
1326
+ setClauses.push(`${this.toSnakeCase(key)} = $${paramIndex}`);
1327
+ values.push(value);
1328
+ paramIndex++;
1329
+ }
1330
+ });
1331
+ if (setClauses.length === 0) {
1332
+ throw new Error('No valid fields to update');
1333
+ }
1334
+ // Build the WHERE clause for contract IDs
1335
+ const contractIdPlaceholders = contractIds
1336
+ .map((_, index) => `$${paramIndex + index}`)
1337
+ .join(', ');
1338
+ values.push(...contractIds);
1339
+ const query = `
1340
+ UPDATE canton_app_markers
1341
+ SET ${setClauses.join(', ')}, updated_at = NOW()
1342
+ WHERE contract_id IN (${contractIdPlaceholders})
1343
+ RETURNING *
1344
+ `;
1345
+ const result = await this.pool.query(query, values);
1346
+ return result.rows.map(row => this.mapAppMarkerFromDb(row));
1347
+ }
1348
+ async getCantonAppMarkersByStatus(status, limit = 100, sortOrder = 'ASC') {
1349
+ const query = `
1350
+ SELECT * FROM canton_app_markers
1351
+ WHERE status = $1
1352
+ ORDER BY tx_record_time ${sortOrder}
1353
+ LIMIT $2
1354
+ `;
1355
+ const result = await this.pool.query(query, [status, limit]);
1356
+ return result.rows.map(row => this.mapAppMarkerFromDb(row));
1357
+ }
1358
+ async getLatestCantonAppMarkers(limit = 3, beneficiaryPartyId) {
1359
+ let query = `
1360
+ SELECT * FROM canton_app_markers
1361
+ `;
1362
+ const params = [];
1363
+ if (beneficiaryPartyId) {
1364
+ query += ` WHERE beneficiary_party_id = $1`;
1365
+ params.push(beneficiaryPartyId);
1366
+ }
1367
+ query += ` ORDER BY tx_record_time DESC LIMIT $${params.length + 1}`;
1368
+ params.push(limit);
1369
+ const result = await this.pool.query(query, params);
1370
+ return result.rows.map(row => this.mapAppMarkerFromDb(row));
1371
+ }
1372
+ async getOldestUnarchivedCantonAppMarkers(limit = 3, beneficiaryPartyId) {
1373
+ let query = `
1374
+ SELECT * FROM canton_app_markers
1375
+ WHERE status = 'created'
1376
+ `;
1377
+ const params = [];
1378
+ if (beneficiaryPartyId) {
1379
+ query += ` AND beneficiary_party_id = $${params.length + 1}`;
1380
+ params.push(beneficiaryPartyId);
1381
+ }
1382
+ query += ` ORDER BY tx_record_time ASC LIMIT $${params.length + 1}`;
1383
+ params.push(limit);
1384
+ const result = await this.pool.query(query, params);
1385
+ return result.rows.map(row => this.mapAppMarkerFromDb(row));
1386
+ }
1387
+ async updateCantonAppMarker(id, updates) {
1388
+ const setClauses = [];
1389
+ const values = [];
1390
+ let paramIndex = 1;
1391
+ // Build dynamic update query
1392
+ Object.entries(updates).forEach(([key, value]) => {
1393
+ if (key !== 'id' && key !== 'created_at') {
1394
+ setClauses.push(`${this.toSnakeCase(key)} = $${paramIndex}`);
1395
+ values.push(value);
1396
+ paramIndex++;
1397
+ }
1398
+ });
1399
+ if (setClauses.length === 0) {
1400
+ throw new Error('No valid fields to update');
1401
+ }
1402
+ values.push(id);
1403
+ const query = `
1404
+ UPDATE canton_app_markers
1405
+ SET ${setClauses.join(', ')}, updated_at = NOW()
1406
+ WHERE id = $${paramIndex}
1407
+ RETURNING *
1408
+ `;
1409
+ const result = await this.pool.query(query, values);
1410
+ return result.rows.length > 0
1411
+ ? this.mapAppMarkerFromDb(result.rows[0])
1412
+ : null;
1413
+ }
1414
+ // Canton Parties CRUD operations
1415
+ async insertCantonParty(party) {
1416
+ const query = `
1417
+ INSERT INTO canton_parties (
1418
+ party_id, portal_id, provider, last_payment_until, is_active_customer_since
1419
+ ) VALUES ($1, $2, $3, $4, $5)
1420
+ RETURNING *
1421
+ `;
1422
+ const values = [
1423
+ party.party_id,
1424
+ party.portal_id,
1425
+ party.provider,
1426
+ party.last_payment_until,
1427
+ party.is_active_customer_since,
1428
+ ];
1429
+ const result = await this.pool.query(query, values);
1430
+ return this.mapPartyFromDb(result.rows[0]);
1431
+ }
1432
+ async getCantonParty(id) {
1433
+ const query = `
1434
+ SELECT
1435
+ cp.*,
1436
+ p.company
1437
+ FROM canton_parties cp
1438
+ LEFT JOIN portal p ON cp.portal_id = p.id
1439
+ WHERE cp.id = $1
1440
+ `;
1441
+ const result = await this.pool.query(query, [id]);
1442
+ return result.rows.length > 0 ? this.mapPartyFromDb(result.rows[0]) : null;
1443
+ }
1444
+ async getCantonPartyByPartyId(partyId) {
1445
+ const query = `
1446
+ SELECT
1447
+ cp.*,
1448
+ p.company
1449
+ FROM canton_parties cp
1450
+ LEFT JOIN portal p ON cp.portal_id = p.id
1451
+ WHERE cp.party_id = $1
1452
+ `;
1453
+ const result = await this.pool.query(query, [partyId]);
1454
+ return result.rows.length > 0 ? this.mapPartyFromDb(result.rows[0]) : null;
1455
+ }
1456
+ async updateCantonParty(id, updates) {
1457
+ const setClauses = [];
1458
+ const values = [];
1459
+ let paramIndex = 1;
1460
+ // Build dynamic update query - exclude amulets field
1461
+ Object.entries(updates).forEach(([key, value]) => {
1462
+ if (key !== 'id' && key !== 'created_at' && key !== 'amulets') {
1463
+ setClauses.push(`${this.toSnakeCase(key)} = $${paramIndex}`);
1464
+ values.push(value);
1465
+ paramIndex++;
1466
+ }
1467
+ });
1468
+ if (setClauses.length === 0) {
1469
+ throw new Error('No valid fields to update');
1470
+ }
1471
+ values.push(id);
1472
+ const query = `
1473
+ UPDATE canton_parties
1474
+ SET ${setClauses.join(', ')}, updated_at = NOW()
1475
+ WHERE id = $${paramIndex}
1476
+ RETURNING id
1477
+ `;
1478
+ const result = await this.pool.query(query, values);
1479
+ if (result.rows.length > 0) {
1480
+ return this.getCantonParty(id);
1481
+ }
1482
+ return null;
1483
+ }
1484
+ async updateCantonPartyByPartyId(partyId, updates) {
1485
+ const setClauses = [];
1486
+ const values = [];
1487
+ let paramIndex = 1;
1488
+ // Build dynamic update query - exclude amulets field
1489
+ Object.entries(updates).forEach(([key, value]) => {
1490
+ if (key !== 'id' && key !== 'created_at' && key !== 'amulets') {
1491
+ setClauses.push(`${this.toSnakeCase(key)} = $${paramIndex}`);
1492
+ values.push(value);
1493
+ paramIndex++;
1494
+ }
1495
+ });
1496
+ if (setClauses.length === 0) {
1497
+ throw new Error('No valid fields to update');
1498
+ }
1499
+ values.push(partyId);
1500
+ const query = `
1501
+ UPDATE canton_parties
1502
+ SET ${setClauses.join(', ')}, updated_at = NOW()
1503
+ WHERE party_id = $${paramIndex}
1504
+ RETURNING party_id
1505
+ `;
1506
+ const result = await this.pool.query(query, values);
1507
+ if (result.rows.length > 0) {
1508
+ return this.getCantonPartyByPartyId(partyId);
1509
+ }
1510
+ return null;
1511
+ }
1512
+ // Amulet operations removed - use getActiveContracts instead
1513
+ // async addAmuletToParty() - REMOVED
1514
+ // async removeAmuletFromParty() - REMOVED
1515
+ // async replaceAmuletsForParty() - REMOVED
1516
+ async getActiveCustomersWithExpiredPayment() {
1517
+ const query = `
1518
+ SELECT
1519
+ cp.*,
1520
+ p.company
1521
+ FROM canton_parties cp
1522
+ LEFT JOIN portal p ON cp.portal_id = p.id
1523
+ WHERE cp.is_active_customer_since IS NOT NULL
1524
+ AND (cp.last_payment_until IS NULL OR cp.last_payment_until < NOW())
1525
+ ORDER BY cp.last_payment_until ASC NULLS FIRST
1526
+ `;
1527
+ const result = await this.pool.query(query);
1528
+ return result.rows.map(row => this.mapPartyFromDb(row));
1529
+ }
1530
+ async getAllParties(provider) {
1531
+ let query = `
1532
+ SELECT
1533
+ cp.*,
1534
+ p.company
1535
+ FROM canton_parties cp
1536
+ INNER JOIN portal p ON cp.portal_id = p.id
1537
+ `;
1538
+ const values = [];
1539
+ if (provider) {
1540
+ // Handle 5n-broker case since database enum doesn't support it yet
1541
+ if (provider === '5n-broker') {
1542
+ // For now, return empty array since 5n-broker parties aren't in the database yet
1543
+ console.log('⚠️ 5n-broker provider requested - returning empty array (not in database yet)');
1544
+ return [];
1545
+ }
1546
+ query += ` WHERE cp.provider = $1`;
1547
+ values.push(provider);
1548
+ }
1549
+ query += ` ORDER BY cp.created_at ASC`;
1550
+ console.log(`🔍 Database query for network=${this.network}, provider=${provider}:`, { query, values });
1551
+ const result = await this.pool.query(query, values);
1552
+ console.log(`📋 Database returned ${result.rows.length} parties for network=${this.network}, provider=${provider}`);
1553
+ if (result.rows.length > 0) {
1554
+ console.log('📝 Sample database parties:', result.rows
1555
+ .slice(0, 3)
1556
+ .map(row => ({ party_id: row.party_id, provider: row.provider })));
1557
+ }
1558
+ const parties = result.rows.map(row => this.mapPartyFromDb(row));
1559
+ // Get payment statistics for all parties
1560
+ const partyIds = parties.map(party => party.party_id);
1561
+ const paymentStats = await this.getLifetimePaymentStatisticsForParties(partyIds);
1562
+ // Merge payment statistics with party data
1563
+ return parties.map(party => {
1564
+ const stats = paymentStats.get(party.party_id);
1565
+ return {
1566
+ ...party,
1567
+ payment_count: stats?.paymentCount ?? 0,
1568
+ total_value_sent: stats?.totalValueSent ?? 0,
1569
+ total_fees_paid: stats?.totalFeesPaid ?? 0,
1570
+ };
1571
+ });
1572
+ }
1573
+ // OCF Deployments CRUD operations
1574
+ async insertOcfDeployment(deployment) {
1575
+ const query = `
1576
+ INSERT INTO ocf_deployments (
1577
+ ocf_object_id, version, chain_id, status, tx_hash, contract_id, party_id, wallet_address
1578
+ ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
1579
+ RETURNING id, ocf_object_id, version, chain_id, status, tx_hash, contract_id, party_id, wallet_address, created_at, updated_at
1580
+ `;
1581
+ const values = [
1582
+ deployment.ocf_object_id,
1583
+ deployment.version,
1584
+ deployment.chain_id,
1585
+ deployment.status,
1586
+ deployment.tx_hash,
1587
+ deployment.contract_id,
1588
+ deployment.party_id,
1589
+ deployment.wallet_address,
1590
+ ];
1591
+ const result = await this.pool.query(query, values);
1592
+ const row = result.rows[0];
1593
+ return {
1594
+ id: row.id,
1595
+ ocf_object_id: row.ocf_object_id,
1596
+ version: row.version,
1597
+ chain_id: row.chain_id,
1598
+ status: row.status,
1599
+ tx_hash: row.tx_hash,
1600
+ contract_id: row.contract_id,
1601
+ party_id: row.party_id,
1602
+ wallet_address: row.wallet_address,
1603
+ created_at: row.created_at,
1604
+ updated_at: row.updated_at,
1605
+ };
1606
+ }
1607
+ async getLatestOcfDeploymentByPartyId(partyId) {
1608
+ const query = `
1609
+ SELECT id, ocf_object_id, version, chain_id, status, tx_hash, contract_id, party_id, wallet_address, created_at, updated_at
1610
+ FROM ocf_deployments
1611
+ WHERE party_id = $1
1612
+ ORDER BY version DESC
1613
+ LIMIT 1
1614
+ `;
1615
+ const result = await this.pool.query(query, [partyId]);
1616
+ if (result.rows.length === 0) {
1617
+ return null;
1618
+ }
1619
+ const row = result.rows[0];
1620
+ return {
1621
+ id: row.id,
1622
+ ocf_object_id: row.ocf_object_id,
1623
+ version: row.version,
1624
+ chain_id: row.chain_id,
1625
+ status: row.status,
1626
+ tx_hash: row.tx_hash,
1627
+ contract_id: row.contract_id,
1628
+ party_id: row.party_id,
1629
+ wallet_address: row.wallet_address,
1630
+ created_at: row.created_at,
1631
+ updated_at: row.updated_at,
1632
+ };
1633
+ }
1634
+ async getLatestOcfObjectByPortalId(portalId) {
1635
+ const query = `
1636
+ SELECT o.id as ocf_object_id, o.version
1637
+ FROM latest_ocf_objects o
1638
+ WHERE o.portal_id = $1
1639
+ AND o.type = 'ISSUER'
1640
+ ORDER BY o.version DESC
1641
+ LIMIT 1
1642
+ `;
1643
+ const result = await this.pool.query(query, [portalId]);
1644
+ if (result.rows.length === 0) {
1645
+ return null;
1646
+ }
1647
+ const row = result.rows[0];
1648
+ return {
1649
+ ocf_object_id: row.ocf_object_id,
1650
+ version: row.version,
1651
+ };
1652
+ }
1653
+ /** Retrieve a specific OCF object row by id and version */
1654
+ async getOcfObjectDataByIdAndVersion(ocfObjectId, version) {
1655
+ const query = `
1656
+ SELECT type, subtype, ocf_data
1657
+ FROM ocf_objects
1658
+ WHERE id = $1 AND version = $2
1659
+ LIMIT 1
1660
+ `;
1661
+ const result = await this.pool.query(query, [ocfObjectId, version]);
1662
+ if (result.rows.length === 0)
1663
+ return null;
1664
+ const row = result.rows[0];
1665
+ return {
1666
+ type: row.type,
1667
+ subtype: row.subtype ?? null,
1668
+ ocf_data: row.ocf_data,
1669
+ };
1670
+ }
1671
+ /** List OCF deployments, optionally filtered */
1672
+ async listOcfDeployments(params) {
1673
+ const conditions = [];
1674
+ const values = [];
1675
+ let idx = 1;
1676
+ if (params?.chainId) {
1677
+ conditions.push(`chain_id = $${idx++}`);
1678
+ values.push(params.chainId);
1679
+ }
1680
+ if (params?.status) {
1681
+ conditions.push(`status = $${idx++}`);
1682
+ values.push(params.status);
1683
+ }
1684
+ if (params?.partyId) {
1685
+ conditions.push(`party_id = $${idx++}`);
1686
+ values.push(params.partyId);
1687
+ }
1688
+ const whereClause = conditions.length
1689
+ ? `WHERE ${conditions.join(' AND ')}`
1690
+ : '';
1691
+ const limitClause = typeof params?.limit === 'number' ? `LIMIT ${params.limit}` : '';
1692
+ const offsetClause = typeof params?.offset === 'number' ? `OFFSET ${params.offset}` : '';
1693
+ const query = `
1694
+ SELECT id, ocf_object_id, version, chain_id, status, tx_hash, contract_id, party_id, wallet_address, created_at, updated_at
1695
+ FROM ocf_deployments
1696
+ ${whereClause}
1697
+ ORDER BY created_at DESC
1698
+ ${limitClause}
1699
+ ${offsetClause}
1700
+ `;
1701
+ const result = await this.pool.query(query, values);
1702
+ return result.rows.map(row => ({
1703
+ id: row.id,
1704
+ ocf_object_id: row.ocf_object_id,
1705
+ version: row.version,
1706
+ chain_id: row.chain_id,
1707
+ status: row.status,
1708
+ tx_hash: row.tx_hash,
1709
+ contract_id: row.contract_id,
1710
+ party_id: row.party_id,
1711
+ wallet_address: row.wallet_address,
1712
+ created_at: row.created_at,
1713
+ updated_at: row.updated_at,
1714
+ }));
1715
+ }
1716
+ /** Count total rows in latest_ocf_objects for progress stats */
1717
+ async countLatestOcfObjects() {
1718
+ const query = `SELECT COUNT(*) AS cnt FROM latest_ocf_objects`;
1719
+ const result = await this.pool.query(query);
1720
+ return Number(result.rows[0]?.cnt ?? 0);
1721
+ }
1722
+ /**
1723
+ * Count rows in latest_ocf_objects that belong to portals with sync_captable_onchain=true Used for actionable
1724
+ * progress stats (objects that should be synced onchain)
1725
+ */
1726
+ async countLatestOcfObjectsForOnchainSync() {
1727
+ const query = `
1728
+ SELECT COUNT(*) AS cnt
1729
+ FROM latest_ocf_objects o
1730
+ JOIN portal p ON p.id = o.portal_id
1731
+ WHERE p.enable_canton_rewards = true
1732
+ `;
1733
+ const result = await this.pool.query(query);
1734
+ return Number(result.rows[0]?.cnt ?? 0);
1735
+ }
1736
+ async getLatestOcfObjectDataByPortalId(portalId) {
1737
+ const query = `
1738
+ SELECT o.id as ocf_object_id, o.version, o.ocf_data, o.type
1739
+ FROM latest_ocf_objects o
1740
+ WHERE o.portal_id = $1
1741
+ AND o.type = 'ISSUER'
1742
+ ORDER BY o.version DESC
1743
+ LIMIT 1
1744
+ `;
1745
+ const result = await this.pool.query(query, [portalId]);
1746
+ if (result.rows.length === 0) {
1747
+ return null;
1748
+ }
1749
+ const row = result.rows[0];
1750
+ return {
1751
+ ocf_object_id: row.ocf_object_id,
1752
+ version: row.version,
1753
+ ocf_data: row.ocf_data,
1754
+ type: row.type,
1755
+ };
1756
+ }
1757
+ async getPartiesWithoutOcfObjects() {
1758
+ const query = `
1759
+ SELECT
1760
+ cp.party_id,
1761
+ cp.portal_id,
1762
+ cp.provider,
1763
+ p.company
1764
+ FROM canton_parties cp
1765
+ LEFT JOIN portal p ON cp.portal_id = p.id
1766
+ WHERE NOT EXISTS (
1767
+ SELECT 1 FROM latest_ocf_objects o
1768
+ WHERE o.portal_id = cp.portal_id
1769
+ AND o.type = 'ISSUER'
1770
+ )
1771
+ ORDER BY cp.created_at ASC
1772
+ `;
1773
+ const result = await this.pool.query(query);
1774
+ return result.rows;
1775
+ }
1776
+ async getPartiesWithOcfObjectsButNoIssuerDeployments() {
1777
+ const query = `
1778
+ SELECT
1779
+ cp.party_id,
1780
+ cp.portal_id,
1781
+ cp.provider,
1782
+ p.company,
1783
+ o.id as ocf_object_id,
1784
+ o.version as ocf_version,
1785
+ o.ocf_data,
1786
+ o.type as ocf_type
1787
+ FROM canton_parties cp
1788
+ LEFT JOIN portal p ON cp.portal_id = p.id
1789
+ INNER JOIN latest_ocf_objects o ON o.portal_id = cp.portal_id AND o.type = 'ISSUER'
1790
+ WHERE NOT EXISTS (
1791
+ SELECT 1 FROM ocf_deployments od
1792
+ WHERE od.party_id = cp.party_id
1793
+ AND od.ocf_object_id = o.id
1794
+ )
1795
+ ORDER BY cp.created_at ASC
1796
+ `;
1797
+ const result = await this.pool.query(query);
1798
+ return result.rows;
1799
+ }
1800
+ async getPartiesWithOutdatedIssuerDeployments() {
1801
+ const query = `
1802
+ SELECT
1803
+ cp.party_id,
1804
+ cp.portal_id,
1805
+ cp.provider,
1806
+ p.company,
1807
+ latest_ocf.version as latest_ocf_version,
1808
+ latest_deployment.version as deployed_version,
1809
+ latest_ocf.id as latest_ocf_object_id,
1810
+ latest_deployment.contract_id as current_contract_id,
1811
+ latest_ocf.ocf_data,
1812
+ latest_ocf.type as ocf_type
1813
+ FROM canton_parties cp
1814
+ LEFT JOIN portal p ON cp.portal_id = p.id
1815
+ INNER JOIN (
1816
+ SELECT o.portal_id, o.id, o.version, o.ocf_data, o.type
1817
+ FROM latest_ocf_objects o
1818
+ WHERE o.type = 'ISSUER'
1819
+ ) latest_ocf ON latest_ocf.portal_id = cp.portal_id
1820
+ INNER JOIN (
1821
+ SELECT od.party_id, od.ocf_object_id, od.version, od.contract_id,
1822
+ ROW_NUMBER() OVER (PARTITION BY od.party_id ORDER BY od.version DESC) as rn
1823
+ FROM ocf_deployments od
1824
+ WHERE od.status = 'deployed'
1825
+ ) latest_deployment ON latest_deployment.party_id = cp.party_id
1826
+ AND latest_deployment.rn = 1
1827
+ WHERE latest_ocf.version > latest_deployment.version
1828
+ ORDER BY cp.created_at ASC
1829
+ `;
1830
+ const result = await this.pool.query(query);
1831
+ return result.rows;
1832
+ }
1833
+ async getOnchainEquityValuations() {
1834
+ const query = `
1835
+ SELECT p.id AS portal_id,
1836
+ p.id AS company_id,
1837
+ p.company->>'name' AS company_name,
1838
+ CASE
1839
+ WHEN COALESCE((pp.company_data ->> 'company_custom_valuation')::numeric, 0) > COALESCE((pp.company_data ->> 'company_computed_valuation')::numeric, 0)
1840
+ THEN COALESCE((pp.company_data ->> 'company_custom_valuation')::numeric, 0)
1841
+ ELSE COALESCE((pp.company_data ->> 'company_computed_valuation')::numeric, 0)
1842
+ END AS company_valuation
1843
+ FROM portal p
1844
+ JOIN portal_private pp ON pp.portal_id = p.id
1845
+ WHERE (p.company ->> 'name') !~* 'Fairbnb'
1846
+ AND (
1847
+ p.captable_minted
1848
+ OR (
1849
+ p.domain !~* 'staging'
1850
+ AND p.domain !~* 'websitecf'
1851
+ AND p.domain !~* '.cafe'
1852
+ )
1853
+ )
1854
+ ORDER BY p.captable_minted,
1855
+ CASE
1856
+ WHEN COALESCE((pp.company_data ->> 'company_custom_valuation')::numeric, 0) > COALESCE((pp.company_data ->> 'company_computed_valuation')::numeric, 0)
1857
+ THEN COALESCE((pp.company_data ->> 'company_custom_valuation')::numeric, 0)
1858
+ ELSE COALESCE((pp.company_data ->> 'company_computed_valuation')::numeric, 0)
1859
+ END DESC
1860
+ `;
1861
+ const result = await this.pool.query(query);
1862
+ return result.rows.map(row => ({
1863
+ portal_id: row.portal_id,
1864
+ company_id: row.company_id,
1865
+ company_name: row.company_name ?? null,
1866
+ company_valuation: Number(row.company_valuation ?? 0),
1867
+ }));
1868
+ }
1869
+ async getPortalsNeedingStockClassDeployments() {
1870
+ const query = `
1871
+ SELECT DISTINCT
1872
+ cp.portal_id,
1873
+ cp.party_id,
1874
+ cp.provider,
1875
+ cp.created_at,
1876
+ p.company,
1877
+ loo.ocf_data,
1878
+ loo.id AS ocf_object_id,
1879
+ loo.version AS ocf_version,
1880
+ (
1881
+ SELECT od_issuer.contract_id
1882
+ FROM ocf_deployments od_issuer
1883
+ JOIN latest_ocf_objects loo_issuer ON od_issuer.ocf_object_id = loo_issuer.id
1884
+ WHERE od_issuer.party_id = cp.party_id
1885
+ AND od_issuer.status = 'deployed'
1886
+ AND loo_issuer.type = 'ISSUER'
1887
+ ORDER BY od_issuer.created_at DESC
1888
+ LIMIT 1
1889
+ ) as issuer_contract_id
1890
+ FROM canton_parties cp
1891
+ LEFT JOIN portal p ON cp.portal_id = p.id
1892
+ JOIN latest_ocf_objects loo ON cp.portal_id = loo.portal_id
1893
+ WHERE
1894
+ loo.type = 'STOCK_CLASS'
1895
+ AND EXISTS (
1896
+ SELECT 1 FROM ocf_deployments od_issuer
1897
+ JOIN latest_ocf_objects loo_issuer ON od_issuer.ocf_object_id = loo_issuer.id
1898
+ WHERE od_issuer.party_id = cp.party_id
1899
+ AND od_issuer.status = 'deployed'
1900
+ AND loo_issuer.type = 'ISSUER'
1901
+ )
1902
+ AND NOT EXISTS (
1903
+ SELECT 1 FROM ocf_deployments od_sc
1904
+ WHERE od_sc.ocf_object_id = loo.id
1905
+ AND od_sc.party_id = cp.party_id
1906
+ AND od_sc.status = 'deployed'
1907
+ )
1908
+ ORDER BY cp.created_at ASC
1909
+ `;
1910
+ const result = await this.pool.query(query);
1911
+ return result.rows;
1912
+ }
1913
+ async getPortalsNeedingStockClassUpdates() {
1914
+ const query = `
1915
+ SELECT DISTINCT
1916
+ cp.portal_id,
1917
+ cp.party_id,
1918
+ cp.provider,
1919
+ cp.created_at,
1920
+ p.company,
1921
+ loo.ocf_data,
1922
+ loo.id AS ocf_object_id,
1923
+ loo.version as latest_ocf_version,
1924
+ od.version as deployed_version,
1925
+ od.contract_id as current_contract_id,
1926
+ (loo.version - od.version) AS version_diff,
1927
+ (
1928
+ SELECT od_issuer.contract_id
1929
+ FROM ocf_deployments od_issuer
1930
+ JOIN latest_ocf_objects loo_issuer ON od_issuer.ocf_object_id = loo_issuer.id
1931
+ WHERE od_issuer.party_id = cp.party_id
1932
+ AND od_issuer.status = 'deployed'
1933
+ AND loo_issuer.type = 'ISSUER'
1934
+ ORDER BY od_issuer.created_at DESC
1935
+ LIMIT 1
1936
+ ) as issuer_contract_id
1937
+ FROM canton_parties cp
1938
+ LEFT JOIN portal p ON cp.portal_id = p.id
1939
+ JOIN latest_ocf_objects loo ON cp.portal_id = loo.portal_id
1940
+ JOIN ocf_deployments od ON loo.id = od.ocf_object_id AND cp.party_id = od.party_id
1941
+ WHERE
1942
+ loo.type = 'STOCK_CLASS'
1943
+ AND EXISTS (
1944
+ SELECT 1 FROM ocf_deployments od_issuer
1945
+ JOIN latest_ocf_objects loo_issuer ON od_issuer.ocf_object_id = loo_issuer.id
1946
+ WHERE od_issuer.party_id = cp.party_id
1947
+ AND od_issuer.status = 'deployed'
1948
+ AND loo_issuer.type = 'ISSUER'
1949
+ )
1950
+ AND od.version < loo.version
1951
+ AND od.status = 'deployed'
1952
+ ORDER BY version_diff DESC, cp.created_at ASC
1953
+ `;
1954
+ const result = await this.pool.query(query);
1955
+ return result.rows;
1956
+ }
1957
+ async getPortalsNeedingStakeholderDeployments() {
1958
+ const query = `
1959
+ SELECT DISTINCT
1960
+ cp.portal_id,
1961
+ cp.party_id,
1962
+ cp.provider,
1963
+ cp.created_at,
1964
+ p.company,
1965
+ loo.ocf_data,
1966
+ loo.id AS ocf_object_id,
1967
+ loo.version AS ocf_version,
1968
+ (
1969
+ SELECT od_issuer.contract_id
1970
+ FROM ocf_deployments od_issuer
1971
+ JOIN latest_ocf_objects loo_issuer ON od_issuer.ocf_object_id = loo_issuer.id
1972
+ WHERE od_issuer.party_id = cp.party_id
1973
+ AND od_issuer.status = 'deployed'
1974
+ AND loo_issuer.type = 'ISSUER'
1975
+ ORDER BY od_issuer.created_at DESC
1976
+ LIMIT 1
1977
+ ) as issuer_contract_id
1978
+ FROM canton_parties cp
1979
+ LEFT JOIN portal p ON cp.portal_id = p.id
1980
+ JOIN latest_ocf_objects loo ON cp.portal_id = loo.portal_id
1981
+ WHERE
1982
+ loo.type = 'STAKEHOLDER'
1983
+ AND EXISTS (
1984
+ SELECT 1 FROM ocf_deployments od_issuer
1985
+ JOIN latest_ocf_objects loo_issuer ON od_issuer.ocf_object_id = loo_issuer.id
1986
+ WHERE od_issuer.party_id = cp.party_id
1987
+ AND od_issuer.status = 'deployed'
1988
+ AND loo_issuer.type = 'ISSUER'
1989
+ )
1990
+ AND NOT EXISTS (
1991
+ SELECT 1 FROM ocf_deployments od_sh
1992
+ WHERE od_sh.ocf_object_id = loo.id
1993
+ AND od_sh.party_id = cp.party_id
1994
+ AND od_sh.status = 'deployed'
1995
+ )
1996
+ ORDER BY cp.created_at ASC
1997
+ `;
1998
+ const result = await this.pool.query(query);
1999
+ return result.rows;
2000
+ }
2001
+ async getPortalsNeedingStakeholderReDeployments() {
2002
+ const query = `
2003
+ SELECT DISTINCT
2004
+ cp.portal_id,
2005
+ cp.party_id,
2006
+ cp.provider,
2007
+ cp.created_at,
2008
+ p.company,
2009
+ loo.ocf_data,
2010
+ loo.id AS ocf_object_id,
2011
+ loo.version as latest_ocf_version,
2012
+ od.version as deployed_version,
2013
+ od.contract_id as current_contract_id,
2014
+ (loo.version - od.version) AS version_diff,
2015
+ (
2016
+ SELECT od_issuer.contract_id
2017
+ FROM ocf_deployments od_issuer
2018
+ JOIN latest_ocf_objects loo_issuer ON od_issuer.ocf_object_id = loo_issuer.id
2019
+ WHERE od_issuer.party_id = cp.party_id
2020
+ AND od_issuer.status = 'deployed'
2021
+ AND loo_issuer.type = 'ISSUER'
2022
+ ORDER BY od_issuer.created_at DESC
2023
+ LIMIT 1
2024
+ ) as issuer_contract_id
2025
+ FROM canton_parties cp
2026
+ LEFT JOIN portal p ON cp.portal_id = p.id
2027
+ JOIN latest_ocf_objects loo ON cp.portal_id = loo.portal_id
2028
+ JOIN ocf_deployments od ON loo.id = od.ocf_object_id AND cp.party_id = od.party_id
2029
+ WHERE
2030
+ loo.type = 'STAKEHOLDER'
2031
+ AND EXISTS (
2032
+ SELECT 1 FROM ocf_deployments od_issuer
2033
+ JOIN latest_ocf_objects loo_issuer ON od_issuer.ocf_object_id = loo_issuer.id
2034
+ WHERE od_issuer.party_id = cp.party_id
2035
+ AND od_issuer.status = 'deployed'
2036
+ AND loo_issuer.type = 'ISSUER'
2037
+ )
2038
+ AND od.version < loo.version
2039
+ AND od.status = 'deployed'
2040
+ ORDER BY version_diff DESC, cp.created_at ASC
2041
+ `;
2042
+ const result = await this.pool.query(query);
2043
+ return result.rows;
2044
+ }
2045
+ async getPortalsNeedingStockPlanDeployments() {
2046
+ const query = `
2047
+ SELECT DISTINCT
2048
+ cp.portal_id,
2049
+ cp.party_id,
2050
+ cp.provider,
2051
+ cp.created_at,
2052
+ p.company,
2053
+ loo.ocf_data,
2054
+ loo.id AS ocf_object_id,
2055
+ loo.version AS ocf_version,
2056
+ (
2057
+ SELECT od_issuer.contract_id
2058
+ FROM ocf_deployments od_issuer
2059
+ JOIN latest_ocf_objects loo_issuer ON od_issuer.ocf_object_id = loo_issuer.id
2060
+ WHERE od_issuer.party_id = cp.party_id
2061
+ AND od_issuer.status = 'deployed'
2062
+ AND loo_issuer.type = 'ISSUER'
2063
+ ORDER BY od_issuer.created_at DESC
2064
+ LIMIT 1
2065
+ ) as issuer_contract_id
2066
+ FROM canton_parties cp
2067
+ LEFT JOIN portal p ON cp.portal_id = p.id
2068
+ JOIN latest_ocf_objects loo ON cp.portal_id = loo.portal_id
2069
+ WHERE
2070
+ loo.type = 'STOCK_PLAN'
2071
+ AND EXISTS (
2072
+ SELECT 1 FROM ocf_deployments od_issuer
2073
+ JOIN latest_ocf_objects loo_issuer ON od_issuer.ocf_object_id = loo_issuer.id
2074
+ WHERE od_issuer.party_id = cp.party_id
2075
+ AND od_issuer.status = 'deployed'
2076
+ AND loo_issuer.type = 'ISSUER'
2077
+ )
2078
+ AND NOT EXISTS (
2079
+ SELECT 1 FROM ocf_deployments od_sp
2080
+ WHERE od_sp.ocf_object_id = loo.id
2081
+ AND od_sp.party_id = cp.party_id
2082
+ AND od_sp.status = 'deployed'
2083
+ )
2084
+ ORDER BY cp.created_at ASC
2085
+ `;
2086
+ const result = await this.pool.query(query);
2087
+ return result.rows;
2088
+ }
2089
+ async getPortalsNeedingStockPlanReDeployments() {
2090
+ const query = `
2091
+ SELECT DISTINCT
2092
+ cp.portal_id,
2093
+ cp.party_id,
2094
+ cp.provider,
2095
+ cp.created_at,
2096
+ p.company,
2097
+ loo.ocf_data,
2098
+ loo.id AS ocf_object_id,
2099
+ loo.version as latest_ocf_version,
2100
+ od.version as deployed_version,
2101
+ od.contract_id as current_contract_id,
2102
+ (loo.version - od.version) AS version_diff,
2103
+ (
2104
+ SELECT od_issuer.contract_id
2105
+ FROM ocf_deployments od_issuer
2106
+ JOIN latest_ocf_objects loo_issuer ON od_issuer.ocf_object_id = loo_issuer.id
2107
+ WHERE od_issuer.party_id = cp.party_id
2108
+ AND od_issuer.status = 'deployed'
2109
+ AND loo_issuer.type = 'ISSUER'
2110
+ ORDER BY od_issuer.created_at DESC
2111
+ LIMIT 1
2112
+ ) as issuer_contract_id
2113
+ FROM canton_parties cp
2114
+ LEFT JOIN portal p ON cp.portal_id = p.id
2115
+ JOIN latest_ocf_objects loo ON cp.portal_id = loo.portal_id
2116
+ JOIN ocf_deployments od ON loo.id = od.ocf_object_id AND cp.party_id = od.party_id
2117
+ WHERE
2118
+ loo.type = 'STOCK_PLAN'
2119
+ AND EXISTS (
2120
+ SELECT 1 FROM ocf_deployments od_issuer
2121
+ JOIN latest_ocf_objects loo_issuer ON od_issuer.ocf_object_id = loo_issuer.id
2122
+ WHERE od_issuer.party_id = cp.party_id
2123
+ AND od_issuer.status = 'deployed'
2124
+ AND loo_issuer.type = 'ISSUER'
2125
+ )
2126
+ AND od.version < loo.version
2127
+ AND od.status = 'deployed'
2128
+ ORDER BY version_diff DESC, cp.created_at ASC
2129
+ `;
2130
+ const result = await this.pool.query(query);
2131
+ return result.rows;
2132
+ }
2133
+ async getPortalsNeedingStockLegendTemplateDeployments() {
2134
+ const query = `
2135
+ SELECT DISTINCT
2136
+ cp.portal_id,
2137
+ cp.party_id,
2138
+ cp.provider,
2139
+ cp.created_at,
2140
+ p.company,
2141
+ loo.ocf_data,
2142
+ loo.id AS ocf_object_id,
2143
+ loo.version AS ocf_version,
2144
+ (
2145
+ SELECT od_issuer.contract_id
2146
+ FROM ocf_deployments od_issuer
2147
+ JOIN latest_ocf_objects loo_issuer ON od_issuer.ocf_object_id = loo_issuer.id
2148
+ WHERE od_issuer.party_id = cp.party_id
2149
+ AND od_issuer.status = 'deployed'
2150
+ AND loo_issuer.type = 'ISSUER'
2151
+ ORDER BY od_issuer.created_at DESC
2152
+ LIMIT 1
2153
+ ) as issuer_contract_id
2154
+ FROM canton_parties cp
2155
+ LEFT JOIN portal p ON cp.portal_id = p.id
2156
+ JOIN latest_ocf_objects loo ON cp.portal_id = loo.portal_id
2157
+ WHERE
2158
+ (loo.type = 'OBJECT' AND loo.subtype = 'STOCK_LEGEND_TEMPLATE')
2159
+ AND EXISTS (
2160
+ SELECT 1 FROM ocf_deployments od_issuer
2161
+ JOIN latest_ocf_objects loo_issuer ON od_issuer.ocf_object_id = loo_issuer.id
2162
+ WHERE od_issuer.party_id = cp.party_id
2163
+ AND od_issuer.status = 'deployed'
2164
+ AND loo_issuer.type = 'ISSUER'
2165
+ )
2166
+ AND NOT EXISTS (
2167
+ SELECT 1 FROM ocf_deployments od
2168
+ WHERE od.ocf_object_id = loo.id
2169
+ AND od.party_id = cp.party_id
2170
+ AND od.status = 'deployed'
2171
+ )
2172
+ ORDER BY cp.created_at ASC
2173
+ `;
2174
+ const result = await this.pool.query(query);
2175
+ return result.rows;
2176
+ }
2177
+ async getPortalsNeedingDocumentDeployments() {
2178
+ const query = `
2179
+ SELECT DISTINCT
2180
+ cp.portal_id,
2181
+ cp.party_id,
2182
+ cp.provider,
2183
+ cp.created_at,
2184
+ p.company,
2185
+ loo.ocf_data,
2186
+ loo.id AS ocf_object_id,
2187
+ loo.version AS ocf_version,
2188
+ (
2189
+ SELECT od_issuer.contract_id
2190
+ FROM ocf_deployments od_issuer
2191
+ JOIN latest_ocf_objects loo_issuer ON od_issuer.ocf_object_id = loo_issuer.id
2192
+ WHERE od_issuer.party_id = cp.party_id
2193
+ AND od_issuer.status = 'deployed'
2194
+ AND loo_issuer.type = 'ISSUER'
2195
+ ORDER BY od_issuer.created_at DESC
2196
+ LIMIT 1
2197
+ ) as issuer_contract_id
2198
+ FROM canton_parties cp
2199
+ LEFT JOIN portal p ON cp.portal_id = p.id
2200
+ JOIN latest_ocf_objects loo ON cp.portal_id = loo.portal_id
2201
+ WHERE
2202
+ (loo.type = 'OBJECT' AND loo.subtype = 'DOCUMENT')
2203
+ AND EXISTS (
2204
+ SELECT 1 FROM ocf_deployments od_issuer
2205
+ JOIN latest_ocf_objects loo_issuer ON od_issuer.ocf_object_id = loo_issuer.id
2206
+ WHERE od_issuer.party_id = cp.party_id
2207
+ AND od_issuer.status = 'deployed'
2208
+ AND loo_issuer.type = 'ISSUER'
2209
+ )
2210
+ AND NOT EXISTS (
2211
+ SELECT 1 FROM ocf_deployments od
2212
+ WHERE od.ocf_object_id = loo.id
2213
+ AND od.party_id = cp.party_id
2214
+ AND od.status = 'deployed'
2215
+ )
2216
+ ORDER BY cp.created_at ASC
2217
+ `;
2218
+ const result = await this.pool.query(query);
2219
+ return result.rows;
2220
+ }
2221
+ async getPortalsNeedingVestingTermsDeployments() {
2222
+ const query = `
2223
+ SELECT DISTINCT
2224
+ cp.portal_id,
2225
+ cp.party_id,
2226
+ cp.provider,
2227
+ cp.created_at,
2228
+ p.company,
2229
+ loo.ocf_data,
2230
+ loo.id AS ocf_object_id,
2231
+ loo.version AS ocf_version,
2232
+ (
2233
+ SELECT od_issuer.contract_id
2234
+ FROM ocf_deployments od_issuer
2235
+ JOIN latest_ocf_objects loo_issuer ON od_issuer.ocf_object_id = loo_issuer.id
2236
+ WHERE od_issuer.party_id = cp.party_id
2237
+ AND od_issuer.status = 'deployed'
2238
+ AND loo_issuer.type = 'ISSUER'
2239
+ ORDER BY od_issuer.created_at DESC
2240
+ LIMIT 1
2241
+ ) as issuer_contract_id
2242
+ FROM canton_parties cp
2243
+ LEFT JOIN portal p ON cp.portal_id = p.id
2244
+ JOIN latest_ocf_objects loo ON cp.portal_id = loo.portal_id
2245
+ WHERE
2246
+ (loo.type = 'OBJECT' AND loo.subtype = 'VESTING_TERMS')
2247
+ AND EXISTS (
2248
+ SELECT 1 FROM ocf_deployments od_issuer
2249
+ JOIN latest_ocf_objects loo_issuer ON od_issuer.ocf_object_id = loo_issuer.id
2250
+ WHERE od_issuer.party_id = cp.party_id
2251
+ AND od_issuer.status = 'deployed'
2252
+ AND loo_issuer.type = 'ISSUER'
2253
+ )
2254
+ AND NOT EXISTS (
2255
+ SELECT 1 FROM ocf_deployments od
2256
+ WHERE od.ocf_object_id = loo.id
2257
+ AND od.party_id = cp.party_id
2258
+ AND od.status = 'deployed'
2259
+ )
2260
+ ORDER BY cp.created_at ASC
2261
+ `;
2262
+ const result = await this.pool.query(query);
2263
+ return result.rows;
2264
+ }
2265
+ async getPortalsNeedingStockIssuanceDeployments() {
2266
+ const query = `
2267
+ SELECT DISTINCT
2268
+ cp.portal_id,
2269
+ cp.party_id,
2270
+ cp.provider,
2271
+ cp.created_at,
2272
+ p.company,
2273
+ loo.ocf_data,
2274
+ loo.id AS ocf_object_id,
2275
+ loo.version AS ocf_version,
2276
+ (
2277
+ SELECT od_issuer.contract_id
2278
+ FROM ocf_deployments od_issuer
2279
+ JOIN latest_ocf_objects loo_issuer ON od_issuer.ocf_object_id = loo_issuer.id
2280
+ WHERE od_issuer.party_id = cp.party_id
2281
+ AND od_issuer.status = 'deployed'
2282
+ AND loo_issuer.type = 'ISSUER'
2283
+ ORDER BY od_issuer.created_at DESC
2284
+ LIMIT 1
2285
+ ) as issuer_contract_id
2286
+ FROM canton_parties cp
2287
+ LEFT JOIN portal p ON cp.portal_id = p.id
2288
+ JOIN latest_ocf_objects loo ON cp.portal_id = loo.portal_id
2289
+ WHERE
2290
+ (loo.type = 'TRANSACTION' AND loo.subtype = 'TX_STOCK_ISSUANCE')
2291
+ AND EXISTS (
2292
+ SELECT 1 FROM ocf_deployments od_issuer
2293
+ JOIN latest_ocf_objects loo_issuer ON od_issuer.ocf_object_id = loo_issuer.id
2294
+ WHERE od_issuer.party_id = cp.party_id
2295
+ AND od_issuer.status = 'deployed'
2296
+ AND loo_issuer.type = 'ISSUER'
2297
+ )
2298
+ AND NOT EXISTS (
2299
+ SELECT 1 FROM ocf_deployments od
2300
+ WHERE od.ocf_object_id = loo.id
2301
+ AND od.party_id = cp.party_id
2302
+ AND od.status = 'deployed'
2303
+ )
2304
+ ORDER BY cp.created_at ASC
2305
+ `;
2306
+ const result = await this.pool.query(query);
2307
+ return result.rows;
2308
+ }
2309
+ async getPortalsNeedingWarrantIssuanceDeployments() {
2310
+ const query = `
2311
+ SELECT DISTINCT
2312
+ cp.portal_id,
2313
+ cp.party_id,
2314
+ cp.provider,
2315
+ cp.created_at,
2316
+ p.company,
2317
+ loo.ocf_data,
2318
+ loo.id AS ocf_object_id,
2319
+ loo.version AS ocf_version,
2320
+ (
2321
+ SELECT od_issuer.contract_id
2322
+ FROM ocf_deployments od_issuer
2323
+ JOIN latest_ocf_objects loo_issuer ON od_issuer.ocf_object_id = loo_issuer.id
2324
+ WHERE od_issuer.party_id = cp.party_id
2325
+ AND od_issuer.status = 'deployed'
2326
+ AND loo_issuer.type = 'ISSUER'
2327
+ ORDER BY od_issuer.created_at DESC
2328
+ LIMIT 1
2329
+ ) as issuer_contract_id
2330
+ FROM canton_parties cp
2331
+ LEFT JOIN portal p ON cp.portal_id = p.id
2332
+ JOIN latest_ocf_objects loo ON cp.portal_id = loo.portal_id
2333
+ WHERE
2334
+ (loo.type = 'TRANSACTION' AND loo.subtype = 'TX_WARRANT_ISSUANCE')
2335
+ AND EXISTS (
2336
+ SELECT 1 FROM ocf_deployments od_issuer
2337
+ JOIN latest_ocf_objects loo_issuer ON od_issuer.ocf_object_id = loo_issuer.id
2338
+ WHERE od_issuer.party_id = cp.party_id
2339
+ AND od_issuer.status = 'deployed'
2340
+ AND loo_issuer.type = 'ISSUER'
2341
+ )
2342
+ AND NOT EXISTS (
2343
+ SELECT 1 FROM ocf_deployments od
2344
+ WHERE od.ocf_object_id = loo.id
2345
+ AND od.party_id = cp.party_id
2346
+ AND od.status = 'deployed'
2347
+ )
2348
+ ORDER BY cp.created_at ASC
2349
+ `;
2350
+ const result = await this.pool.query(query);
2351
+ return result.rows;
2352
+ }
2353
+ async getPortalsNeedingStockPlanPoolAdjustmentDeployments() {
2354
+ const query = `
2355
+ SELECT DISTINCT
2356
+ cp.portal_id,
2357
+ cp.party_id,
2358
+ cp.provider,
2359
+ cp.created_at,
2360
+ p.company,
2361
+ loo.ocf_data,
2362
+ loo.id AS ocf_object_id,
2363
+ loo.version AS ocf_version,
2364
+ (
2365
+ SELECT od_issuer.contract_id
2366
+ FROM ocf_deployments od_issuer
2367
+ JOIN latest_ocf_objects loo_issuer ON od_issuer.ocf_object_id = loo_issuer.id
2368
+ WHERE od_issuer.party_id = cp.party_id
2369
+ AND od_issuer.status = 'deployed'
2370
+ AND loo_issuer.type = 'ISSUER'
2371
+ ORDER BY od_issuer.created_at DESC
2372
+ LIMIT 1
2373
+ ) as issuer_contract_id
2374
+ FROM canton_parties cp
2375
+ LEFT JOIN portal p ON cp.portal_id = p.id
2376
+ JOIN latest_ocf_objects loo ON cp.portal_id = loo.portal_id
2377
+ WHERE
2378
+ (loo.type = 'TRANSACTION' AND loo.subtype = 'TX_STOCK_PLAN_POOL_ADJUSTMENT')
2379
+ AND EXISTS (
2380
+ SELECT 1 FROM ocf_deployments od_issuer
2381
+ JOIN latest_ocf_objects loo_issuer ON od_issuer.ocf_object_id = loo_issuer.id
2382
+ WHERE od_issuer.party_id = cp.party_id
2383
+ AND od_issuer.status = 'deployed'
2384
+ AND loo_issuer.type = 'ISSUER'
2385
+ )
2386
+ AND NOT EXISTS (
2387
+ SELECT 1 FROM ocf_deployments od
2388
+ WHERE od.ocf_object_id = loo.id
2389
+ AND od.party_id = cp.party_id
2390
+ AND od.status = 'deployed'
2391
+ )
2392
+ ORDER BY cp.created_at ASC
2393
+ `;
2394
+ const result = await this.pool.query(query);
2395
+ return result.rows;
2396
+ }
2397
+ async getPortalsNeedingStockClassAuthorizedSharesAdjustmentDeployments() {
2398
+ const query = `
2399
+ SELECT DISTINCT
2400
+ cp.portal_id,
2401
+ cp.party_id,
2402
+ cp.provider,
2403
+ cp.created_at,
2404
+ p.company,
2405
+ loo.ocf_data,
2406
+ loo.id AS ocf_object_id,
2407
+ loo.version AS ocf_version,
2408
+ (
2409
+ SELECT od_issuer.contract_id
2410
+ FROM ocf_deployments od_issuer
2411
+ JOIN latest_ocf_objects loo_issuer ON od_issuer.ocf_object_id = loo_issuer.id
2412
+ WHERE od_issuer.party_id = cp.party_id
2413
+ AND od_issuer.status = 'deployed'
2414
+ AND loo_issuer.type = 'ISSUER'
2415
+ ORDER BY od_issuer.created_at DESC
2416
+ LIMIT 1
2417
+ ) as issuer_contract_id
2418
+ FROM canton_parties cp
2419
+ LEFT JOIN portal p ON cp.portal_id = p.id
2420
+ JOIN latest_ocf_objects loo ON cp.portal_id = loo.portal_id
2421
+ WHERE
2422
+ (loo.type = 'TRANSACTION' AND loo.subtype = 'TX_STOCK_CLASS_AUTHORIZED_SHARES_ADJUSTMENT')
2423
+ AND EXISTS (
2424
+ SELECT 1 FROM ocf_deployments od_issuer
2425
+ JOIN latest_ocf_objects loo_issuer ON od_issuer.ocf_object_id = loo_issuer.id
2426
+ WHERE od_issuer.party_id = cp.party_id
2427
+ AND od_issuer.status = 'deployed'
2428
+ AND loo_issuer.type = 'ISSUER'
2429
+ )
2430
+ AND NOT EXISTS (
2431
+ SELECT 1 FROM ocf_deployments od
2432
+ WHERE od.ocf_object_id = loo.id
2433
+ AND od.party_id = cp.party_id
2434
+ AND od.status = 'deployed'
2435
+ )
2436
+ ORDER BY cp.created_at ASC
2437
+ `;
2438
+ const result = await this.pool.query(query);
2439
+ return result.rows;
2440
+ }
2441
+ async getPortalsNeedingStockCancellationDeployments() {
2442
+ const query = `
2443
+ SELECT DISTINCT
2444
+ cp.portal_id,
2445
+ cp.party_id,
2446
+ cp.provider,
2447
+ cp.created_at,
2448
+ p.company,
2449
+ loo.ocf_data,
2450
+ loo.id AS ocf_object_id,
2451
+ loo.version AS ocf_version,
2452
+ (
2453
+ SELECT od_issuer.contract_id
2454
+ FROM ocf_deployments od_issuer
2455
+ JOIN latest_ocf_objects loo_issuer ON od_issuer.ocf_object_id = loo_issuer.id
2456
+ WHERE od_issuer.party_id = cp.party_id
2457
+ AND od_issuer.status = 'deployed'
2458
+ AND loo_issuer.type = 'ISSUER'
2459
+ ORDER BY od_issuer.created_at DESC
2460
+ LIMIT 1
2461
+ ) as issuer_contract_id
2462
+ FROM canton_parties cp
2463
+ LEFT JOIN portal p ON cp.portal_id = p.id
2464
+ JOIN latest_ocf_objects loo ON cp.portal_id = loo.portal_id
2465
+ WHERE
2466
+ (loo.type = 'TRANSACTION' AND loo.subtype = 'TX_STOCK_CANCELLATION')
2467
+ AND EXISTS (
2468
+ SELECT 1 FROM ocf_deployments od_issuer
2469
+ JOIN latest_ocf_objects loo_issuer ON od_issuer.ocf_object_id = loo_issuer.id
2470
+ WHERE od_issuer.party_id = cp.party_id
2471
+ AND od_issuer.status = 'deployed'
2472
+ AND loo_issuer.type = 'ISSUER'
2473
+ )
2474
+ AND NOT EXISTS (
2475
+ SELECT 1 FROM ocf_deployments od
2476
+ WHERE od.ocf_object_id = loo.id
2477
+ AND od.party_id = cp.party_id
2478
+ AND od.status = 'deployed'
2479
+ )
2480
+ ORDER BY cp.created_at ASC
2481
+ `;
2482
+ const result = await this.pool.query(query);
2483
+ return result.rows;
2484
+ }
2485
+ async getPortalsNeedingIssuerAuthorizedSharesAdjustmentDeployments() {
2486
+ const query = `
2487
+ SELECT DISTINCT
2488
+ cp.portal_id,
2489
+ cp.party_id,
2490
+ cp.provider,
2491
+ cp.created_at,
2492
+ p.company,
2493
+ loo.ocf_data,
2494
+ loo.id AS ocf_object_id,
2495
+ loo.version AS ocf_version,
2496
+ (
2497
+ SELECT od_issuer.contract_id
2498
+ FROM ocf_deployments od_issuer
2499
+ JOIN latest_ocf_objects loo_issuer ON od_issuer.ocf_object_id = loo_issuer.id
2500
+ WHERE od_issuer.party_id = cp.party_id
2501
+ AND od_issuer.status = 'deployed'
2502
+ AND loo_issuer.type = 'ISSUER'
2503
+ ORDER BY od_issuer.created_at DESC
2504
+ LIMIT 1
2505
+ ) as issuer_contract_id
2506
+ FROM canton_parties cp
2507
+ LEFT JOIN portal p ON cp.portal_id = p.id
2508
+ JOIN latest_ocf_objects loo ON cp.portal_id = loo.portal_id
2509
+ WHERE
2510
+ (loo.type = 'TRANSACTION' AND loo.subtype = 'TX_ISSUER_AUTHORIZED_SHARES_ADJUSTMENT')
2511
+ AND EXISTS (
2512
+ SELECT 1 FROM ocf_deployments od_issuer
2513
+ JOIN latest_ocf_objects loo_issuer ON od_issuer.ocf_object_id = loo_issuer.id
2514
+ WHERE od_issuer.party_id = cp.party_id
2515
+ AND od_issuer.status = 'deployed'
2516
+ AND loo_issuer.type = 'ISSUER'
2517
+ )
2518
+ AND NOT EXISTS (
2519
+ SELECT 1 FROM ocf_deployments od
2520
+ WHERE od.ocf_object_id = loo.id
2521
+ AND od.party_id = cp.party_id
2522
+ AND od.status = 'deployed'
2523
+ )
2524
+ ORDER BY cp.created_at ASC
2525
+ `;
2526
+ const result = await this.pool.query(query);
2527
+ return result.rows;
2528
+ }
2529
+ async getPortalsNeedingEquityCompensationIssuanceDeployments() {
2530
+ const query = `
2531
+ SELECT DISTINCT
2532
+ cp.portal_id,
2533
+ cp.party_id,
2534
+ cp.provider,
2535
+ cp.created_at,
2536
+ p.company,
2537
+ loo.ocf_data,
2538
+ loo.id AS ocf_object_id,
2539
+ loo.version AS ocf_version,
2540
+ (
2541
+ SELECT od_issuer.contract_id
2542
+ FROM ocf_deployments od_issuer
2543
+ JOIN latest_ocf_objects loo_issuer ON od_issuer.ocf_object_id = loo_issuer.id
2544
+ WHERE od_issuer.party_id = cp.party_id
2545
+ AND od_issuer.status = 'deployed'
2546
+ AND loo_issuer.type = 'ISSUER'
2547
+ ORDER BY od_issuer.created_at DESC
2548
+ LIMIT 1
2549
+ ) as issuer_contract_id
2550
+ FROM canton_parties cp
2551
+ LEFT JOIN portal p ON cp.portal_id = p.id
2552
+ JOIN latest_ocf_objects loo ON cp.portal_id = loo.portal_id
2553
+ WHERE
2554
+ (loo.type = 'TRANSACTION' AND loo.subtype = 'TX_EQUITY_COMPENSATION_ISSUANCE')
2555
+ AND EXISTS (
2556
+ SELECT 1 FROM ocf_deployments od_issuer
2557
+ JOIN latest_ocf_objects loo_issuer ON od_issuer.ocf_object_id = loo_issuer.id
2558
+ WHERE od_issuer.party_id = cp.party_id
2559
+ AND od_issuer.status = 'deployed'
2560
+ AND loo_issuer.type = 'ISSUER'
2561
+ )
2562
+ AND NOT EXISTS (
2563
+ SELECT 1 FROM ocf_deployments od
2564
+ WHERE od.ocf_object_id = loo.id
2565
+ AND od.party_id = cp.party_id
2566
+ AND od.status = 'deployed'
2567
+ )
2568
+ ORDER BY cp.created_at ASC
2569
+ `;
2570
+ const result = await this.pool.query(query);
2571
+ return result.rows;
2572
+ }
2573
+ async getPortalsNeedingEquityCompensationExerciseDeployments() {
2574
+ const query = `
2575
+ SELECT DISTINCT
2576
+ cp.portal_id,
2577
+ cp.party_id,
2578
+ cp.provider,
2579
+ cp.created_at,
2580
+ p.company,
2581
+ loo.ocf_data,
2582
+ loo.id AS ocf_object_id,
2583
+ loo.version AS ocf_version,
2584
+ (
2585
+ SELECT od_issuer.contract_id
2586
+ FROM ocf_deployments od_issuer
2587
+ JOIN latest_ocf_objects loo_issuer ON od_issuer.ocf_object_id = loo_issuer.id
2588
+ WHERE od_issuer.party_id = cp.party_id
2589
+ AND od_issuer.status = 'deployed'
2590
+ AND loo_issuer.type = 'ISSUER'
2591
+ ORDER BY od_issuer.created_at DESC
2592
+ LIMIT 1
2593
+ ) as issuer_contract_id
2594
+ FROM canton_parties cp
2595
+ LEFT JOIN portal p ON cp.portal_id = p.id
2596
+ JOIN latest_ocf_objects loo ON cp.portal_id = loo.portal_id
2597
+ WHERE
2598
+ (loo.type = 'TRANSACTION' AND loo.subtype = 'TX_EQUITY_COMPENSATION_EXERCISE')
2599
+ AND EXISTS (
2600
+ SELECT 1 FROM ocf_deployments od_issuer
2601
+ JOIN latest_ocf_objects loo_issuer ON od_issuer.ocf_object_id = loo_issuer.id
2602
+ WHERE od_issuer.party_id = cp.party_id
2603
+ AND od_issuer.status = 'deployed'
2604
+ AND loo_issuer.type = 'ISSUER'
2605
+ )
2606
+ AND NOT EXISTS (
2607
+ SELECT 1 FROM ocf_deployments od
2608
+ WHERE od.ocf_object_id = loo.id
2609
+ AND od.party_id = cp.party_id
2610
+ AND od.status = 'deployed'
2611
+ )
2612
+ ORDER BY cp.created_at ASC
2613
+ `;
2614
+ const result = await this.pool.query(query);
2615
+ return result.rows;
2616
+ }
2617
+ async getPortalsNeedingConvertibleIssuanceDeployments() {
2618
+ const query = `
2619
+ SELECT DISTINCT
2620
+ cp.portal_id,
2621
+ cp.party_id,
2622
+ cp.provider,
2623
+ cp.created_at,
2624
+ p.company,
2625
+ loo.ocf_data,
2626
+ loo.id AS ocf_object_id,
2627
+ loo.version AS ocf_version,
2628
+ (
2629
+ SELECT od_issuer.contract_id
2630
+ FROM ocf_deployments od_issuer
2631
+ JOIN latest_ocf_objects loo_issuer ON od_issuer.ocf_object_id = loo_issuer.id
2632
+ WHERE od_issuer.party_id = cp.party_id
2633
+ AND od_issuer.status = 'deployed'
2634
+ AND loo_issuer.type = 'ISSUER'
2635
+ ORDER BY od_issuer.created_at DESC
2636
+ LIMIT 1
2637
+ ) as issuer_contract_id
2638
+ FROM canton_parties cp
2639
+ LEFT JOIN portal p ON cp.portal_id = p.id
2640
+ JOIN latest_ocf_objects loo ON cp.portal_id = loo.portal_id
2641
+ WHERE
2642
+ (loo.type = 'TRANSACTION' AND loo.subtype = 'TX_CONVERTIBLE_ISSUANCE')
2643
+ AND EXISTS (
2644
+ SELECT 1 FROM ocf_deployments od_issuer
2645
+ JOIN latest_ocf_objects loo_issuer ON od_issuer.ocf_object_id = loo_issuer.id
2646
+ WHERE od_issuer.party_id = cp.party_id
2647
+ AND od_issuer.status = 'deployed'
2648
+ AND loo_issuer.type = 'ISSUER'
2649
+ )
2650
+ AND NOT EXISTS (
2651
+ SELECT 1 FROM ocf_deployments od
2652
+ WHERE od.ocf_object_id = loo.id
2653
+ AND od.party_id = cp.party_id
2654
+ AND od.status = 'deployed'
2655
+ )
2656
+ ORDER BY cp.created_at ASC
2657
+ `;
2658
+ const result = await this.pool.query(query);
2659
+ return result.rows;
2660
+ }
2661
+ /**
2662
+ * Returns the next party_id that has any pending OCF work (missing deployments or updates), ordered by the associated
2663
+ * party creation time.
2664
+ */
2665
+ async getNextPartyIdWithPendingOcfWork() {
2666
+ const query = `
2667
+ WITH eligible_party AS (
2668
+ SELECT DISTINCT cp.party_id, cp.created_at
2669
+ FROM canton_parties cp
2670
+ JOIN latest_ocf_objects loo ON cp.portal_id = loo.portal_id
2671
+ WHERE
2672
+ -- Issuer must be deployed for this party
2673
+ EXISTS (
2674
+ SELECT 1 FROM ocf_deployments od_issuer
2675
+ JOIN latest_ocf_objects loo_issuer ON od_issuer.ocf_object_id = loo_issuer.id
2676
+ WHERE od_issuer.party_id = cp.party_id
2677
+ AND od_issuer.status = 'deployed'
2678
+ AND loo_issuer.type = 'ISSUER'
2679
+ )
2680
+ AND (
2681
+ -- Missing for any object/transaction/record type (excluding ISSUER)
2682
+ (loo.type <> 'ISSUER' AND NOT EXISTS (
2683
+ SELECT 1 FROM ocf_deployments od
2684
+ WHERE od.ocf_object_id = loo.id
2685
+ AND od.party_id = cp.party_id
2686
+ AND od.status = 'deployed'
2687
+ ))
2688
+ OR
2689
+ -- Updates for any type/subtype except ISSUER
2690
+ (
2691
+ loo.type <> 'ISSUER'
2692
+ AND EXISTS (
2693
+ SELECT 1 FROM ocf_deployments od2
2694
+ WHERE od2.ocf_object_id = loo.id
2695
+ AND od2.party_id = cp.party_id
2696
+ AND od2.status = 'deployed'
2697
+ AND od2.version < loo.version
2698
+ )
2699
+ )
2700
+ )
2701
+ )
2702
+ SELECT party_id
2703
+ FROM eligible_party
2704
+ ORDER BY created_at ASC
2705
+ LIMIT 1
2706
+ `;
2707
+ const result = await this.pool.query(query);
2708
+ if (result.rows.length === 0)
2709
+ return null;
2710
+ return result.rows[0].party_id;
2711
+ }
2712
+ /**
2713
+ * Returns all pending OCF items (missing or updates) for a given party in a single query. This unifies
2714
+ * objects/transactions and core records, and includes metadata needed by the script.
2715
+ */
2716
+ async getPendingOcfItemsForParty(partyId) {
2717
+ const query = `
2718
+ (
2719
+ -- Missing deployments across all OCF types/subtypes
2720
+ SELECT DISTINCT
2721
+ cp.portal_id,
2722
+ cp.party_id,
2723
+ cp.provider,
2724
+ p.company,
2725
+ loo.id AS ocf_object_id,
2726
+ loo.ocf_data,
2727
+ loo.version AS ocf_version,
2728
+ NULL::int AS latest_ocf_version,
2729
+ NULL::int AS deployed_version,
2730
+ NULL::text AS current_contract_id,
2731
+ loo.type AS ocf_type,
2732
+ loo.subtype AS ocf_subtype,
2733
+ (
2734
+ SELECT od_issuer.contract_id
2735
+ FROM ocf_deployments od_issuer
2736
+ JOIN latest_ocf_objects loo_issuer ON od_issuer.ocf_object_id = loo_issuer.id
2737
+ WHERE od_issuer.party_id = cp.party_id
2738
+ AND od_issuer.status = 'deployed'
2739
+ AND loo_issuer.type = 'ISSUER'
2740
+ ORDER BY od_issuer.created_at DESC
2741
+ LIMIT 1
2742
+ ) AS issuer_contract_id,
2743
+ cp.created_at
2744
+ FROM canton_parties cp
2745
+ LEFT JOIN portal p ON cp.portal_id = p.id
2746
+ JOIN latest_ocf_objects loo ON cp.portal_id = loo.portal_id
2747
+ WHERE cp.party_id = $1
2748
+ AND EXISTS (
2749
+ SELECT 1 FROM ocf_deployments od_issuer
2750
+ JOIN latest_ocf_objects loo_issuer ON od_issuer.ocf_object_id = loo_issuer.id
2751
+ WHERE od_issuer.party_id = cp.party_id
2752
+ AND od_issuer.status = 'deployed'
2753
+ AND loo_issuer.type = 'ISSUER'
2754
+ )
2755
+ AND NOT EXISTS (
2756
+ SELECT 1 FROM ocf_deployments od
2757
+ WHERE od.ocf_object_id = loo.id
2758
+ AND od.party_id = cp.party_id
2759
+ AND od.status = 'deployed'
2760
+ )
2761
+ )
2762
+ UNION ALL
2763
+ (
2764
+ -- Updates for core record types
2765
+ SELECT DISTINCT
2766
+ cp.portal_id,
2767
+ cp.party_id,
2768
+ cp.provider,
2769
+ p.company,
2770
+ loo.id AS ocf_object_id,
2771
+ loo.ocf_data,
2772
+ NULL::int AS ocf_version,
2773
+ loo.version AS latest_ocf_version,
2774
+ od.version AS deployed_version,
2775
+ od.contract_id AS current_contract_id,
2776
+ loo.type AS ocf_type,
2777
+ loo.subtype AS ocf_subtype,
2778
+ (
2779
+ SELECT od_issuer.contract_id
2780
+ FROM ocf_deployments od_issuer
2781
+ JOIN latest_ocf_objects loo_issuer ON od_issuer.ocf_object_id = loo_issuer.id
2782
+ WHERE od_issuer.party_id = cp.party_id
2783
+ AND od_issuer.status = 'deployed'
2784
+ AND loo_issuer.type = 'ISSUER'
2785
+ ORDER BY od_issuer.created_at DESC
2786
+ LIMIT 1
2787
+ ) AS issuer_contract_id,
2788
+ cp.created_at
2789
+ FROM canton_parties cp
2790
+ LEFT JOIN portal p ON cp.portal_id = p.id
2791
+ JOIN latest_ocf_objects loo ON cp.portal_id = loo.portal_id
2792
+ JOIN ocf_deployments od ON loo.id = od.ocf_object_id AND cp.party_id = od.party_id
2793
+ WHERE cp.party_id = $1
2794
+ AND loo.type <> 'ISSUER'
2795
+ AND od.status = 'deployed'
2796
+ AND od.version < loo.version
2797
+ )
2798
+ ORDER BY created_at ASC
2799
+ `;
2800
+ const result = await this.pool.query(query, [partyId]);
2801
+ return result.rows;
2802
+ }
2803
+ async getLatestValuationReportByPortalId(portalId) {
2804
+ const query = `
2805
+ SELECT *
2806
+ FROM canton_valuation_reports
2807
+ WHERE portal_id = $1
2808
+ ORDER BY version DESC
2809
+ LIMIT 1
2810
+ `;
2811
+ const result = await this.pool.query(query, [portalId]);
2812
+ if (result.rows.length === 0)
2813
+ return null;
2814
+ return this.mapValuationReportFromDb(result.rows[0]);
2815
+ }
2816
+ /** Get the latest valuation report row for each distinct company_id */
2817
+ async getLatestValuationReportsGroupedByCompany() {
2818
+ const query = `
2819
+ SELECT DISTINCT ON (company_id)
2820
+ id, portal_id, company_id, version, contract_id, company_valuation,
2821
+ tx_update_id, last_valuation_until, created_at, updated_at
2822
+ FROM canton_valuation_reports
2823
+ ORDER BY company_id, version DESC
2824
+ `;
2825
+ const result = await this.pool.query(query);
2826
+ return result.rows.map(row => this.mapValuationReportFromDb(row));
2827
+ }
2828
+ async insertValuationReport(report) {
2829
+ const query = `
2830
+ INSERT INTO canton_valuation_reports (
2831
+ portal_id, company_id, version, contract_id, company_valuation, tx_update_id, last_valuation_until
2832
+ ) VALUES ($1, $2, $3, $4, $5, $6, $7)
2833
+ RETURNING *
2834
+ `;
2835
+ const values = [
2836
+ report.portal_id,
2837
+ report.company_id,
2838
+ report.version,
2839
+ report.contract_id,
2840
+ report.company_valuation,
2841
+ report.tx_update_id,
2842
+ report.last_valuation_until,
2843
+ ];
2844
+ const result = await this.pool.query(query, values);
2845
+ return this.mapValuationReportFromDb(result.rows[0]);
2846
+ }
2847
+ // Helper methods for mapping database results
2848
+ mapTransferFromDb(row) {
2849
+ return {
2850
+ id: row.id,
2851
+ api_tracking_id: row.api_tracking_id,
2852
+ api_expires_at: row.api_expires_at,
2853
+ transfer_sender_party_id: row.transfer_sender_party_id,
2854
+ transfer_receiver_party_id: row.transfer_receiver_party_id,
2855
+ transfer_amount: parseFloat(row.transfer_amount),
2856
+ transfer_description: row.transfer_description,
2857
+ transfer_burned_amount: parseFloat(row.transfer_burned_amount),
2858
+ receiver_holding_cids: row.receiver_holding_cids,
2859
+ sender_change_cids: row.sender_change_cids,
2860
+ tx_update_id: row.tx_update_id,
2861
+ tx_record_time: row.tx_record_time,
2862
+ status: row.status,
2863
+ app_reward_coupon_id: row.app_reward_coupon_id,
2864
+ created_at: row.created_at,
2865
+ updated_at: row.updated_at,
2866
+ };
2867
+ }
2868
+ mapRewardCouponFromDb(row) {
2869
+ return {
2870
+ id: row.id,
2871
+ status: row.status,
2872
+ tx_update_id: row.tx_update_id,
2873
+ tx_record_time: row.tx_record_time,
2874
+ contract_id: row.contract_id,
2875
+ template_id: row.template_id,
2876
+ package_name: row.package_name,
2877
+ dso_party_id: row.dso_party_id,
2878
+ provider_party_id: row.provider_party_id,
2879
+ featured: row.featured,
2880
+ round_number: row.round_number,
2881
+ beneficiary_party_id: row.beneficiary_party_id,
2882
+ coupon_amount: parseFloat(row.coupon_amount),
2883
+ tx_archive_update_id: row.tx_archive_update_id,
2884
+ tx_archive_record_time: row.tx_archive_record_time,
2885
+ app_reward_amount: row.app_reward_amount
2886
+ ? parseFloat(row.app_reward_amount)
2887
+ : null,
2888
+ created_at: row.created_at,
2889
+ updated_at: row.updated_at,
2890
+ };
2891
+ }
2892
+ mapAppMarkerFromDb(row) {
2893
+ return {
2894
+ id: row.id,
2895
+ status: row.status,
2896
+ contract_id: row.contract_id,
2897
+ provider_party_id: row.provider_party_id,
2898
+ beneficiary_party_id: row.beneficiary_party_id,
2899
+ tx_record_time: row.tx_record_time,
2900
+ weight: parseFloat(row.weight),
2901
+ created_at: row.created_at,
2902
+ updated_at: row.updated_at,
2903
+ };
2904
+ }
2905
+ mapRedemptionFromDb(row) {
2906
+ return {
2907
+ id: row.id,
2908
+ transfer_id: row.transfer_id,
2909
+ app_reward_coupon_ids: row.app_reward_coupon_ids,
2910
+ tx_update_id: row.tx_update_id,
2911
+ tx_record_time: row.tx_record_time,
2912
+ tx_synchronizer_id: row.tx_synchronizer_id,
2913
+ tx_effective_at: row.tx_effective_at,
2914
+ total_app_rewards: parseFloat(row.total_app_rewards),
2915
+ created_at: row.created_at,
2916
+ updated_at: row.updated_at,
2917
+ };
2918
+ }
2919
+ mapRewardCouponWithTransferFromDb(row) {
2920
+ const coupon = {
2921
+ id: row.coupon_id,
2922
+ status: row.coupon_status,
2923
+ tx_update_id: row.coupon_tx_update_id,
2924
+ tx_record_time: row.coupon_tx_record_time,
2925
+ contract_id: row.coupon_contract_id,
2926
+ template_id: row.coupon_template_id,
2927
+ package_name: row.coupon_package_name,
2928
+ dso_party_id: row.coupon_dso_party_id,
2929
+ provider_party_id: row.coupon_provider_party_id,
2930
+ featured: row.coupon_featured,
2931
+ round_number: row.coupon_round_number,
2932
+ beneficiary_party_id: row.coupon_beneficiary_party_id,
2933
+ coupon_amount: parseFloat(row.coupon_coupon_amount),
2934
+ tx_archive_update_id: row.coupon_tx_archive_update_id,
2935
+ tx_archive_record_time: row.coupon_tx_archive_record_time,
2936
+ app_reward_amount: row.coupon_app_reward_amount
2937
+ ? parseFloat(row.coupon_app_reward_amount)
2938
+ : null,
2939
+ created_at: row.coupon_created_at,
2940
+ updated_at: row.coupon_updated_at,
2941
+ };
2942
+ const transfer = {
2943
+ id: row.transfer_id,
2944
+ api_tracking_id: row.transfer_api_tracking_id,
2945
+ api_expires_at: row.transfer_api_expires_at,
2946
+ transfer_sender_party_id: row.transfer_transfer_sender_party_id,
2947
+ transfer_receiver_party_id: row.transfer_transfer_receiver_party_id,
2948
+ transfer_amount: parseFloat(row.transfer_transfer_amount),
2949
+ transfer_description: row.transfer_transfer_description,
2950
+ transfer_burned_amount: parseFloat(row.transfer_transfer_burned_amount),
2951
+ receiver_holding_cids: row.transfer_receiver_holding_cids,
2952
+ sender_change_cids: row.transfer_sender_change_cids,
2953
+ tx_update_id: row.transfer_tx_update_id,
2954
+ tx_record_time: row.transfer_tx_record_time,
2955
+ status: row.transfer_status,
2956
+ app_reward_coupon_id: row.transfer_app_reward_coupon_id,
2957
+ created_at: row.transfer_created_at,
2958
+ updated_at: row.transfer_updated_at,
2959
+ };
2960
+ const redemption = row.redemption_id
2961
+ ? {
2962
+ id: row.redemption_id,
2963
+ transfer_id: row.transfer_id,
2964
+ app_reward_coupon_ids: [], // This would need to be fetched separately if needed
2965
+ tx_update_id: row.redemption_tx_update_id,
2966
+ tx_record_time: row.redemption_tx_record_time,
2967
+ tx_synchronizer_id: row.redemption_tx_synchronizer_id,
2968
+ tx_effective_at: row.redemption_tx_effective_at,
2969
+ total_app_rewards: parseFloat(row.total_app_rewards),
2970
+ created_at: row.redemption_created_at,
2971
+ updated_at: row.redemption_updated_at,
2972
+ }
2973
+ : null;
2974
+ return {
2975
+ ...coupon,
2976
+ transfer,
2977
+ redemption,
2978
+ };
2979
+ }
2980
+ mapPartyFromDb(row) {
2981
+ return {
2982
+ id: row.id,
2983
+ party_id: row.party_id,
2984
+ portal_id: row.portal_id,
2985
+ provider: row.provider,
2986
+ last_payment_until: row.last_payment_until,
2987
+ is_active_customer_since: row.is_active_customer_since,
2988
+ created_at: row.created_at,
2989
+ updated_at: row.updated_at,
2990
+ portal: row.company
2991
+ ? {
2992
+ id: row.portal_id,
2993
+ company: row.company,
2994
+ }
2995
+ : null,
2996
+ };
2997
+ }
2998
+ toSnakeCase(str) {
2999
+ return str.replace(/[A-Z]/g, letter => `_${letter.toLowerCase()}`);
3000
+ }
3001
+ /**
3002
+ * Get historical issuance rates for specific rounds from the scan_acs_store table This queries ClosedMiningRound
3003
+ * contracts to get issuance rates for old rounds
3004
+ *
3005
+ * @param roundNumbers - Array of round numbers to get issuance rates for
3006
+ * @returns Array of issuance rates for the specified rounds
3007
+ */
3008
+ async getHistoricalIssuanceRates(roundNumbers) {
3009
+ if (roundNumbers.length === 0) {
3010
+ return [];
3011
+ }
3012
+ // Query the scan_acs_store table for ClosedMiningRound contracts
3013
+ // The template_id_qualified_name should match the ClosedMiningRound template
3014
+ const query = `
3015
+ SELECT
3016
+ round,
3017
+ create_arguments
3018
+ FROM scan_acs_store
3019
+ WHERE template_id_qualified_name LIKE '%:Splice.Round:ClosedMiningRound'
3020
+ AND round = ANY($1)
3021
+ AND create_arguments IS NOT NULL
3022
+ ORDER BY round ASC
3023
+ `;
3024
+ try {
3025
+ const result = await this.pool.query(query, [roundNumbers]);
3026
+ return result.rows.map(row => {
3027
+ const payload = row.create_arguments;
3028
+ const roundNumber = parseInt(row.round, 10);
3029
+ // Extract issuance rates from the contract payload
3030
+ // The payload is stored as JSON and contains the issuance rate fields
3031
+ const issuancePerFeaturedAppRewardCoupon = parseFloat(payload.issuancePerFeaturedAppRewardCoupon ?? '0');
3032
+ const issuancePerUnfeaturedAppRewardCoupon = parseFloat(payload.issuancePerUnfeaturedAppRewardCoupon ?? '0');
3033
+ return {
3034
+ roundNumber,
3035
+ issuancePerFeaturedAppRewardCoupon,
3036
+ issuancePerUnfeaturedAppRewardCoupon,
3037
+ };
3038
+ });
3039
+ }
3040
+ catch (error) {
3041
+ console.error('Error querying historical issuance rates:', error);
3042
+ throw new Error(`Failed to get historical issuance rates: ${error instanceof Error ? error.message : 'Unknown error'}`);
3043
+ }
3044
+ }
3045
+ mapValuationReportFromDb(row) {
3046
+ return {
3047
+ id: row.id,
3048
+ portal_id: row.portal_id,
3049
+ company_id: row.company_id,
3050
+ version: row.version,
3051
+ contract_id: row.contract_id,
3052
+ company_valuation: Number(row.company_valuation),
3053
+ tx_update_id: row.tx_update_id,
3054
+ last_valuation_until: row.last_valuation_until,
3055
+ created_at: row.created_at,
3056
+ updated_at: row.updated_at,
3057
+ };
3058
+ }
3059
+ /**
3060
+ * Debug method to execute custom queries for investigation
3061
+ *
3062
+ * @param query - SQL query to execute
3063
+ * @param params - Query parameters
3064
+ * @returns Query result
3065
+ */
3066
+ async debugQuery(query, params = []) {
3067
+ try {
3068
+ const result = await this.pool.query(query, params);
3069
+ return result.rows;
3070
+ }
3071
+ catch (error) {
3072
+ console.error('Error executing debug query:', error);
3073
+ throw new Error(`Failed to execute debug query: ${error instanceof Error ? error.message : 'Unknown error'}`);
3074
+ }
3075
+ }
3076
+ }
3077
+ exports.FairmintDbClient = FairmintDbClient;
3078
+ //# sourceMappingURL=fairmintDbClient.js.map