|
1 | 1 | /** |
2 | | - * BillingTracker — quota checks and usage recording via the billing_module. |
3 | | - * |
4 | | - * Discovers billing configuration from metaschema_modules_public.billing_module. |
5 | | - * Gracefully no-ops when billing is not provisioned (standalone dev mode). |
| 2 | + * BillingTracker — re-exports BillingLoader from @constructive-io/module-loader |
| 3 | + * under the legacy name `BillingTracker`. |
6 | 4 | */ |
7 | 5 |
|
8 | | -import { Logger } from '@pgpmjs/logger'; |
| 6 | +import { BillingLoader } from '@constructive-io/module-loader'; |
9 | 7 | import type { Pool } from 'pg'; |
10 | 8 |
|
11 | | -import { TtlCache } from './cache'; |
12 | | -import type { BillingModuleConfig } from './types'; |
13 | | - |
14 | | -const log = new Logger('compute:billing'); |
15 | | - |
16 | | -const BILLING_MODULE_SQL = ` |
17 | | - SELECT |
18 | | - s.schema_name AS public_schema, |
19 | | - ps.schema_name AS private_schema, |
20 | | - bm.record_usage_function |
21 | | - FROM metaschema_modules_public.billing_module bm |
22 | | - JOIN metaschema_public.schema s ON bm.schema_id = s.id |
23 | | - JOIN metaschema_public.schema ps ON bm.private_schema_id = ps.id |
24 | | - WHERE bm.database_id = $1 |
25 | | - LIMIT 1 |
26 | | -`; |
27 | | - |
28 | | -export class BillingTracker { |
29 | | - private pool: Pool; |
30 | | - private cache: TtlCache<BillingModuleConfig | null>; |
31 | | - private databaseId: string; |
32 | | - |
| 9 | +export class BillingTracker extends BillingLoader { |
33 | 10 | constructor(pool: Pool, databaseId: string, cacheTtlMs?: number) { |
34 | | - this.pool = pool; |
35 | | - this.databaseId = databaseId; |
36 | | - this.cache = new TtlCache<BillingModuleConfig | null>(cacheTtlMs ?? 60_000); |
37 | | - } |
38 | | - |
39 | | - async load(databaseId?: string): Promise<BillingModuleConfig | null> { |
40 | | - const dbId = databaseId ?? this.databaseId; |
41 | | - const cached = this.cache.get(dbId); |
42 | | - if (cached !== undefined) return cached; |
43 | | - |
44 | | - try { |
45 | | - const { rows } = await this.pool.query(BILLING_MODULE_SQL, [dbId]); |
46 | | - if (!rows.length || !rows[0].record_usage_function) { |
47 | | - this.cache.set(dbId, null); |
48 | | - return null; |
49 | | - } |
50 | | - const config: BillingModuleConfig = { |
51 | | - publicSchema: rows[0].public_schema, |
52 | | - privateSchema: rows[0].private_schema, |
53 | | - recordUsageFunction: rows[0].record_usage_function, |
54 | | - }; |
55 | | - log.debug(`loaded billing module: ${config.privateSchema}.${config.recordUsageFunction}`); |
56 | | - this.cache.set(dbId, config); |
57 | | - return config; |
58 | | - } catch { |
59 | | - this.cache.set(dbId, null); |
60 | | - return null; |
61 | | - } |
62 | | - } |
63 | | - |
64 | | - /** |
65 | | - * Check if the entity has quota for this meter. |
66 | | - * Returns true if billing is not provisioned (graceful degradation). |
67 | | - */ |
68 | | - async checkQuota( |
69 | | - entityId: string, |
70 | | - meterSlug: string, |
71 | | - amount: number = 1, |
72 | | - databaseId?: string |
73 | | - ): Promise<boolean> { |
74 | | - const config = await this.load(databaseId); |
75 | | - if (!config) return true; |
76 | | - |
77 | | - try { |
78 | | - const sql = `SELECT "${config.privateSchema}"."check_billing_quota"($1, $2::uuid, $3) AS allowed`; |
79 | | - const { rows } = await this.pool.query(sql, [meterSlug, entityId, amount]); |
80 | | - return rows[0]?.allowed !== false; |
81 | | - } catch (err: any) { |
82 | | - log.warn(`check_billing_quota failed (allowing): ${err.message}`); |
83 | | - return true; |
84 | | - } |
85 | | - } |
86 | | - |
87 | | - /** |
88 | | - * Record usage after a function completes. |
89 | | - * No-ops if billing is not provisioned. |
90 | | - */ |
91 | | - async recordUsage( |
92 | | - entityId: string, |
93 | | - meterSlug: string, |
94 | | - amount: number, |
95 | | - metadata: Record<string, unknown>, |
96 | | - databaseId?: string |
97 | | - ): Promise<void> { |
98 | | - const config = await this.load(databaseId); |
99 | | - if (!config) return; |
100 | | - |
101 | | - try { |
102 | | - const sql = `SELECT "${config.privateSchema}"."${config.recordUsageFunction}"($1, $2::uuid, $3, $4::jsonb)`; |
103 | | - await this.pool.query(sql, [meterSlug, entityId, amount, JSON.stringify(metadata)]); |
104 | | - log.debug(`recorded usage: ${meterSlug} entity=${entityId} amount=${amount}`); |
105 | | - } catch (err: any) { |
106 | | - log.warn(`record_usage failed (non-fatal): ${err.message}`); |
107 | | - } |
| 11 | + super(pool, databaseId, cacheTtlMs); |
108 | 12 | } |
109 | 13 | } |
0 commit comments