@parsrun/payments 0.1.0

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.
@@ -0,0 +1,2916 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __export = (target, all) => {
3
+ for (var name in all)
4
+ __defProp(target, name, { get: all[name], enumerable: true });
5
+ };
6
+
7
+ // src/usage/types.ts
8
+ var UsageErrorCodes = {
9
+ QUOTA_EXCEEDED: "QUOTA_EXCEEDED",
10
+ PLAN_NOT_FOUND: "PLAN_NOT_FOUND",
11
+ FEATURE_NOT_FOUND: "FEATURE_NOT_FOUND",
12
+ CUSTOMER_NOT_FOUND: "CUSTOMER_NOT_FOUND",
13
+ DUPLICATE_EVENT: "DUPLICATE_EVENT",
14
+ STORAGE_ERROR: "STORAGE_ERROR",
15
+ INVALID_PERIOD: "INVALID_PERIOD"
16
+ };
17
+ var UsageError = class extends Error {
18
+ constructor(message, code, details) {
19
+ super(message);
20
+ this.code = code;
21
+ this.details = details;
22
+ this.name = "UsageError";
23
+ }
24
+ };
25
+ var QuotaExceededError = class extends UsageError {
26
+ constructor(featureKey, limit, currentUsage, requestedQuantity = 1) {
27
+ super(
28
+ `Quota exceeded for feature "${featureKey}": ${currentUsage}/${limit ?? "unlimited"} used`,
29
+ "QUOTA_EXCEEDED",
30
+ { featureKey, limit, currentUsage, requestedQuantity }
31
+ );
32
+ this.featureKey = featureKey;
33
+ this.limit = limit;
34
+ this.currentUsage = currentUsage;
35
+ this.requestedQuantity = requestedQuantity;
36
+ this.name = "QuotaExceededError";
37
+ }
38
+ };
39
+
40
+ // src/usage/memory-storage.ts
41
+ function generateId() {
42
+ return `${Date.now().toString(36)}_${Math.random().toString(36).substring(2, 9)}`;
43
+ }
44
+ var MemoryUsageStorage = class {
45
+ plans = /* @__PURE__ */ new Map();
46
+ planFeatures = /* @__PURE__ */ new Map();
47
+ customerPlans = /* @__PURE__ */ new Map();
48
+ usageEvents = [];
49
+ usageAggregates = /* @__PURE__ */ new Map();
50
+ alerts = /* @__PURE__ */ new Map();
51
+ idempotencyKeys = /* @__PURE__ */ new Set();
52
+ accessStatuses = /* @__PURE__ */ new Map();
53
+ billingCycles = /* @__PURE__ */ new Map();
54
+ // ============================================================================
55
+ // Plans
56
+ // ============================================================================
57
+ async getPlan(planId) {
58
+ return this.plans.get(planId) ?? null;
59
+ }
60
+ async getPlanByName(name) {
61
+ for (const plan of this.plans.values()) {
62
+ if (plan.name === name) {
63
+ return plan;
64
+ }
65
+ }
66
+ return null;
67
+ }
68
+ async listPlans(options) {
69
+ const plans2 = Array.from(this.plans.values());
70
+ if (options?.activeOnly) {
71
+ return plans2.filter((p) => p.isActive);
72
+ }
73
+ return plans2;
74
+ }
75
+ /**
76
+ * Add a plan (for testing/setup)
77
+ */
78
+ addPlan(plan) {
79
+ this.plans.set(plan.id, plan);
80
+ if (plan.features.length > 0) {
81
+ this.planFeatures.set(plan.id, plan.features);
82
+ }
83
+ }
84
+ // ============================================================================
85
+ // Plan Features
86
+ // ============================================================================
87
+ async getPlanFeatures(planId) {
88
+ return this.planFeatures.get(planId) ?? [];
89
+ }
90
+ async getFeatureLimit(planId, featureKey) {
91
+ const features = this.planFeatures.get(planId);
92
+ if (!features) return null;
93
+ return features.find((f) => f.featureKey === featureKey) ?? null;
94
+ }
95
+ /**
96
+ * Add plan features (for testing/setup)
97
+ */
98
+ addPlanFeatures(planId, features) {
99
+ this.planFeatures.set(planId, features);
100
+ }
101
+ // ============================================================================
102
+ // Customer-Plan Mapping
103
+ // ============================================================================
104
+ async getCustomerPlanId(customerId) {
105
+ return this.customerPlans.get(customerId) ?? null;
106
+ }
107
+ async setCustomerPlanId(customerId, planId) {
108
+ this.customerPlans.set(customerId, planId);
109
+ }
110
+ // ============================================================================
111
+ // Usage Events
112
+ // ============================================================================
113
+ async recordUsage(event) {
114
+ if (event.idempotencyKey) {
115
+ if (this.idempotencyKeys.has(event.idempotencyKey)) {
116
+ const existing = this.usageEvents.find(
117
+ (e) => e.idempotencyKey === event.idempotencyKey
118
+ );
119
+ if (existing) return existing;
120
+ }
121
+ this.idempotencyKeys.add(event.idempotencyKey);
122
+ }
123
+ const usageEvent = {
124
+ id: generateId(),
125
+ ...event,
126
+ timestamp: event.timestamp ?? /* @__PURE__ */ new Date()
127
+ };
128
+ this.usageEvents.push(usageEvent);
129
+ return usageEvent;
130
+ }
131
+ async recordUsageBatch(events) {
132
+ const results = [];
133
+ for (const event of events) {
134
+ results.push(await this.recordUsage(event));
135
+ }
136
+ return results;
137
+ }
138
+ async getUsageEvents(customerId, options) {
139
+ let events = this.usageEvents.filter((e) => e.customerId === customerId);
140
+ if (options.featureKey) {
141
+ events = events.filter((e) => e.featureKey === options.featureKey);
142
+ }
143
+ if (options.subscriptionId) {
144
+ events = events.filter((e) => e.subscriptionId === options.subscriptionId);
145
+ }
146
+ if (options.startDate) {
147
+ events = events.filter((e) => e.timestamp >= options.startDate);
148
+ }
149
+ if (options.endDate) {
150
+ events = events.filter((e) => e.timestamp <= options.endDate);
151
+ }
152
+ return events;
153
+ }
154
+ // ============================================================================
155
+ // Usage Aggregates
156
+ // ============================================================================
157
+ getAggregateKey(customerId, featureKey, periodType, periodStart) {
158
+ return `${customerId}:${featureKey}:${periodType}:${periodStart.toISOString()}`;
159
+ }
160
+ async getAggregate(customerId, featureKey, periodType, periodStart) {
161
+ const key = this.getAggregateKey(customerId, featureKey, periodType, periodStart);
162
+ return this.usageAggregates.get(key) ?? null;
163
+ }
164
+ async upsertAggregate(aggregate) {
165
+ const key = this.getAggregateKey(
166
+ aggregate.customerId,
167
+ aggregate.featureKey,
168
+ aggregate.periodType,
169
+ aggregate.periodStart
170
+ );
171
+ const existing = this.usageAggregates.get(key);
172
+ const result = {
173
+ id: existing?.id ?? generateId(),
174
+ ...aggregate,
175
+ lastUpdated: /* @__PURE__ */ new Date()
176
+ };
177
+ this.usageAggregates.set(key, result);
178
+ return result;
179
+ }
180
+ async getAggregates(customerId, options) {
181
+ let aggregates = Array.from(this.usageAggregates.values()).filter(
182
+ (a) => a.customerId === customerId
183
+ );
184
+ if (options.featureKey) {
185
+ aggregates = aggregates.filter((a) => a.featureKey === options.featureKey);
186
+ }
187
+ if (options.periodType) {
188
+ aggregates = aggregates.filter((a) => a.periodType === options.periodType);
189
+ }
190
+ if (options.subscriptionId) {
191
+ aggregates = aggregates.filter((a) => a.subscriptionId === options.subscriptionId);
192
+ }
193
+ if (options.startDate) {
194
+ aggregates = aggregates.filter((a) => a.periodStart >= options.startDate);
195
+ }
196
+ if (options.endDate) {
197
+ aggregates = aggregates.filter((a) => a.periodEnd <= options.endDate);
198
+ }
199
+ return aggregates;
200
+ }
201
+ async getCurrentPeriodUsage(customerId, featureKey, periodStart) {
202
+ const aggregate = await this.getAggregate(customerId, featureKey, "month", periodStart);
203
+ if (aggregate) {
204
+ return aggregate.totalQuantity;
205
+ }
206
+ const events = this.usageEvents.filter(
207
+ (e) => e.customerId === customerId && e.featureKey === featureKey && e.timestamp >= periodStart
208
+ );
209
+ return events.reduce((sum, e) => sum + e.quantity, 0);
210
+ }
211
+ // ============================================================================
212
+ // Alerts
213
+ // ============================================================================
214
+ async createAlert(alert) {
215
+ const usageAlert = {
216
+ id: generateId(),
217
+ ...alert,
218
+ createdAt: /* @__PURE__ */ new Date()
219
+ };
220
+ this.alerts.set(usageAlert.id, usageAlert);
221
+ return usageAlert;
222
+ }
223
+ async getActiveAlerts(customerId) {
224
+ return Array.from(this.alerts.values()).filter(
225
+ (a) => a.customerId === customerId && (a.status === "pending" || a.status === "triggered")
226
+ );
227
+ }
228
+ async updateAlertStatus(alertId, status) {
229
+ const alert = this.alerts.get(alertId);
230
+ if (alert) {
231
+ alert.status = status;
232
+ if (status === "triggered") {
233
+ alert.triggeredAt = /* @__PURE__ */ new Date();
234
+ } else if (status === "acknowledged") {
235
+ alert.acknowledgedAt = /* @__PURE__ */ new Date();
236
+ } else if (status === "resolved") {
237
+ alert.resolvedAt = /* @__PURE__ */ new Date();
238
+ }
239
+ }
240
+ }
241
+ // ============================================================================
242
+ // Usage Reset
243
+ // ============================================================================
244
+ async resetUsage(customerId, featureKeys, periodStart) {
245
+ this.usageEvents = this.usageEvents.filter((e) => {
246
+ if (e.customerId !== customerId) return true;
247
+ if (featureKeys && !featureKeys.includes(e.featureKey)) return true;
248
+ if (periodStart && e.timestamp < periodStart) return true;
249
+ return false;
250
+ });
251
+ await this.resetAggregates(customerId, featureKeys);
252
+ }
253
+ async resetAggregates(customerId, featureKeys) {
254
+ const keysToDelete = [];
255
+ for (const [key, aggregate] of this.usageAggregates) {
256
+ if (aggregate.customerId !== customerId) continue;
257
+ if (featureKeys && !featureKeys.includes(aggregate.featureKey)) continue;
258
+ keysToDelete.push(key);
259
+ }
260
+ for (const key of keysToDelete) {
261
+ this.usageAggregates.delete(key);
262
+ }
263
+ }
264
+ // ============================================================================
265
+ // Access Status
266
+ // ============================================================================
267
+ async getAccessStatus(customerId) {
268
+ return this.accessStatuses.get(customerId) ?? null;
269
+ }
270
+ async setAccessStatus(customerId, status) {
271
+ this.accessStatuses.set(customerId, status);
272
+ }
273
+ // ============================================================================
274
+ // Billing Cycle
275
+ // ============================================================================
276
+ async getBillingCycle(customerId) {
277
+ return this.billingCycles.get(customerId) ?? null;
278
+ }
279
+ async setBillingCycle(customerId, start, end) {
280
+ this.billingCycles.set(customerId, { start, end });
281
+ }
282
+ // ============================================================================
283
+ // Utilities
284
+ // ============================================================================
285
+ /**
286
+ * Clear all data (for testing)
287
+ */
288
+ clear() {
289
+ this.plans.clear();
290
+ this.planFeatures.clear();
291
+ this.customerPlans.clear();
292
+ this.usageEvents = [];
293
+ this.usageAggregates.clear();
294
+ this.alerts.clear();
295
+ this.idempotencyKeys.clear();
296
+ this.accessStatuses.clear();
297
+ this.billingCycles.clear();
298
+ }
299
+ /**
300
+ * Get stats (for debugging)
301
+ */
302
+ getStats() {
303
+ return {
304
+ plans: this.plans.size,
305
+ customers: this.customerPlans.size,
306
+ events: this.usageEvents.length,
307
+ aggregates: this.usageAggregates.size,
308
+ alerts: this.alerts.size,
309
+ accessStatuses: this.accessStatuses.size,
310
+ billingCycles: this.billingCycles.size
311
+ };
312
+ }
313
+ };
314
+ function createMemoryUsageStorage() {
315
+ return new MemoryUsageStorage();
316
+ }
317
+
318
+ // src/usage/drizzle-storage.ts
319
+ import { eq, and, gte, lte, sql, inArray } from "drizzle-orm";
320
+
321
+ // src/usage/schema.ts
322
+ var schema_exports = {};
323
+ __export(schema_exports, {
324
+ customerAccessStatus: () => customerAccessStatus,
325
+ customerBillingCycles: () => customerBillingCycles,
326
+ customerPlans: () => customerPlans,
327
+ planFeatures: () => planFeatures,
328
+ plans: () => plans,
329
+ usageAggregates: () => usageAggregates,
330
+ usageAlerts: () => usageAlerts,
331
+ usageEvents: () => usageEvents
332
+ });
333
+ import {
334
+ pgTable,
335
+ text,
336
+ integer,
337
+ boolean,
338
+ timestamp,
339
+ jsonb,
340
+ uniqueIndex,
341
+ index
342
+ } from "drizzle-orm/pg-core";
343
+ var plans = pgTable("usage_plans", {
344
+ id: text("id").primaryKey(),
345
+ name: text("name").notNull().unique(),
346
+ displayName: text("display_name").notNull(),
347
+ description: text("description"),
348
+ tier: integer("tier").notNull().default(0),
349
+ // 0=free, 1=starter, 2=pro, 3=enterprise, 4=custom
350
+ basePrice: integer("base_price").notNull().default(0),
351
+ // cents
352
+ currency: text("currency").notNull().default("usd"),
353
+ billingInterval: text("billing_interval").notNull().default("month"),
354
+ // month, year
355
+ isActive: boolean("is_active").notNull().default(true),
356
+ metadata: jsonb("metadata").$type(),
357
+ createdAt: timestamp("created_at").defaultNow().notNull(),
358
+ updatedAt: timestamp("updated_at").defaultNow().notNull()
359
+ });
360
+ var planFeatures = pgTable(
361
+ "usage_plan_features",
362
+ {
363
+ id: text("id").primaryKey(),
364
+ planId: text("plan_id").notNull().references(() => plans.id, { onDelete: "cascade" }),
365
+ featureKey: text("feature_key").notNull(),
366
+ // "api_calls", "storage_gb", "team_members"
367
+ limitValue: integer("limit_value"),
368
+ // null = unlimited
369
+ limitPeriod: text("limit_period"),
370
+ // "hour", "day", "month", null = total
371
+ isEnabled: boolean("is_enabled").notNull().default(true),
372
+ metadata: jsonb("metadata").$type()
373
+ },
374
+ (table) => ({
375
+ planFeatureIdx: uniqueIndex("usage_plan_features_plan_feature_idx").on(
376
+ table.planId,
377
+ table.featureKey
378
+ )
379
+ })
380
+ );
381
+ var customerPlans = pgTable("usage_customer_plans", {
382
+ customerId: text("customer_id").primaryKey(),
383
+ planId: text("plan_id").notNull().references(() => plans.id),
384
+ assignedAt: timestamp("assigned_at").defaultNow().notNull(),
385
+ metadata: jsonb("metadata").$type()
386
+ });
387
+ var customerAccessStatus = pgTable("usage_customer_access_status", {
388
+ customerId: text("customer_id").primaryKey(),
389
+ status: text("status").notNull().default("active"),
390
+ // active, past_due, suspended, canceled, unpaid
391
+ reason: text("reason"),
392
+ suspensionDate: timestamp("suspension_date"),
393
+ failedPaymentAttempts: integer("failed_payment_attempts").default(0),
394
+ gracePeriodEnd: timestamp("grace_period_end"),
395
+ updatedAt: timestamp("updated_at").defaultNow().notNull()
396
+ });
397
+ var customerBillingCycles = pgTable("usage_customer_billing_cycles", {
398
+ customerId: text("customer_id").primaryKey(),
399
+ periodStart: timestamp("period_start").notNull(),
400
+ periodEnd: timestamp("period_end").notNull(),
401
+ updatedAt: timestamp("updated_at").defaultNow().notNull()
402
+ });
403
+ var usageEvents = pgTable(
404
+ "usage_events",
405
+ {
406
+ id: text("id").primaryKey(),
407
+ tenantId: text("tenant_id").notNull(),
408
+ customerId: text("customer_id").notNull(),
409
+ subscriptionId: text("subscription_id"),
410
+ featureKey: text("feature_key").notNull(),
411
+ quantity: integer("quantity").notNull().default(1),
412
+ timestamp: timestamp("timestamp").defaultNow().notNull(),
413
+ metadata: jsonb("metadata").$type(),
414
+ idempotencyKey: text("idempotency_key")
415
+ },
416
+ (table) => ({
417
+ customerIdx: index("usage_events_customer_idx").on(table.customerId),
418
+ featureIdx: index("usage_events_feature_idx").on(table.featureKey),
419
+ timestampIdx: index("usage_events_timestamp_idx").on(table.timestamp),
420
+ idempotencyIdx: uniqueIndex("usage_events_idempotency_idx").on(
421
+ table.idempotencyKey
422
+ ),
423
+ // Composite index for common queries
424
+ customerFeatureTimestampIdx: index(
425
+ "usage_events_customer_feature_timestamp_idx"
426
+ ).on(table.customerId, table.featureKey, table.timestamp)
427
+ })
428
+ );
429
+ var usageAggregates = pgTable(
430
+ "usage_aggregates",
431
+ {
432
+ id: text("id").primaryKey(),
433
+ tenantId: text("tenant_id").notNull(),
434
+ customerId: text("customer_id").notNull(),
435
+ subscriptionId: text("subscription_id"),
436
+ featureKey: text("feature_key").notNull(),
437
+ periodStart: timestamp("period_start").notNull(),
438
+ periodEnd: timestamp("period_end").notNull(),
439
+ periodType: text("period_type").notNull(),
440
+ // "hour", "day", "month"
441
+ totalQuantity: integer("total_quantity").notNull().default(0),
442
+ eventCount: integer("event_count").notNull().default(0),
443
+ lastUpdated: timestamp("last_updated").defaultNow().notNull()
444
+ },
445
+ (table) => ({
446
+ // Unique constraint for upsert
447
+ lookupIdx: uniqueIndex("usage_aggregates_lookup_idx").on(
448
+ table.customerId,
449
+ table.featureKey,
450
+ table.periodType,
451
+ table.periodStart
452
+ ),
453
+ // Index for customer queries
454
+ customerIdx: index("usage_aggregates_customer_idx").on(table.customerId)
455
+ })
456
+ );
457
+ var usageAlerts = pgTable(
458
+ "usage_alerts",
459
+ {
460
+ id: text("id").primaryKey(),
461
+ tenantId: text("tenant_id").notNull(),
462
+ customerId: text("customer_id").notNull(),
463
+ subscriptionId: text("subscription_id"),
464
+ featureKey: text("feature_key").notNull(),
465
+ thresholdPercent: integer("threshold_percent").notNull(),
466
+ // 80, 100, 120
467
+ status: text("status").notNull().default("pending"),
468
+ // pending, triggered, acknowledged, resolved
469
+ currentUsage: integer("current_usage").notNull(),
470
+ limit: integer("limit").notNull(),
471
+ triggeredAt: timestamp("triggered_at"),
472
+ acknowledgedAt: timestamp("acknowledged_at"),
473
+ resolvedAt: timestamp("resolved_at"),
474
+ metadata: jsonb("metadata").$type(),
475
+ createdAt: timestamp("created_at").defaultNow().notNull()
476
+ },
477
+ (table) => ({
478
+ customerIdx: index("usage_alerts_customer_idx").on(table.customerId),
479
+ statusIdx: index("usage_alerts_status_idx").on(table.status)
480
+ })
481
+ );
482
+
483
+ // src/usage/drizzle-storage.ts
484
+ function generateId2() {
485
+ return `${Date.now().toString(36)}_${Math.random().toString(36).substring(2, 9)}`;
486
+ }
487
+ var DrizzleUsageStorage = class {
488
+ db;
489
+ constructor(config) {
490
+ this.db = config.db;
491
+ }
492
+ // ============================================================================
493
+ // Plans
494
+ // ============================================================================
495
+ async getPlan(planId) {
496
+ const rows = await this.db.select().from(plans).where(eq(plans.id, planId)).limit(1);
497
+ const row = rows[0];
498
+ if (!row) return null;
499
+ const features = await this.getPlanFeatures(planId);
500
+ return this.mapRowToPlan(row, features);
501
+ }
502
+ async getPlanByName(name) {
503
+ const rows = await this.db.select().from(plans).where(eq(plans.name, name)).limit(1);
504
+ const row = rows[0];
505
+ if (!row) return null;
506
+ const features = await this.getPlanFeatures(row.id);
507
+ return this.mapRowToPlan(row, features);
508
+ }
509
+ async listPlans(options) {
510
+ let query = this.db.select().from(plans);
511
+ if (options?.activeOnly) {
512
+ query = query.where(eq(plans.isActive, true));
513
+ }
514
+ const rows = await query;
515
+ const result = [];
516
+ for (const row of rows) {
517
+ const features = await this.getPlanFeatures(row.id);
518
+ result.push(this.mapRowToPlan(row, features));
519
+ }
520
+ return result;
521
+ }
522
+ mapRowToPlan(row, features) {
523
+ const plan = {
524
+ id: row.id,
525
+ name: row.name,
526
+ displayName: row.displayName,
527
+ description: row.description,
528
+ tier: row.tier,
529
+ basePrice: row.basePrice,
530
+ currency: row.currency,
531
+ billingInterval: row.billingInterval,
532
+ features,
533
+ isActive: row.isActive,
534
+ createdAt: row.createdAt,
535
+ updatedAt: row.updatedAt
536
+ };
537
+ if (row.metadata !== null) {
538
+ plan.metadata = row.metadata;
539
+ }
540
+ return plan;
541
+ }
542
+ // ============================================================================
543
+ // Plan Features
544
+ // ============================================================================
545
+ async getPlanFeatures(planId) {
546
+ const rows = await this.db.select().from(planFeatures).where(eq(planFeatures.planId, planId));
547
+ return rows.map((row) => this.mapRowToPlanFeature(row));
548
+ }
549
+ async getFeatureLimit(planId, featureKey) {
550
+ const rows = await this.db.select().from(planFeatures).where(
551
+ and(
552
+ eq(planFeatures.planId, planId),
553
+ eq(planFeatures.featureKey, featureKey)
554
+ )
555
+ ).limit(1);
556
+ const row = rows[0];
557
+ if (!row) return null;
558
+ return this.mapRowToPlanFeature(row);
559
+ }
560
+ mapRowToPlanFeature(row) {
561
+ const feature = {
562
+ id: row.id,
563
+ planId: row.planId,
564
+ featureKey: row.featureKey,
565
+ limitValue: row.limitValue,
566
+ limitPeriod: row.limitPeriod,
567
+ isEnabled: row.isEnabled
568
+ };
569
+ if (row.metadata !== null) {
570
+ feature.metadata = row.metadata;
571
+ }
572
+ return feature;
573
+ }
574
+ // ============================================================================
575
+ // Customer-Plan Mapping
576
+ // ============================================================================
577
+ async getCustomerPlanId(customerId) {
578
+ const rows = await this.db.select({ planId: customerPlans.planId }).from(customerPlans).where(eq(customerPlans.customerId, customerId)).limit(1);
579
+ const row = rows[0];
580
+ return row ? row.planId : null;
581
+ }
582
+ async setCustomerPlanId(customerId, planId) {
583
+ await this.db.insert(customerPlans).values({
584
+ customerId,
585
+ planId,
586
+ assignedAt: /* @__PURE__ */ new Date()
587
+ }).onConflictDoUpdate({
588
+ target: customerPlans.customerId,
589
+ set: {
590
+ planId,
591
+ assignedAt: /* @__PURE__ */ new Date()
592
+ }
593
+ });
594
+ }
595
+ // ============================================================================
596
+ // Usage Events
597
+ // ============================================================================
598
+ async recordUsage(event) {
599
+ const id = generateId2();
600
+ if (event.idempotencyKey) {
601
+ const existing = await this.db.select().from(usageEvents).where(eq(usageEvents.idempotencyKey, event.idempotencyKey)).limit(1);
602
+ const existingRow = existing[0];
603
+ if (existingRow) {
604
+ return this.mapRowToUsageEvent(existingRow);
605
+ }
606
+ }
607
+ const insertData = {
608
+ id,
609
+ tenantId: event.tenantId,
610
+ customerId: event.customerId,
611
+ featureKey: event.featureKey,
612
+ quantity: event.quantity,
613
+ timestamp: event.timestamp ?? /* @__PURE__ */ new Date()
614
+ };
615
+ if (event.subscriptionId !== void 0) {
616
+ insertData.subscriptionId = event.subscriptionId;
617
+ }
618
+ if (event.metadata !== void 0) {
619
+ insertData.metadata = event.metadata;
620
+ }
621
+ if (event.idempotencyKey !== void 0) {
622
+ insertData.idempotencyKey = event.idempotencyKey;
623
+ }
624
+ await this.db.insert(usageEvents).values(insertData);
625
+ return {
626
+ id,
627
+ ...event,
628
+ timestamp: event.timestamp ?? /* @__PURE__ */ new Date()
629
+ };
630
+ }
631
+ async recordUsageBatch(events) {
632
+ const results = [];
633
+ for (const event of events) {
634
+ results.push(await this.recordUsage(event));
635
+ }
636
+ return results;
637
+ }
638
+ async getUsageEvents(customerId, options) {
639
+ const conditions = [eq(usageEvents.customerId, customerId)];
640
+ if (options.featureKey) {
641
+ conditions.push(eq(usageEvents.featureKey, options.featureKey));
642
+ }
643
+ if (options.subscriptionId) {
644
+ conditions.push(eq(usageEvents.subscriptionId, options.subscriptionId));
645
+ }
646
+ if (options.startDate) {
647
+ conditions.push(gte(usageEvents.timestamp, options.startDate));
648
+ }
649
+ if (options.endDate) {
650
+ conditions.push(lte(usageEvents.timestamp, options.endDate));
651
+ }
652
+ const rows = await this.db.select().from(usageEvents).where(and(...conditions)).orderBy(usageEvents.timestamp);
653
+ return rows.map((row) => this.mapRowToUsageEvent(row));
654
+ }
655
+ mapRowToUsageEvent(row) {
656
+ const event = {
657
+ id: row.id,
658
+ tenantId: row.tenantId,
659
+ customerId: row.customerId,
660
+ featureKey: row.featureKey,
661
+ quantity: row.quantity,
662
+ timestamp: row.timestamp
663
+ };
664
+ if (row.subscriptionId !== null) {
665
+ event.subscriptionId = row.subscriptionId;
666
+ }
667
+ if (row.metadata !== null) {
668
+ event.metadata = row.metadata;
669
+ }
670
+ if (row.idempotencyKey !== null) {
671
+ event.idempotencyKey = row.idempotencyKey;
672
+ }
673
+ return event;
674
+ }
675
+ // ============================================================================
676
+ // Usage Aggregates
677
+ // ============================================================================
678
+ async getAggregate(customerId, featureKey, periodType, periodStart) {
679
+ const rows = await this.db.select().from(usageAggregates).where(
680
+ and(
681
+ eq(usageAggregates.customerId, customerId),
682
+ eq(usageAggregates.featureKey, featureKey),
683
+ eq(usageAggregates.periodType, periodType),
684
+ eq(usageAggregates.periodStart, periodStart)
685
+ )
686
+ ).limit(1);
687
+ const row = rows[0];
688
+ if (!row) return null;
689
+ return this.mapRowToUsageAggregate(row);
690
+ }
691
+ async upsertAggregate(aggregate) {
692
+ const id = generateId2();
693
+ const insertData = {
694
+ id,
695
+ tenantId: aggregate.tenantId,
696
+ customerId: aggregate.customerId,
697
+ featureKey: aggregate.featureKey,
698
+ periodStart: aggregate.periodStart,
699
+ periodEnd: aggregate.periodEnd,
700
+ periodType: aggregate.periodType,
701
+ totalQuantity: aggregate.totalQuantity,
702
+ eventCount: aggregate.eventCount,
703
+ lastUpdated: /* @__PURE__ */ new Date()
704
+ };
705
+ if (aggregate.subscriptionId !== void 0) {
706
+ insertData.subscriptionId = aggregate.subscriptionId;
707
+ }
708
+ await this.db.insert(usageAggregates).values(insertData).onConflictDoUpdate({
709
+ target: [
710
+ usageAggregates.customerId,
711
+ usageAggregates.featureKey,
712
+ usageAggregates.periodType,
713
+ usageAggregates.periodStart
714
+ ],
715
+ set: {
716
+ totalQuantity: aggregate.totalQuantity,
717
+ eventCount: aggregate.eventCount,
718
+ lastUpdated: /* @__PURE__ */ new Date()
719
+ }
720
+ });
721
+ return {
722
+ id,
723
+ ...aggregate,
724
+ lastUpdated: /* @__PURE__ */ new Date()
725
+ };
726
+ }
727
+ async getAggregates(customerId, options) {
728
+ const conditions = [eq(usageAggregates.customerId, customerId)];
729
+ if (options.featureKey) {
730
+ conditions.push(eq(usageAggregates.featureKey, options.featureKey));
731
+ }
732
+ if (options.periodType) {
733
+ conditions.push(eq(usageAggregates.periodType, options.periodType));
734
+ }
735
+ if (options.subscriptionId) {
736
+ conditions.push(
737
+ eq(usageAggregates.subscriptionId, options.subscriptionId)
738
+ );
739
+ }
740
+ if (options.startDate) {
741
+ conditions.push(gte(usageAggregates.periodStart, options.startDate));
742
+ }
743
+ if (options.endDate) {
744
+ conditions.push(lte(usageAggregates.periodEnd, options.endDate));
745
+ }
746
+ const rows = await this.db.select().from(usageAggregates).where(and(...conditions)).orderBy(usageAggregates.periodStart);
747
+ return rows.map((row) => this.mapRowToUsageAggregate(row));
748
+ }
749
+ async getCurrentPeriodUsage(customerId, featureKey, periodStart) {
750
+ const aggregate = await this.getAggregate(
751
+ customerId,
752
+ featureKey,
753
+ "month",
754
+ periodStart
755
+ );
756
+ if (aggregate) {
757
+ return aggregate.totalQuantity;
758
+ }
759
+ const result = await this.db.select({
760
+ total: sql`COALESCE(SUM(${usageEvents.quantity}), 0)`
761
+ }).from(usageEvents).where(
762
+ and(
763
+ eq(usageEvents.customerId, customerId),
764
+ eq(usageEvents.featureKey, featureKey),
765
+ gte(usageEvents.timestamp, periodStart)
766
+ )
767
+ );
768
+ return Number(result[0]?.total ?? 0);
769
+ }
770
+ mapRowToUsageAggregate(row) {
771
+ const aggregate = {
772
+ id: row.id,
773
+ tenantId: row.tenantId,
774
+ customerId: row.customerId,
775
+ featureKey: row.featureKey,
776
+ periodStart: row.periodStart,
777
+ periodEnd: row.periodEnd,
778
+ periodType: row.periodType,
779
+ totalQuantity: row.totalQuantity,
780
+ eventCount: row.eventCount,
781
+ lastUpdated: row.lastUpdated
782
+ };
783
+ if (row.subscriptionId !== null) {
784
+ aggregate.subscriptionId = row.subscriptionId;
785
+ }
786
+ return aggregate;
787
+ }
788
+ // ============================================================================
789
+ // Alerts
790
+ // ============================================================================
791
+ async createAlert(alert) {
792
+ const id = generateId2();
793
+ const createdAt = /* @__PURE__ */ new Date();
794
+ const insertData = {
795
+ id,
796
+ tenantId: alert.tenantId,
797
+ customerId: alert.customerId,
798
+ featureKey: alert.featureKey,
799
+ thresholdPercent: alert.thresholdPercent,
800
+ status: alert.status,
801
+ currentUsage: alert.currentUsage,
802
+ limit: alert.limit,
803
+ createdAt
804
+ };
805
+ if (alert.subscriptionId !== void 0) {
806
+ insertData.subscriptionId = alert.subscriptionId;
807
+ }
808
+ if (alert.triggeredAt !== void 0) {
809
+ insertData.triggeredAt = alert.triggeredAt;
810
+ }
811
+ if (alert.acknowledgedAt !== void 0) {
812
+ insertData.acknowledgedAt = alert.acknowledgedAt;
813
+ }
814
+ if (alert.resolvedAt !== void 0) {
815
+ insertData.resolvedAt = alert.resolvedAt;
816
+ }
817
+ if (alert.metadata !== void 0) {
818
+ insertData.metadata = alert.metadata;
819
+ }
820
+ await this.db.insert(usageAlerts).values(insertData);
821
+ return {
822
+ id,
823
+ ...alert,
824
+ createdAt
825
+ };
826
+ }
827
+ async getActiveAlerts(customerId) {
828
+ const rows = await this.db.select().from(usageAlerts).where(
829
+ and(
830
+ eq(usageAlerts.customerId, customerId),
831
+ inArray(usageAlerts.status, ["pending", "triggered"])
832
+ )
833
+ );
834
+ return rows.map((row) => this.mapRowToUsageAlert(row));
835
+ }
836
+ async updateAlertStatus(alertId, status) {
837
+ const updateData = { status };
838
+ if (status === "triggered") {
839
+ updateData.triggeredAt = /* @__PURE__ */ new Date();
840
+ } else if (status === "acknowledged") {
841
+ updateData.acknowledgedAt = /* @__PURE__ */ new Date();
842
+ } else if (status === "resolved") {
843
+ updateData.resolvedAt = /* @__PURE__ */ new Date();
844
+ }
845
+ await this.db.update(usageAlerts).set(updateData).where(eq(usageAlerts.id, alertId));
846
+ }
847
+ mapRowToUsageAlert(row) {
848
+ const alert = {
849
+ id: row.id,
850
+ tenantId: row.tenantId,
851
+ customerId: row.customerId,
852
+ featureKey: row.featureKey,
853
+ thresholdPercent: row.thresholdPercent,
854
+ status: row.status,
855
+ currentUsage: row.currentUsage,
856
+ limit: row.limit,
857
+ createdAt: row.createdAt
858
+ };
859
+ if (row.subscriptionId !== null) {
860
+ alert.subscriptionId = row.subscriptionId;
861
+ }
862
+ if (row.triggeredAt !== null) {
863
+ alert.triggeredAt = row.triggeredAt;
864
+ }
865
+ if (row.acknowledgedAt !== null) {
866
+ alert.acknowledgedAt = row.acknowledgedAt;
867
+ }
868
+ if (row.resolvedAt !== null) {
869
+ alert.resolvedAt = row.resolvedAt;
870
+ }
871
+ if (row.metadata !== null) {
872
+ alert.metadata = row.metadata;
873
+ }
874
+ return alert;
875
+ }
876
+ // ============================================================================
877
+ // Usage Reset
878
+ // ============================================================================
879
+ async resetUsage(customerId, featureKeys, periodStart) {
880
+ const eventConditions = [eq(usageEvents.customerId, customerId)];
881
+ const aggregateConditions = [eq(usageAggregates.customerId, customerId)];
882
+ if (featureKeys && featureKeys.length > 0) {
883
+ eventConditions.push(inArray(usageEvents.featureKey, featureKeys));
884
+ aggregateConditions.push(
885
+ inArray(usageAggregates.featureKey, featureKeys)
886
+ );
887
+ }
888
+ if (periodStart) {
889
+ eventConditions.push(gte(usageEvents.timestamp, periodStart));
890
+ aggregateConditions.push(gte(usageAggregates.periodStart, periodStart));
891
+ }
892
+ await this.db.delete(usageEvents).where(and(...eventConditions));
893
+ await this.db.delete(usageAggregates).where(and(...aggregateConditions));
894
+ }
895
+ async resetAggregates(customerId, featureKeys) {
896
+ const conditions = [eq(usageAggregates.customerId, customerId)];
897
+ if (featureKeys && featureKeys.length > 0) {
898
+ conditions.push(inArray(usageAggregates.featureKey, featureKeys));
899
+ }
900
+ await this.db.delete(usageAggregates).where(and(...conditions));
901
+ }
902
+ // ============================================================================
903
+ // Access Status
904
+ // ============================================================================
905
+ async getAccessStatus(customerId) {
906
+ const rows = await this.db.select().from(customerAccessStatus).where(eq(customerAccessStatus.customerId, customerId)).limit(1);
907
+ const row = rows[0];
908
+ if (!row) return null;
909
+ const status = {
910
+ status: row.status,
911
+ updatedAt: row.updatedAt
912
+ };
913
+ if (row.reason !== null) {
914
+ status.reason = row.reason;
915
+ }
916
+ if (row.suspensionDate !== null) {
917
+ status.suspensionDate = row.suspensionDate;
918
+ }
919
+ if (row.failedPaymentAttempts !== null) {
920
+ status.failedPaymentAttempts = row.failedPaymentAttempts;
921
+ }
922
+ if (row.gracePeriodEnd !== null) {
923
+ status.gracePeriodEnd = row.gracePeriodEnd;
924
+ }
925
+ return status;
926
+ }
927
+ async setAccessStatus(customerId, status) {
928
+ const insertData = {
929
+ customerId,
930
+ status: status.status,
931
+ updatedAt: status.updatedAt
932
+ };
933
+ if (status.reason !== void 0) {
934
+ insertData.reason = status.reason;
935
+ }
936
+ if (status.suspensionDate !== void 0) {
937
+ insertData.suspensionDate = status.suspensionDate;
938
+ }
939
+ if (status.failedPaymentAttempts !== void 0) {
940
+ insertData.failedPaymentAttempts = status.failedPaymentAttempts;
941
+ }
942
+ if (status.gracePeriodEnd !== void 0) {
943
+ insertData.gracePeriodEnd = status.gracePeriodEnd;
944
+ }
945
+ await this.db.insert(customerAccessStatus).values(insertData).onConflictDoUpdate({
946
+ target: customerAccessStatus.customerId,
947
+ set: {
948
+ status: status.status,
949
+ reason: status.reason ?? null,
950
+ suspensionDate: status.suspensionDate ?? null,
951
+ failedPaymentAttempts: status.failedPaymentAttempts ?? null,
952
+ gracePeriodEnd: status.gracePeriodEnd ?? null,
953
+ updatedAt: status.updatedAt
954
+ }
955
+ });
956
+ }
957
+ // ============================================================================
958
+ // Billing Cycle
959
+ // ============================================================================
960
+ async getBillingCycle(customerId) {
961
+ const rows = await this.db.select().from(customerBillingCycles).where(eq(customerBillingCycles.customerId, customerId)).limit(1);
962
+ const row = rows[0];
963
+ if (!row) return null;
964
+ return {
965
+ start: row.periodStart,
966
+ end: row.periodEnd
967
+ };
968
+ }
969
+ async setBillingCycle(customerId, start, end) {
970
+ await this.db.insert(customerBillingCycles).values({
971
+ customerId,
972
+ periodStart: start,
973
+ periodEnd: end,
974
+ updatedAt: /* @__PURE__ */ new Date()
975
+ }).onConflictDoUpdate({
976
+ target: customerBillingCycles.customerId,
977
+ set: {
978
+ periodStart: start,
979
+ periodEnd: end,
980
+ updatedAt: /* @__PURE__ */ new Date()
981
+ }
982
+ });
983
+ }
984
+ // ============================================================================
985
+ // Admin Methods (for setup)
986
+ // ============================================================================
987
+ /**
988
+ * Create or update a plan
989
+ */
990
+ async upsertPlan(plan) {
991
+ const now = /* @__PURE__ */ new Date();
992
+ await this.db.insert(plans).values({
993
+ id: plan.id,
994
+ name: plan.name,
995
+ displayName: plan.displayName,
996
+ description: plan.description,
997
+ tier: plan.tier,
998
+ basePrice: plan.basePrice,
999
+ currency: plan.currency,
1000
+ billingInterval: plan.billingInterval,
1001
+ isActive: plan.isActive,
1002
+ metadata: plan.metadata,
1003
+ createdAt: now,
1004
+ updatedAt: now
1005
+ }).onConflictDoUpdate({
1006
+ target: plans.id,
1007
+ set: {
1008
+ name: plan.name,
1009
+ displayName: plan.displayName,
1010
+ description: plan.description,
1011
+ tier: plan.tier,
1012
+ basePrice: plan.basePrice,
1013
+ currency: plan.currency,
1014
+ billingInterval: plan.billingInterval,
1015
+ isActive: plan.isActive,
1016
+ metadata: plan.metadata,
1017
+ updatedAt: now
1018
+ }
1019
+ });
1020
+ await this.db.delete(planFeatures).where(eq(planFeatures.planId, plan.id));
1021
+ for (const feature of plan.features) {
1022
+ await this.db.insert(planFeatures).values({
1023
+ id: feature.id || generateId2(),
1024
+ planId: plan.id,
1025
+ featureKey: feature.featureKey,
1026
+ limitValue: feature.limitValue,
1027
+ limitPeriod: feature.limitPeriod,
1028
+ isEnabled: feature.isEnabled,
1029
+ metadata: feature.metadata
1030
+ });
1031
+ }
1032
+ return {
1033
+ ...plan,
1034
+ createdAt: now,
1035
+ updatedAt: now
1036
+ };
1037
+ }
1038
+ /**
1039
+ * Delete a plan
1040
+ */
1041
+ async deletePlan(planId) {
1042
+ await this.db.delete(plans).where(eq(plans.id, planId));
1043
+ }
1044
+ };
1045
+ function createDrizzleUsageStorage(config) {
1046
+ return new DrizzleUsageStorage(config);
1047
+ }
1048
+
1049
+ // src/usage/quota-manager.ts
1050
+ var nullLogger = {
1051
+ debug: () => {
1052
+ },
1053
+ info: () => {
1054
+ },
1055
+ warn: () => {
1056
+ },
1057
+ error: () => {
1058
+ }
1059
+ };
1060
+ function getPeriodBoundaries(limitPeriod) {
1061
+ const now = /* @__PURE__ */ new Date();
1062
+ switch (limitPeriod) {
1063
+ case "hour": {
1064
+ const start = new Date(now);
1065
+ start.setMinutes(0, 0, 0);
1066
+ const end = new Date(start);
1067
+ end.setHours(end.getHours() + 1);
1068
+ return { start, end };
1069
+ }
1070
+ case "day": {
1071
+ const start = new Date(now);
1072
+ start.setHours(0, 0, 0, 0);
1073
+ const end = new Date(start);
1074
+ end.setDate(end.getDate() + 1);
1075
+ return { start, end };
1076
+ }
1077
+ case "month":
1078
+ default: {
1079
+ const start = new Date(now.getFullYear(), now.getMonth(), 1);
1080
+ const end = new Date(now.getFullYear(), now.getMonth() + 1, 1);
1081
+ return { start, end };
1082
+ }
1083
+ }
1084
+ }
1085
+ var QuotaManager = class {
1086
+ storage;
1087
+ logger;
1088
+ softLimits;
1089
+ gracePercent;
1090
+ overageAllowedFeatures;
1091
+ onOverage;
1092
+ constructor(config) {
1093
+ this.storage = config.storage;
1094
+ this.logger = config.logger ?? nullLogger;
1095
+ this.softLimits = config.softLimits ?? false;
1096
+ this.gracePercent = config.gracePercent ?? 0;
1097
+ this.overageAllowedFeatures = config.overageAllowedFeatures ?? null;
1098
+ if (config.onOverage !== void 0) {
1099
+ this.onOverage = config.onOverage;
1100
+ }
1101
+ }
1102
+ /**
1103
+ * Check if overage is allowed for a feature
1104
+ */
1105
+ isOverageAllowed(featureKey) {
1106
+ if (!this.softLimits) return false;
1107
+ if (this.overageAllowedFeatures === null) return true;
1108
+ return this.overageAllowedFeatures.includes(featureKey);
1109
+ }
1110
+ /**
1111
+ * Check if quota allows the requested quantity
1112
+ */
1113
+ async checkQuota(customerId, featureKey, requestedQuantity = 1) {
1114
+ const planId = await this.storage.getCustomerPlanId(customerId);
1115
+ if (!planId) {
1116
+ return {
1117
+ allowed: true,
1118
+ currentUsage: 0,
1119
+ limit: null,
1120
+ remaining: null,
1121
+ wouldExceed: false,
1122
+ percentAfter: null
1123
+ };
1124
+ }
1125
+ const feature = await this.storage.getFeatureLimit(planId, featureKey);
1126
+ if (!feature) {
1127
+ return {
1128
+ allowed: true,
1129
+ currentUsage: 0,
1130
+ limit: null,
1131
+ remaining: null,
1132
+ wouldExceed: false,
1133
+ percentAfter: null
1134
+ };
1135
+ }
1136
+ if (!feature.isEnabled) {
1137
+ return {
1138
+ allowed: false,
1139
+ currentUsage: 0,
1140
+ limit: 0,
1141
+ remaining: 0,
1142
+ wouldExceed: true,
1143
+ percentAfter: 100
1144
+ };
1145
+ }
1146
+ if (feature.limitValue === null) {
1147
+ return {
1148
+ allowed: true,
1149
+ currentUsage: 0,
1150
+ limit: null,
1151
+ remaining: null,
1152
+ wouldExceed: false,
1153
+ percentAfter: null
1154
+ };
1155
+ }
1156
+ const { start } = getPeriodBoundaries(feature.limitPeriod);
1157
+ const currentUsage = await this.storage.getCurrentPeriodUsage(
1158
+ customerId,
1159
+ featureKey,
1160
+ start
1161
+ );
1162
+ const effectiveLimit = Math.ceil(feature.limitValue * (1 + this.gracePercent / 100));
1163
+ const usageAfter = currentUsage + requestedQuantity;
1164
+ const wouldExceed = usageAfter > effectiveLimit;
1165
+ const percentAfter = Math.round(usageAfter / feature.limitValue * 100);
1166
+ const overageAllowed = this.isOverageAllowed(featureKey);
1167
+ const allowed = overageAllowed ? true : !wouldExceed;
1168
+ this.logger.debug("Quota check", {
1169
+ customerId,
1170
+ featureKey,
1171
+ currentUsage,
1172
+ requestedQuantity,
1173
+ limit: feature.limitValue,
1174
+ effectiveLimit,
1175
+ allowed,
1176
+ wouldExceed
1177
+ });
1178
+ return {
1179
+ allowed,
1180
+ currentUsage,
1181
+ limit: feature.limitValue,
1182
+ remaining: Math.max(0, feature.limitValue - currentUsage),
1183
+ wouldExceed,
1184
+ percentAfter
1185
+ };
1186
+ }
1187
+ /**
1188
+ * Enforce quota - throws if exceeded
1189
+ */
1190
+ async enforceQuota(customerId, featureKey, requestedQuantity = 1) {
1191
+ const result = await this.checkQuota(customerId, featureKey, requestedQuantity);
1192
+ if (!result.allowed) {
1193
+ this.logger.warn("Quota exceeded", {
1194
+ customerId,
1195
+ featureKey,
1196
+ currentUsage: result.currentUsage,
1197
+ limit: result.limit,
1198
+ requestedQuantity
1199
+ });
1200
+ throw new QuotaExceededError(
1201
+ featureKey,
1202
+ result.limit,
1203
+ result.currentUsage,
1204
+ requestedQuantity
1205
+ );
1206
+ }
1207
+ }
1208
+ /**
1209
+ * Get quota status for a feature
1210
+ */
1211
+ async getQuotaStatus(customerId, featureKey) {
1212
+ const planId = await this.storage.getCustomerPlanId(customerId);
1213
+ if (!planId) {
1214
+ const now = /* @__PURE__ */ new Date();
1215
+ return {
1216
+ featureKey,
1217
+ limit: null,
1218
+ used: 0,
1219
+ remaining: null,
1220
+ percentUsed: null,
1221
+ periodStart: new Date(now.getFullYear(), now.getMonth(), 1),
1222
+ periodEnd: new Date(now.getFullYear(), now.getMonth() + 1, 1),
1223
+ isExceeded: false,
1224
+ isUnlimited: true
1225
+ };
1226
+ }
1227
+ const feature = await this.storage.getFeatureLimit(planId, featureKey);
1228
+ if (!feature) {
1229
+ const now = /* @__PURE__ */ new Date();
1230
+ return {
1231
+ featureKey,
1232
+ limit: null,
1233
+ used: 0,
1234
+ remaining: null,
1235
+ percentUsed: null,
1236
+ periodStart: new Date(now.getFullYear(), now.getMonth(), 1),
1237
+ periodEnd: new Date(now.getFullYear(), now.getMonth() + 1, 1),
1238
+ isExceeded: false,
1239
+ isUnlimited: true
1240
+ };
1241
+ }
1242
+ const { start, end } = getPeriodBoundaries(feature.limitPeriod);
1243
+ const used = await this.storage.getCurrentPeriodUsage(customerId, featureKey, start);
1244
+ const isUnlimited = feature.limitValue === null;
1245
+ const limit = feature.limitValue;
1246
+ const remaining = isUnlimited ? null : Math.max(0, limit - used);
1247
+ const percentUsed = isUnlimited ? null : Math.round(used / limit * 100);
1248
+ const isExceeded = !isUnlimited && used >= limit;
1249
+ const overageAllowed = this.isOverageAllowed(featureKey);
1250
+ const overage = isUnlimited ? void 0 : used > limit ? used - limit : void 0;
1251
+ if (overage !== void 0 && overage > 0 && overageAllowed && this.onOverage) {
1252
+ try {
1253
+ await this.onOverage(customerId, featureKey, overage, limit);
1254
+ } catch (error) {
1255
+ this.logger.error("Overage callback failed", {
1256
+ error: error instanceof Error ? error.message : String(error)
1257
+ });
1258
+ }
1259
+ }
1260
+ const result = {
1261
+ featureKey,
1262
+ limit,
1263
+ used,
1264
+ remaining,
1265
+ percentUsed,
1266
+ periodStart: start,
1267
+ periodEnd: end,
1268
+ isExceeded,
1269
+ isUnlimited
1270
+ };
1271
+ if (overage !== void 0) {
1272
+ result.overage = overage;
1273
+ }
1274
+ if (overageAllowed) {
1275
+ result.overageAllowed = true;
1276
+ }
1277
+ return result;
1278
+ }
1279
+ /**
1280
+ * Get all quota statuses for a customer
1281
+ */
1282
+ async getAllQuotas(customerId) {
1283
+ const planId = await this.storage.getCustomerPlanId(customerId);
1284
+ if (!planId) return [];
1285
+ const features = await this.storage.getPlanFeatures(planId);
1286
+ const quotas = [];
1287
+ for (const feature of features) {
1288
+ const status = await this.getQuotaStatus(customerId, feature.featureKey);
1289
+ quotas.push(status);
1290
+ }
1291
+ return quotas;
1292
+ }
1293
+ /**
1294
+ * Check if any quota is exceeded
1295
+ */
1296
+ async hasExceededQuotas(customerId) {
1297
+ const quotas = await this.getAllQuotas(customerId);
1298
+ return quotas.some((q) => q.isExceeded);
1299
+ }
1300
+ /**
1301
+ * Get exceeded quotas
1302
+ */
1303
+ async getExceededQuotas(customerId) {
1304
+ const quotas = await this.getAllQuotas(customerId);
1305
+ return quotas.filter((q) => q.isExceeded);
1306
+ }
1307
+ };
1308
+ function createQuotaManager(config) {
1309
+ return new QuotaManager(config);
1310
+ }
1311
+
1312
+ // src/usage/usage-tracker.ts
1313
+ var nullLogger2 = {
1314
+ debug: () => {
1315
+ },
1316
+ info: () => {
1317
+ },
1318
+ warn: () => {
1319
+ },
1320
+ error: () => {
1321
+ }
1322
+ };
1323
+ function getPeriodStart(timestamp2, periodType) {
1324
+ const date = new Date(timestamp2);
1325
+ switch (periodType) {
1326
+ case "hour":
1327
+ date.setMinutes(0, 0, 0);
1328
+ return date;
1329
+ case "day":
1330
+ date.setHours(0, 0, 0, 0);
1331
+ return date;
1332
+ case "month":
1333
+ return new Date(date.getFullYear(), date.getMonth(), 1);
1334
+ }
1335
+ }
1336
+ function getPeriodEnd(periodStart, periodType) {
1337
+ const date = new Date(periodStart);
1338
+ switch (periodType) {
1339
+ case "hour":
1340
+ date.setHours(date.getHours() + 1);
1341
+ return date;
1342
+ case "day":
1343
+ date.setDate(date.getDate() + 1);
1344
+ return date;
1345
+ case "month":
1346
+ return new Date(date.getFullYear(), date.getMonth() + 1, 1);
1347
+ }
1348
+ }
1349
+ var UsageTracker = class {
1350
+ storage;
1351
+ logger;
1352
+ aggregateOnRecord;
1353
+ alertThresholds;
1354
+ onThresholdReached;
1355
+ constructor(config) {
1356
+ this.storage = config.storage;
1357
+ this.logger = config.logger ?? nullLogger2;
1358
+ this.aggregateOnRecord = config.aggregateOnRecord ?? true;
1359
+ this.alertThresholds = config.alertThresholds ?? [80, 100];
1360
+ if (config.onThresholdReached !== void 0) {
1361
+ this.onThresholdReached = config.onThresholdReached;
1362
+ }
1363
+ }
1364
+ /**
1365
+ * Track a usage event
1366
+ */
1367
+ async trackUsage(options) {
1368
+ const timestamp2 = options.timestamp ?? /* @__PURE__ */ new Date();
1369
+ const quantity = options.quantity ?? 1;
1370
+ this.logger.debug("Recording usage", {
1371
+ customerId: options.customerId,
1372
+ featureKey: options.featureKey,
1373
+ quantity
1374
+ });
1375
+ const eventData = {
1376
+ tenantId: options.tenantId,
1377
+ customerId: options.customerId,
1378
+ featureKey: options.featureKey,
1379
+ quantity,
1380
+ timestamp: timestamp2
1381
+ };
1382
+ if (options.subscriptionId !== void 0) {
1383
+ eventData.subscriptionId = options.subscriptionId;
1384
+ }
1385
+ if (options.metadata !== void 0) {
1386
+ eventData.metadata = options.metadata;
1387
+ }
1388
+ if (options.idempotencyKey !== void 0) {
1389
+ eventData.idempotencyKey = options.idempotencyKey;
1390
+ }
1391
+ const event = await this.storage.recordUsage(eventData);
1392
+ if (this.aggregateOnRecord) {
1393
+ await this.updateAggregates(event);
1394
+ }
1395
+ await this.checkThresholds(options.customerId, options.featureKey);
1396
+ return event;
1397
+ }
1398
+ /**
1399
+ * Track multiple usage events
1400
+ */
1401
+ async trackBatch(events) {
1402
+ const results = [];
1403
+ for (const event of events) {
1404
+ const result = await this.trackUsage(event);
1405
+ results.push(result);
1406
+ }
1407
+ return results;
1408
+ }
1409
+ /**
1410
+ * Update aggregates for an event
1411
+ */
1412
+ async updateAggregates(event) {
1413
+ const periodTypes = ["hour", "day", "month"];
1414
+ for (const periodType of periodTypes) {
1415
+ const periodStart = getPeriodStart(event.timestamp, periodType);
1416
+ const periodEnd = getPeriodEnd(periodStart, periodType);
1417
+ const existing = await this.storage.getAggregate(
1418
+ event.customerId,
1419
+ event.featureKey,
1420
+ periodType,
1421
+ periodStart
1422
+ );
1423
+ const aggregateData = {
1424
+ tenantId: event.tenantId,
1425
+ customerId: event.customerId,
1426
+ featureKey: event.featureKey,
1427
+ periodStart,
1428
+ periodEnd,
1429
+ periodType,
1430
+ totalQuantity: (existing?.totalQuantity ?? 0) + event.quantity,
1431
+ eventCount: (existing?.eventCount ?? 0) + 1,
1432
+ lastUpdated: /* @__PURE__ */ new Date()
1433
+ };
1434
+ if (event.subscriptionId !== void 0) {
1435
+ aggregateData.subscriptionId = event.subscriptionId;
1436
+ }
1437
+ await this.storage.upsertAggregate(aggregateData);
1438
+ }
1439
+ }
1440
+ /**
1441
+ * Check thresholds and create alerts if needed
1442
+ */
1443
+ async checkThresholds(customerId, featureKey) {
1444
+ const planId = await this.storage.getCustomerPlanId(customerId);
1445
+ if (!planId) return;
1446
+ const feature = await this.storage.getFeatureLimit(planId, featureKey);
1447
+ if (!feature || feature.limitValue === null) return;
1448
+ const periodStart = getPeriodStart(/* @__PURE__ */ new Date(), "month");
1449
+ const currentUsage = await this.storage.getCurrentPeriodUsage(
1450
+ customerId,
1451
+ featureKey,
1452
+ periodStart
1453
+ );
1454
+ const percentUsed = Math.round(currentUsage / feature.limitValue * 100);
1455
+ const activeAlerts = await this.storage.getActiveAlerts(customerId);
1456
+ const alertedThresholds = new Set(
1457
+ activeAlerts.filter((a) => a.featureKey === featureKey).map((a) => a.thresholdPercent)
1458
+ );
1459
+ for (const threshold of this.alertThresholds) {
1460
+ if (percentUsed >= threshold && !alertedThresholds.has(threshold)) {
1461
+ const alert = await this.storage.createAlert({
1462
+ tenantId: "",
1463
+ // Will be filled from customer
1464
+ customerId,
1465
+ featureKey,
1466
+ thresholdPercent: threshold,
1467
+ status: "triggered",
1468
+ currentUsage,
1469
+ limit: feature.limitValue,
1470
+ triggeredAt: /* @__PURE__ */ new Date()
1471
+ });
1472
+ this.logger.info("Usage threshold reached", {
1473
+ customerId,
1474
+ featureKey,
1475
+ threshold,
1476
+ percentUsed,
1477
+ currentUsage,
1478
+ limit: feature.limitValue
1479
+ });
1480
+ if (this.onThresholdReached) {
1481
+ try {
1482
+ await this.onThresholdReached(alert);
1483
+ } catch (error) {
1484
+ this.logger.error("Threshold callback failed", {
1485
+ error: error instanceof Error ? error.message : String(error)
1486
+ });
1487
+ }
1488
+ }
1489
+ }
1490
+ }
1491
+ }
1492
+ /**
1493
+ * Get usage for a customer
1494
+ */
1495
+ async getUsage(customerId, featureKey, periodType = "month") {
1496
+ const periodStart = getPeriodStart(/* @__PURE__ */ new Date(), periodType);
1497
+ return this.storage.getCurrentPeriodUsage(customerId, featureKey, periodStart);
1498
+ }
1499
+ /**
1500
+ * Get usage aggregates
1501
+ */
1502
+ async getAggregates(customerId, featureKey, periodType, startDate, endDate) {
1503
+ const options = {
1504
+ featureKey,
1505
+ periodType
1506
+ };
1507
+ if (startDate !== void 0) {
1508
+ options.startDate = startDate;
1509
+ }
1510
+ if (endDate !== void 0) {
1511
+ options.endDate = endDate;
1512
+ }
1513
+ return this.storage.getAggregates(customerId, options);
1514
+ }
1515
+ /**
1516
+ * Force aggregate recalculation for a period
1517
+ */
1518
+ async recalculateAggregates(customerId, featureKey, periodType, periodStart) {
1519
+ const periodEnd = getPeriodEnd(periodStart, periodType);
1520
+ const events = await this.storage.getUsageEvents(customerId, {
1521
+ featureKey,
1522
+ startDate: periodStart,
1523
+ endDate: periodEnd
1524
+ });
1525
+ const totalQuantity = events.reduce((sum, e) => sum + e.quantity, 0);
1526
+ const eventCount = events.length;
1527
+ const tenantId = events[0]?.tenantId ?? "";
1528
+ const subscriptionId = events[0]?.subscriptionId;
1529
+ const aggregateData = {
1530
+ tenantId,
1531
+ customerId,
1532
+ featureKey,
1533
+ periodStart,
1534
+ periodEnd,
1535
+ periodType,
1536
+ totalQuantity,
1537
+ eventCount,
1538
+ lastUpdated: /* @__PURE__ */ new Date()
1539
+ };
1540
+ if (subscriptionId !== void 0) {
1541
+ aggregateData.subscriptionId = subscriptionId;
1542
+ }
1543
+ return this.storage.upsertAggregate(aggregateData);
1544
+ }
1545
+ };
1546
+ function createUsageTracker(config) {
1547
+ return new UsageTracker(config);
1548
+ }
1549
+
1550
+ // src/usage/lifecycle-hooks.ts
1551
+ var nullLogger3 = {
1552
+ debug: () => {
1553
+ },
1554
+ info: () => {
1555
+ },
1556
+ warn: () => {
1557
+ },
1558
+ error: () => {
1559
+ }
1560
+ };
1561
+ var SubscriptionLifecycle = class {
1562
+ handlers;
1563
+ logger;
1564
+ constructor(logger) {
1565
+ this.handlers = /* @__PURE__ */ new Map();
1566
+ this.logger = logger ?? nullLogger3;
1567
+ }
1568
+ /**
1569
+ * Register an event handler
1570
+ */
1571
+ on(event, handler) {
1572
+ const handlers = this.handlers.get(event) ?? [];
1573
+ handlers.push(handler);
1574
+ this.handlers.set(event, handlers);
1575
+ this.logger.debug("Lifecycle handler registered", { event });
1576
+ return this;
1577
+ }
1578
+ /**
1579
+ * Remove an event handler
1580
+ */
1581
+ off(event, handler) {
1582
+ const handlers = this.handlers.get(event);
1583
+ if (handlers) {
1584
+ const index2 = handlers.indexOf(handler);
1585
+ if (index2 !== -1) {
1586
+ handlers.splice(index2, 1);
1587
+ }
1588
+ }
1589
+ return this;
1590
+ }
1591
+ /**
1592
+ * Emit an event to all handlers
1593
+ */
1594
+ async emit(event) {
1595
+ this.logger.info("Lifecycle event", {
1596
+ type: event.type,
1597
+ subscriptionId: event.subscription.id,
1598
+ provider: event.provider
1599
+ });
1600
+ const specificHandlers = this.handlers.get(event.type) ?? [];
1601
+ const wildcardHandlers = this.handlers.get("*") ?? [];
1602
+ const allHandlers = [...specificHandlers, ...wildcardHandlers];
1603
+ const results = await Promise.allSettled(
1604
+ allHandlers.map((handler) => handler(event))
1605
+ );
1606
+ for (const result of results) {
1607
+ if (result.status === "rejected") {
1608
+ this.logger.error("Lifecycle handler failed", {
1609
+ type: event.type,
1610
+ error: result.reason instanceof Error ? result.reason.message : String(result.reason)
1611
+ });
1612
+ }
1613
+ }
1614
+ }
1615
+ /**
1616
+ * Check if there are handlers for an event
1617
+ */
1618
+ hasHandlers(event) {
1619
+ const handlers = this.handlers.get(event);
1620
+ return handlers !== void 0 && handlers.length > 0;
1621
+ }
1622
+ /**
1623
+ * Get handler count for an event
1624
+ */
1625
+ handlerCount(event) {
1626
+ return this.handlers.get(event)?.length ?? 0;
1627
+ }
1628
+ /**
1629
+ * Clear all handlers
1630
+ */
1631
+ clear() {
1632
+ this.handlers.clear();
1633
+ }
1634
+ // ============================================================================
1635
+ // Convenience Methods
1636
+ // ============================================================================
1637
+ /**
1638
+ * Handle subscription created
1639
+ */
1640
+ onCreated(handler) {
1641
+ return this.on("subscription.created", handler);
1642
+ }
1643
+ /**
1644
+ * Handle subscription activated
1645
+ */
1646
+ onActivated(handler) {
1647
+ return this.on("subscription.activated", handler);
1648
+ }
1649
+ /**
1650
+ * Handle subscription updated
1651
+ */
1652
+ onUpdated(handler) {
1653
+ return this.on("subscription.updated", handler);
1654
+ }
1655
+ /**
1656
+ * Handle plan changed
1657
+ */
1658
+ onPlanChanged(handler) {
1659
+ return this.on("subscription.plan_changed", handler);
1660
+ }
1661
+ /**
1662
+ * Handle subscription canceled
1663
+ */
1664
+ onCanceled(handler) {
1665
+ return this.on("subscription.canceled", handler);
1666
+ }
1667
+ /**
1668
+ * Handle subscription expired
1669
+ */
1670
+ onExpired(handler) {
1671
+ return this.on("subscription.expired", handler);
1672
+ }
1673
+ /**
1674
+ * Handle subscription renewed
1675
+ */
1676
+ onRenewed(handler) {
1677
+ return this.on("subscription.renewed", handler);
1678
+ }
1679
+ /**
1680
+ * Handle trial started
1681
+ */
1682
+ onTrialStarted(handler) {
1683
+ return this.on("subscription.trial_started", handler);
1684
+ }
1685
+ /**
1686
+ * Handle trial ended
1687
+ */
1688
+ onTrialEnded(handler) {
1689
+ return this.on("subscription.trial_ended", handler);
1690
+ }
1691
+ /**
1692
+ * Handle payment failed
1693
+ */
1694
+ onPaymentFailed(handler) {
1695
+ return this.on("subscription.payment_failed", handler);
1696
+ }
1697
+ /**
1698
+ * Handle payment succeeded
1699
+ */
1700
+ onPaymentSucceeded(handler) {
1701
+ return this.on("subscription.payment_succeeded", handler);
1702
+ }
1703
+ /**
1704
+ * Handle period reset
1705
+ */
1706
+ onPeriodReset(handler) {
1707
+ return this.on("subscription.period_reset", handler);
1708
+ }
1709
+ /**
1710
+ * Handle all events
1711
+ */
1712
+ onAll(handler) {
1713
+ return this.on("*", handler);
1714
+ }
1715
+ };
1716
+ function createSubscriptionLifecycle(logger) {
1717
+ return new SubscriptionLifecycle(logger);
1718
+ }
1719
+
1720
+ // src/usage/usage-service.ts
1721
+ var nullLogger4 = {
1722
+ debug: () => {
1723
+ },
1724
+ info: () => {
1725
+ },
1726
+ warn: () => {
1727
+ },
1728
+ error: () => {
1729
+ }
1730
+ };
1731
+ var UsageService = class {
1732
+ storage;
1733
+ logger;
1734
+ quotaManager;
1735
+ usageTracker;
1736
+ lifecycle;
1737
+ limitExceededHandler;
1738
+ resetPeriod;
1739
+ autoResetOnRenewal;
1740
+ paymentGraceDays;
1741
+ maxFailedPayments;
1742
+ accessStatusChangedHandler;
1743
+ periodResetHandler;
1744
+ constructor(config) {
1745
+ this.storage = config.storage;
1746
+ this.logger = config.logger ?? nullLogger4;
1747
+ this.resetPeriod = config.resetPeriod ?? "monthly";
1748
+ this.autoResetOnRenewal = config.autoResetOnRenewal ?? true;
1749
+ this.paymentGraceDays = config.paymentGraceDays ?? 3;
1750
+ this.maxFailedPayments = config.maxFailedPayments ?? 3;
1751
+ if (config.onAccessStatusChanged !== void 0) {
1752
+ this.accessStatusChangedHandler = config.onAccessStatusChanged;
1753
+ }
1754
+ if (config.onPeriodReset !== void 0) {
1755
+ this.periodResetHandler = config.onPeriodReset;
1756
+ }
1757
+ if (config.onLimitExceeded !== void 0) {
1758
+ this.limitExceededHandler = config.onLimitExceeded;
1759
+ }
1760
+ const quotaManagerConfig = {
1761
+ storage: config.storage
1762
+ };
1763
+ if (config.logger !== void 0) {
1764
+ quotaManagerConfig.logger = config.logger;
1765
+ }
1766
+ this.quotaManager = new QuotaManager(quotaManagerConfig);
1767
+ const usageTrackerConfig = {
1768
+ storage: config.storage,
1769
+ aggregateOnRecord: config.aggregateImmediately ?? true,
1770
+ alertThresholds: config.alertThresholds ?? [80, 100]
1771
+ };
1772
+ if (config.logger !== void 0) {
1773
+ usageTrackerConfig.logger = config.logger;
1774
+ }
1775
+ if (config.onThresholdReached !== void 0) {
1776
+ usageTrackerConfig.onThresholdReached = config.onThresholdReached;
1777
+ }
1778
+ this.usageTracker = new UsageTracker(usageTrackerConfig);
1779
+ this.lifecycle = config.logger !== void 0 ? new SubscriptionLifecycle(config.logger) : new SubscriptionLifecycle();
1780
+ this.logger.info("UsageService initialized");
1781
+ }
1782
+ // ============================================================================
1783
+ // Usage Tracking
1784
+ // ============================================================================
1785
+ /**
1786
+ * Track a usage event
1787
+ */
1788
+ async trackUsage(options) {
1789
+ return this.usageTracker.trackUsage(options);
1790
+ }
1791
+ /**
1792
+ * Track multiple usage events
1793
+ */
1794
+ async trackBatch(events) {
1795
+ return this.usageTracker.trackBatch(events);
1796
+ }
1797
+ /**
1798
+ * Get current usage for a feature
1799
+ */
1800
+ async getUsage(customerId, featureKey, periodType = "month") {
1801
+ return this.usageTracker.getUsage(customerId, featureKey, periodType);
1802
+ }
1803
+ /**
1804
+ * Get usage aggregates
1805
+ */
1806
+ async getAggregates(customerId, options = {}) {
1807
+ return this.storage.getAggregates(customerId, options);
1808
+ }
1809
+ // ============================================================================
1810
+ // Quota Management
1811
+ // ============================================================================
1812
+ /**
1813
+ * Check if quota allows the requested quantity
1814
+ */
1815
+ async checkQuota(customerId, featureKey, quantity = 1) {
1816
+ return this.quotaManager.checkQuota(customerId, featureKey, quantity);
1817
+ }
1818
+ /**
1819
+ * Enforce quota - throws if exceeded
1820
+ */
1821
+ async enforceQuota(customerId, featureKey, quantity = 1) {
1822
+ return this.quotaManager.enforceQuota(customerId, featureKey, quantity);
1823
+ }
1824
+ /**
1825
+ * Get quota status for a feature
1826
+ */
1827
+ async getQuotaStatus(customerId, featureKey) {
1828
+ const status = await this.quotaManager.getQuotaStatus(customerId, featureKey);
1829
+ if (status.isExceeded && this.limitExceededHandler) {
1830
+ try {
1831
+ await this.limitExceededHandler(status, customerId);
1832
+ } catch (error) {
1833
+ this.logger.error("Limit exceeded callback failed", {
1834
+ error: error instanceof Error ? error.message : String(error)
1835
+ });
1836
+ }
1837
+ }
1838
+ return status;
1839
+ }
1840
+ /**
1841
+ * Get all quota statuses for a customer
1842
+ */
1843
+ async getAllQuotas(customerId) {
1844
+ return this.quotaManager.getAllQuotas(customerId);
1845
+ }
1846
+ /**
1847
+ * Check if any quota is exceeded
1848
+ */
1849
+ async hasExceededQuotas(customerId) {
1850
+ return this.quotaManager.hasExceededQuotas(customerId);
1851
+ }
1852
+ // ============================================================================
1853
+ // Plan Management
1854
+ // ============================================================================
1855
+ /**
1856
+ * Get customer's current plan
1857
+ */
1858
+ async getCustomerPlan(customerId) {
1859
+ const planId = await this.storage.getCustomerPlanId(customerId);
1860
+ if (!planId) return null;
1861
+ return this.storage.getPlan(planId);
1862
+ }
1863
+ /**
1864
+ * Set customer's plan
1865
+ */
1866
+ async setCustomerPlan(customerId, planId) {
1867
+ this.logger.info("Setting customer plan", { customerId, planId });
1868
+ await this.storage.setCustomerPlanId(customerId, planId);
1869
+ }
1870
+ /**
1871
+ * Get plan by ID
1872
+ */
1873
+ async getPlan(planId) {
1874
+ return this.storage.getPlan(planId);
1875
+ }
1876
+ /**
1877
+ * Get plan by name
1878
+ */
1879
+ async getPlanByName(name) {
1880
+ return this.storage.getPlanByName(name);
1881
+ }
1882
+ /**
1883
+ * List all plans
1884
+ */
1885
+ async listPlans(options) {
1886
+ return this.storage.listPlans(options);
1887
+ }
1888
+ /**
1889
+ * Get plan features
1890
+ */
1891
+ async getPlanFeatures(planId) {
1892
+ return this.storage.getPlanFeatures(planId);
1893
+ }
1894
+ // ============================================================================
1895
+ // Subscription Lifecycle
1896
+ // ============================================================================
1897
+ /**
1898
+ * Register a subscription event handler
1899
+ */
1900
+ on(event, handler) {
1901
+ this.lifecycle.on(event, handler);
1902
+ return this;
1903
+ }
1904
+ /**
1905
+ * Remove a subscription event handler
1906
+ */
1907
+ off(event, handler) {
1908
+ this.lifecycle.off(event, handler);
1909
+ return this;
1910
+ }
1911
+ /**
1912
+ * Handle subscription created
1913
+ */
1914
+ onSubscriptionCreated(handler) {
1915
+ this.lifecycle.onCreated(handler);
1916
+ return this;
1917
+ }
1918
+ /**
1919
+ * Handle subscription updated
1920
+ */
1921
+ onSubscriptionUpdated(handler) {
1922
+ this.lifecycle.onUpdated(handler);
1923
+ return this;
1924
+ }
1925
+ /**
1926
+ * Handle subscription canceled
1927
+ */
1928
+ onSubscriptionCanceled(handler) {
1929
+ this.lifecycle.onCanceled(handler);
1930
+ return this;
1931
+ }
1932
+ /**
1933
+ * Handle plan changed
1934
+ */
1935
+ onPlanChanged(handler) {
1936
+ this.lifecycle.onPlanChanged(handler);
1937
+ return this;
1938
+ }
1939
+ /**
1940
+ * Handle subscription renewed
1941
+ */
1942
+ onRenewed(handler) {
1943
+ this.lifecycle.onRenewed(handler);
1944
+ return this;
1945
+ }
1946
+ /**
1947
+ * Handle payment failed
1948
+ */
1949
+ onPaymentFailed(handler) {
1950
+ this.lifecycle.onPaymentFailed(handler);
1951
+ return this;
1952
+ }
1953
+ /**
1954
+ * Handle period reset
1955
+ */
1956
+ onPeriodReset(handler) {
1957
+ this.lifecycle.onPeriodReset(handler);
1958
+ return this;
1959
+ }
1960
+ /**
1961
+ * Get the lifecycle manager for advanced usage
1962
+ */
1963
+ get lifecycleManager() {
1964
+ return this.lifecycle;
1965
+ }
1966
+ // ============================================================================
1967
+ // Alerts
1968
+ // ============================================================================
1969
+ /**
1970
+ * Get active alerts for a customer
1971
+ */
1972
+ async getActiveAlerts(customerId) {
1973
+ return this.storage.getActiveAlerts(customerId);
1974
+ }
1975
+ /**
1976
+ * Acknowledge an alert
1977
+ */
1978
+ async acknowledgeAlert(alertId) {
1979
+ await this.storage.updateAlertStatus(alertId, "acknowledged");
1980
+ }
1981
+ /**
1982
+ * Resolve an alert
1983
+ */
1984
+ async resolveAlert(alertId) {
1985
+ await this.storage.updateAlertStatus(alertId, "resolved");
1986
+ }
1987
+ // ============================================================================
1988
+ // Usage Reset
1989
+ // ============================================================================
1990
+ /**
1991
+ * Reset usage for a customer
1992
+ * Typically called on subscription renewal
1993
+ */
1994
+ async resetUsage(customerId, featureKeys) {
1995
+ this.logger.info("Resetting usage", { customerId, featureKeys });
1996
+ const periodStart = await this.getResetPeriodStart(customerId);
1997
+ await this.storage.resetUsage(customerId, featureKeys, periodStart);
1998
+ if (this.periodResetHandler) {
1999
+ const features = featureKeys ?? await this.getCustomerFeatureKeys(customerId);
2000
+ for (const featureKey of features) {
2001
+ try {
2002
+ await this.periodResetHandler(customerId, featureKey);
2003
+ } catch (error) {
2004
+ this.logger.error("Period reset callback failed", {
2005
+ customerId,
2006
+ featureKey,
2007
+ error: error instanceof Error ? error.message : String(error)
2008
+ });
2009
+ }
2010
+ }
2011
+ }
2012
+ const plan = await this.getCustomerPlan(customerId);
2013
+ if (plan) {
2014
+ await this.lifecycle.emit({
2015
+ type: "subscription.period_reset",
2016
+ subscription: {
2017
+ id: "",
2018
+ customerId,
2019
+ status: "active",
2020
+ priceId: "",
2021
+ currentPeriodStart: periodStart,
2022
+ currentPeriodEnd: /* @__PURE__ */ new Date(),
2023
+ cancelAtPeriodEnd: false,
2024
+ provider: "stripe"
2025
+ // Default, will be overridden by BillingIntegration
2026
+ },
2027
+ newPlan: plan,
2028
+ timestamp: /* @__PURE__ */ new Date(),
2029
+ provider: "stripe"
2030
+ });
2031
+ }
2032
+ this.logger.info("Usage reset complete", { customerId });
2033
+ }
2034
+ /**
2035
+ * Get the period start date based on reset period setting
2036
+ */
2037
+ async getResetPeriodStart(customerId) {
2038
+ if (this.resetPeriod === "billing_cycle") {
2039
+ const billingCycle = await this.storage.getBillingCycle(customerId);
2040
+ if (billingCycle) {
2041
+ return billingCycle.start;
2042
+ }
2043
+ }
2044
+ const now = /* @__PURE__ */ new Date();
2045
+ return new Date(now.getFullYear(), now.getMonth(), 1);
2046
+ }
2047
+ /**
2048
+ * Get feature keys for a customer's plan
2049
+ */
2050
+ async getCustomerFeatureKeys(customerId) {
2051
+ const planId = await this.storage.getCustomerPlanId(customerId);
2052
+ if (!planId) return [];
2053
+ const features = await this.storage.getPlanFeatures(planId);
2054
+ return features.map((f) => f.featureKey);
2055
+ }
2056
+ /**
2057
+ * Check if auto-reset on renewal is enabled
2058
+ */
2059
+ get autoResetEnabled() {
2060
+ return this.autoResetOnRenewal;
2061
+ }
2062
+ /**
2063
+ * Get current reset period setting
2064
+ */
2065
+ get currentResetPeriod() {
2066
+ return this.resetPeriod;
2067
+ }
2068
+ // ============================================================================
2069
+ // Access Status
2070
+ // ============================================================================
2071
+ /**
2072
+ * Get customer access status
2073
+ */
2074
+ async getAccessStatus(customerId) {
2075
+ const status = await this.storage.getAccessStatus(customerId);
2076
+ if (status) {
2077
+ return status;
2078
+ }
2079
+ return {
2080
+ status: "active",
2081
+ updatedAt: /* @__PURE__ */ new Date()
2082
+ };
2083
+ }
2084
+ /**
2085
+ * Set customer access status
2086
+ */
2087
+ async setAccessStatus(customerId, status, options) {
2088
+ const previousStatusInfo = await this.storage.getAccessStatus(customerId);
2089
+ const previousStatus = previousStatusInfo?.status;
2090
+ const newStatus = {
2091
+ status,
2092
+ updatedAt: /* @__PURE__ */ new Date()
2093
+ };
2094
+ if (options?.reason !== void 0) {
2095
+ newStatus.reason = options.reason;
2096
+ }
2097
+ if (options?.suspensionDate !== void 0) {
2098
+ newStatus.suspensionDate = options.suspensionDate;
2099
+ }
2100
+ if (options?.failedPaymentAttempts !== void 0) {
2101
+ newStatus.failedPaymentAttempts = options.failedPaymentAttempts;
2102
+ }
2103
+ if (options?.gracePeriodEnd !== void 0) {
2104
+ newStatus.gracePeriodEnd = options.gracePeriodEnd;
2105
+ }
2106
+ await this.storage.setAccessStatus(customerId, newStatus);
2107
+ this.logger.info("Access status changed", {
2108
+ customerId,
2109
+ previousStatus,
2110
+ newStatus: status,
2111
+ reason: options?.reason
2112
+ });
2113
+ if (this.accessStatusChangedHandler) {
2114
+ try {
2115
+ await this.accessStatusChangedHandler(customerId, newStatus, previousStatus);
2116
+ } catch (error) {
2117
+ this.logger.error("Access status change callback failed", {
2118
+ error: error instanceof Error ? error.message : String(error)
2119
+ });
2120
+ }
2121
+ }
2122
+ }
2123
+ /**
2124
+ * Handle payment failure - update access status
2125
+ */
2126
+ async handlePaymentFailure(customerId) {
2127
+ const currentStatus = await this.getAccessStatus(customerId);
2128
+ const failedAttempts = (currentStatus.failedPaymentAttempts ?? 0) + 1;
2129
+ if (failedAttempts >= this.maxFailedPayments) {
2130
+ await this.setAccessStatus(customerId, "suspended", {
2131
+ reason: `Payment failed ${failedAttempts} times`,
2132
+ failedPaymentAttempts: failedAttempts
2133
+ });
2134
+ } else {
2135
+ const gracePeriodEnd = /* @__PURE__ */ new Date();
2136
+ gracePeriodEnd.setDate(gracePeriodEnd.getDate() + this.paymentGraceDays);
2137
+ await this.setAccessStatus(customerId, "past_due", {
2138
+ reason: `Payment failed (attempt ${failedAttempts}/${this.maxFailedPayments})`,
2139
+ failedPaymentAttempts: failedAttempts,
2140
+ gracePeriodEnd,
2141
+ suspensionDate: gracePeriodEnd
2142
+ });
2143
+ }
2144
+ }
2145
+ /**
2146
+ * Handle successful payment - restore access status
2147
+ */
2148
+ async handlePaymentSuccess(customerId) {
2149
+ const currentStatus = await this.getAccessStatus(customerId);
2150
+ if (currentStatus.status !== "active") {
2151
+ await this.setAccessStatus(customerId, "active", {
2152
+ reason: "Payment successful",
2153
+ failedPaymentAttempts: 0
2154
+ });
2155
+ }
2156
+ }
2157
+ /**
2158
+ * Check if customer has access (not suspended/canceled)
2159
+ */
2160
+ async hasAccess(customerId) {
2161
+ const status = await this.getAccessStatus(customerId);
2162
+ return status.status === "active" || status.status === "past_due";
2163
+ }
2164
+ /**
2165
+ * Check if customer is in grace period
2166
+ */
2167
+ async isInGracePeriod(customerId) {
2168
+ const status = await this.getAccessStatus(customerId);
2169
+ if (status.status !== "past_due") return false;
2170
+ if (!status.gracePeriodEnd) return false;
2171
+ return /* @__PURE__ */ new Date() < status.gracePeriodEnd;
2172
+ }
2173
+ // ============================================================================
2174
+ // Billing Cycle
2175
+ // ============================================================================
2176
+ /**
2177
+ * Set customer billing cycle
2178
+ */
2179
+ async setBillingCycle(customerId, start, end) {
2180
+ await this.storage.setBillingCycle(customerId, start, end);
2181
+ this.logger.debug("Billing cycle set", { customerId, start, end });
2182
+ }
2183
+ /**
2184
+ * Get customer billing cycle
2185
+ */
2186
+ async getBillingCycle(customerId) {
2187
+ return this.storage.getBillingCycle(customerId);
2188
+ }
2189
+ // ============================================================================
2190
+ // Internal
2191
+ // ============================================================================
2192
+ /**
2193
+ * Get the underlying storage
2194
+ */
2195
+ get storageBackend() {
2196
+ return this.storage;
2197
+ }
2198
+ /**
2199
+ * Get the quota manager
2200
+ */
2201
+ get quotas() {
2202
+ return this.quotaManager;
2203
+ }
2204
+ /**
2205
+ * Get the usage tracker
2206
+ */
2207
+ get tracker() {
2208
+ return this.usageTracker;
2209
+ }
2210
+ };
2211
+ function createUsageService(config) {
2212
+ return new UsageService(config);
2213
+ }
2214
+
2215
+ // src/usage/billing-integration.ts
2216
+ var nullLogger5 = {
2217
+ debug: () => {
2218
+ },
2219
+ info: () => {
2220
+ },
2221
+ warn: () => {
2222
+ },
2223
+ error: () => {
2224
+ }
2225
+ };
2226
+ var BillingIntegration = class {
2227
+ billing;
2228
+ usage;
2229
+ logger;
2230
+ autoInitializePlan;
2231
+ autoResetOnRenewal;
2232
+ autoManageAccessStatus;
2233
+ resolvePlanId;
2234
+ config;
2235
+ constructor(config) {
2236
+ this.billing = config.billing;
2237
+ this.usage = config.usage;
2238
+ this.logger = config.logger ?? nullLogger5;
2239
+ this.autoInitializePlan = config.autoInitializePlan ?? true;
2240
+ this.autoResetOnRenewal = config.autoResetOnRenewal ?? true;
2241
+ this.autoManageAccessStatus = config.autoManageAccessStatus ?? true;
2242
+ this.resolvePlanId = config.resolvePlanId ?? ((priceId) => priceId);
2243
+ this.config = config;
2244
+ this.setupWebhookHandlers();
2245
+ }
2246
+ /**
2247
+ * Setup webhook handlers on billing service
2248
+ */
2249
+ setupWebhookHandlers() {
2250
+ this.billing.onWebhook("subscription.created", async (event) => {
2251
+ await this.handleSubscriptionCreated(event);
2252
+ });
2253
+ this.billing.onWebhook("subscription.updated", async (event) => {
2254
+ await this.handleSubscriptionUpdated(event);
2255
+ });
2256
+ this.billing.onWebhook("subscription.deleted", async (event) => {
2257
+ await this.handleSubscriptionCanceled(event);
2258
+ });
2259
+ this.billing.onWebhook("invoice.paid", async (event) => {
2260
+ await this.handleInvoicePaid(event);
2261
+ });
2262
+ this.billing.onWebhook("invoice.payment_failed", async (event) => {
2263
+ await this.handlePaymentFailed(event);
2264
+ });
2265
+ this.logger.info("Billing integration initialized");
2266
+ }
2267
+ /**
2268
+ * Handle subscription created webhook
2269
+ */
2270
+ async handleSubscriptionCreated(event) {
2271
+ const subscription = event.data;
2272
+ this.logger.info("Subscription created", {
2273
+ subscriptionId: subscription.id,
2274
+ customerId: subscription.customerId,
2275
+ provider: event.provider
2276
+ });
2277
+ if (this.autoInitializePlan && subscription.priceId) {
2278
+ const planId = await this.resolvePlanId(subscription.priceId, event.provider);
2279
+ await this.usage.setCustomerPlan(subscription.customerId, planId);
2280
+ this.logger.debug("Customer plan initialized", {
2281
+ customerId: subscription.customerId,
2282
+ planId
2283
+ });
2284
+ }
2285
+ if (subscription.currentPeriodStart && subscription.currentPeriodEnd) {
2286
+ await this.usage.setBillingCycle(
2287
+ subscription.customerId,
2288
+ subscription.currentPeriodStart,
2289
+ subscription.currentPeriodEnd
2290
+ );
2291
+ }
2292
+ if (this.autoManageAccessStatus) {
2293
+ await this.usage.setAccessStatus(subscription.customerId, "active", {
2294
+ reason: "Subscription created"
2295
+ });
2296
+ }
2297
+ const plan = subscription.priceId ? await this.usage.getPlan(await this.resolvePlanId(subscription.priceId, event.provider)) : null;
2298
+ const lifecycleEvent = {
2299
+ type: "subscription.created",
2300
+ subscription: {
2301
+ id: subscription.id,
2302
+ customerId: subscription.customerId,
2303
+ status: subscription.status,
2304
+ priceId: subscription.priceId ?? "",
2305
+ currentPeriodStart: subscription.currentPeriodStart ?? /* @__PURE__ */ new Date(),
2306
+ currentPeriodEnd: subscription.currentPeriodEnd ?? /* @__PURE__ */ new Date(),
2307
+ cancelAtPeriodEnd: false,
2308
+ provider: event.provider
2309
+ },
2310
+ timestamp: /* @__PURE__ */ new Date(),
2311
+ provider: event.provider
2312
+ };
2313
+ if (plan !== null) {
2314
+ lifecycleEvent.newPlan = plan;
2315
+ }
2316
+ await this.usage.lifecycleManager.emit(lifecycleEvent);
2317
+ if (this.config.onSubscriptionCreated) {
2318
+ await this.config.onSubscriptionCreated(lifecycleEvent);
2319
+ }
2320
+ }
2321
+ /**
2322
+ * Handle subscription updated webhook
2323
+ */
2324
+ async handleSubscriptionUpdated(event) {
2325
+ const subscription = event.data;
2326
+ this.logger.info("Subscription updated", {
2327
+ subscriptionId: subscription.id,
2328
+ customerId: subscription.customerId,
2329
+ provider: event.provider
2330
+ });
2331
+ const priceChanged = subscription.priceId !== subscription.previousPriceId;
2332
+ let previousPlan = null;
2333
+ let newPlan = null;
2334
+ if (priceChanged && subscription.priceId) {
2335
+ if (subscription.previousPriceId) {
2336
+ const previousPlanId = await this.resolvePlanId(subscription.previousPriceId, event.provider);
2337
+ previousPlan = await this.usage.getPlan(previousPlanId);
2338
+ }
2339
+ const newPlanId = await this.resolvePlanId(subscription.priceId, event.provider);
2340
+ await this.usage.setCustomerPlan(subscription.customerId, newPlanId);
2341
+ newPlan = await this.usage.getPlan(newPlanId);
2342
+ this.logger.info("Customer plan changed", {
2343
+ customerId: subscription.customerId,
2344
+ previousPlanId: previousPlan?.id,
2345
+ newPlanId
2346
+ });
2347
+ }
2348
+ const eventType = priceChanged ? "subscription.plan_changed" : "subscription.updated";
2349
+ const lifecycleEvent = {
2350
+ type: eventType,
2351
+ subscription: {
2352
+ id: subscription.id,
2353
+ customerId: subscription.customerId,
2354
+ status: subscription.status,
2355
+ priceId: subscription.priceId ?? "",
2356
+ currentPeriodStart: subscription.currentPeriodStart ?? /* @__PURE__ */ new Date(),
2357
+ currentPeriodEnd: subscription.currentPeriodEnd ?? /* @__PURE__ */ new Date(),
2358
+ cancelAtPeriodEnd: false,
2359
+ provider: event.provider
2360
+ },
2361
+ timestamp: /* @__PURE__ */ new Date(),
2362
+ provider: event.provider
2363
+ };
2364
+ if (previousPlan !== null) {
2365
+ lifecycleEvent.previousPlan = previousPlan;
2366
+ }
2367
+ if (newPlan !== null) {
2368
+ lifecycleEvent.newPlan = newPlan;
2369
+ }
2370
+ await this.usage.lifecycleManager.emit(lifecycleEvent);
2371
+ if (priceChanged && this.config.onPlanChanged) {
2372
+ await this.config.onPlanChanged(lifecycleEvent);
2373
+ }
2374
+ }
2375
+ /**
2376
+ * Handle subscription canceled webhook
2377
+ */
2378
+ async handleSubscriptionCanceled(event) {
2379
+ const subscription = event.data;
2380
+ this.logger.info("Subscription canceled", {
2381
+ subscriptionId: subscription.id,
2382
+ customerId: subscription.customerId,
2383
+ provider: event.provider
2384
+ });
2385
+ if (this.autoManageAccessStatus) {
2386
+ await this.usage.setAccessStatus(subscription.customerId, "canceled", {
2387
+ reason: "Subscription canceled"
2388
+ });
2389
+ }
2390
+ const currentPlan = await this.usage.getCustomerPlan(subscription.customerId);
2391
+ const lifecycleEvent = {
2392
+ type: "subscription.canceled",
2393
+ subscription: {
2394
+ id: subscription.id,
2395
+ customerId: subscription.customerId,
2396
+ status: "canceled",
2397
+ priceId: subscription.priceId ?? "",
2398
+ currentPeriodStart: /* @__PURE__ */ new Date(),
2399
+ currentPeriodEnd: subscription.currentPeriodEnd ?? /* @__PURE__ */ new Date(),
2400
+ cancelAtPeriodEnd: true,
2401
+ provider: event.provider
2402
+ },
2403
+ timestamp: /* @__PURE__ */ new Date(),
2404
+ provider: event.provider
2405
+ };
2406
+ if (currentPlan !== null) {
2407
+ lifecycleEvent.previousPlan = currentPlan;
2408
+ }
2409
+ await this.usage.lifecycleManager.emit(lifecycleEvent);
2410
+ if (this.config.onSubscriptionCanceled) {
2411
+ await this.config.onSubscriptionCanceled(lifecycleEvent);
2412
+ }
2413
+ }
2414
+ /**
2415
+ * Handle invoice paid webhook (renewal)
2416
+ */
2417
+ async handleInvoicePaid(event) {
2418
+ const invoice = event.data;
2419
+ if (invoice.billingReason !== "subscription_cycle") {
2420
+ return;
2421
+ }
2422
+ this.logger.info("Subscription renewed", {
2423
+ invoiceId: invoice.id,
2424
+ customerId: invoice.customerId,
2425
+ subscriptionId: invoice.subscriptionId,
2426
+ provider: event.provider
2427
+ });
2428
+ if (invoice.periodStart && invoice.periodEnd) {
2429
+ await this.usage.setBillingCycle(
2430
+ invoice.customerId,
2431
+ invoice.periodStart,
2432
+ invoice.periodEnd
2433
+ );
2434
+ }
2435
+ if (this.autoResetOnRenewal && this.usage.autoResetEnabled) {
2436
+ this.logger.info("Auto-resetting quotas on renewal", {
2437
+ customerId: invoice.customerId
2438
+ });
2439
+ await this.usage.resetUsage(invoice.customerId);
2440
+ }
2441
+ if (this.autoManageAccessStatus) {
2442
+ await this.usage.handlePaymentSuccess(invoice.customerId);
2443
+ }
2444
+ const currentPlan = await this.usage.getCustomerPlan(invoice.customerId);
2445
+ const periodStart = invoice.periodStart ?? /* @__PURE__ */ new Date();
2446
+ const periodEnd = invoice.periodEnd ?? /* @__PURE__ */ new Date();
2447
+ const lifecycleEvent = {
2448
+ type: "subscription.renewed",
2449
+ subscription: {
2450
+ id: invoice.subscriptionId ?? invoice.id,
2451
+ customerId: invoice.customerId,
2452
+ status: "active",
2453
+ priceId: "",
2454
+ currentPeriodStart: periodStart,
2455
+ currentPeriodEnd: periodEnd,
2456
+ cancelAtPeriodEnd: false,
2457
+ provider: event.provider
2458
+ },
2459
+ timestamp: /* @__PURE__ */ new Date(),
2460
+ provider: event.provider
2461
+ };
2462
+ if (currentPlan !== null) {
2463
+ lifecycleEvent.newPlan = currentPlan;
2464
+ }
2465
+ await this.usage.lifecycleManager.emit(lifecycleEvent);
2466
+ if (this.config.onSubscriptionRenewed) {
2467
+ await this.config.onSubscriptionRenewed(lifecycleEvent);
2468
+ }
2469
+ }
2470
+ /**
2471
+ * Handle payment failed webhook
2472
+ */
2473
+ async handlePaymentFailed(event) {
2474
+ const invoice = event.data;
2475
+ this.logger.warn("Payment failed", {
2476
+ invoiceId: invoice.id,
2477
+ customerId: invoice.customerId,
2478
+ subscriptionId: invoice.subscriptionId,
2479
+ provider: event.provider
2480
+ });
2481
+ if (this.autoManageAccessStatus) {
2482
+ await this.usage.handlePaymentFailure(invoice.customerId);
2483
+ }
2484
+ const currentPlan = await this.usage.getCustomerPlan(invoice.customerId);
2485
+ const lifecycleEvent = {
2486
+ type: "subscription.payment_failed",
2487
+ subscription: {
2488
+ id: invoice.subscriptionId ?? invoice.id,
2489
+ customerId: invoice.customerId,
2490
+ status: "past_due",
2491
+ priceId: "",
2492
+ currentPeriodStart: /* @__PURE__ */ new Date(),
2493
+ currentPeriodEnd: /* @__PURE__ */ new Date(),
2494
+ cancelAtPeriodEnd: false,
2495
+ provider: event.provider
2496
+ },
2497
+ timestamp: /* @__PURE__ */ new Date(),
2498
+ provider: event.provider
2499
+ };
2500
+ if (currentPlan !== null) {
2501
+ lifecycleEvent.newPlan = currentPlan;
2502
+ }
2503
+ await this.usage.lifecycleManager.emit(lifecycleEvent);
2504
+ if (this.config.onPaymentFailed) {
2505
+ await this.config.onPaymentFailed(lifecycleEvent);
2506
+ }
2507
+ }
2508
+ /**
2509
+ * Manually sync customer plan from billing provider
2510
+ */
2511
+ async syncCustomerPlan(customerId, provider) {
2512
+ const subscriptionOptions = {
2513
+ customerId
2514
+ };
2515
+ if (provider !== void 0) {
2516
+ subscriptionOptions.provider = provider;
2517
+ }
2518
+ const subscriptions = await this.billing.getSubscriptions(subscriptionOptions);
2519
+ const activeSubscription = subscriptions.find(
2520
+ (sub) => sub.status === "active" || sub.status === "trialing"
2521
+ );
2522
+ if (!activeSubscription) {
2523
+ this.logger.debug("No active subscription found", { customerId });
2524
+ return null;
2525
+ }
2526
+ const planId = await this.resolvePlanId(
2527
+ activeSubscription.priceId,
2528
+ activeSubscription.provider
2529
+ );
2530
+ await this.usage.setCustomerPlan(customerId, planId);
2531
+ const plan = await this.usage.getPlan(planId);
2532
+ this.logger.info("Customer plan synced", {
2533
+ customerId,
2534
+ planId,
2535
+ subscriptionId: activeSubscription.id
2536
+ });
2537
+ return plan;
2538
+ }
2539
+ };
2540
+ function integrateBillingWithUsage(config) {
2541
+ return new BillingIntegration(config);
2542
+ }
2543
+ var createBillingIntegration = integrateBillingWithUsage;
2544
+
2545
+ // src/usage/usage-meter.ts
2546
+ var nullLogger6 = {
2547
+ debug: () => {
2548
+ },
2549
+ info: () => {
2550
+ },
2551
+ warn: () => {
2552
+ },
2553
+ error: () => {
2554
+ }
2555
+ };
2556
+ var UsageMeter = class {
2557
+ reporter;
2558
+ providerType;
2559
+ logger;
2560
+ strategy;
2561
+ maxRetries;
2562
+ retryDelayMs;
2563
+ maxBufferSize;
2564
+ config;
2565
+ buffer = [];
2566
+ intervalId = null;
2567
+ isSyncing = false;
2568
+ isRunning = false;
2569
+ constructor(config) {
2570
+ this.reporter = config.reporter;
2571
+ this.providerType = config.providerType;
2572
+ this.logger = config.logger ?? nullLogger6;
2573
+ this.strategy = config.strategy;
2574
+ this.maxRetries = config.maxRetries ?? 3;
2575
+ this.retryDelayMs = config.retryDelayMs ?? 1e3;
2576
+ this.maxBufferSize = config.maxBufferSize ?? 1e4;
2577
+ this.config = config;
2578
+ this.start();
2579
+ }
2580
+ /**
2581
+ * Start the meter (auto-sync based on strategy)
2582
+ */
2583
+ start() {
2584
+ if (this.isRunning) return;
2585
+ this.isRunning = true;
2586
+ if (this.strategy.type === "interval") {
2587
+ this.intervalId = setInterval(() => {
2588
+ this.flush().catch((err) => {
2589
+ this.logger.error("Auto-sync failed", { error: err.message });
2590
+ });
2591
+ }, this.strategy.intervalMs);
2592
+ this.logger.info("Usage meter started", {
2593
+ strategy: "interval",
2594
+ intervalMs: this.strategy.intervalMs,
2595
+ provider: this.providerType
2596
+ });
2597
+ } else if (this.strategy.type === "threshold") {
2598
+ this.logger.info("Usage meter started", {
2599
+ strategy: "threshold",
2600
+ maxRecords: this.strategy.maxRecords,
2601
+ maxAgeMs: this.strategy.maxAgeMs,
2602
+ provider: this.providerType
2603
+ });
2604
+ } else {
2605
+ this.logger.info("Usage meter started", {
2606
+ strategy: "manual",
2607
+ provider: this.providerType
2608
+ });
2609
+ }
2610
+ }
2611
+ /**
2612
+ * Stop the meter
2613
+ */
2614
+ stop() {
2615
+ this.isRunning = false;
2616
+ if (this.intervalId) {
2617
+ clearInterval(this.intervalId);
2618
+ this.intervalId = null;
2619
+ }
2620
+ this.logger.info("Usage meter stopped");
2621
+ }
2622
+ /**
2623
+ * Record usage (buffered)
2624
+ */
2625
+ record(options) {
2626
+ const usageRecord = {
2627
+ subscriptionItemId: options.subscriptionItemId,
2628
+ quantity: options.quantity,
2629
+ action: options.action ?? "increment"
2630
+ };
2631
+ if (options.timestamp !== void 0) {
2632
+ usageRecord.timestamp = options.timestamp;
2633
+ }
2634
+ if (options.idempotencyKey !== void 0) {
2635
+ usageRecord.idempotencyKey = options.idempotencyKey;
2636
+ }
2637
+ const record = {
2638
+ record: usageRecord,
2639
+ customerId: options.customerId,
2640
+ featureKey: options.featureKey,
2641
+ addedAt: /* @__PURE__ */ new Date(),
2642
+ attempts: 0
2643
+ };
2644
+ this.buffer.push(record);
2645
+ this.logger.debug("Usage recorded", {
2646
+ customerId: options.customerId,
2647
+ featureKey: options.featureKey,
2648
+ quantity: options.quantity,
2649
+ bufferSize: this.buffer.length
2650
+ });
2651
+ if (this.buffer.length > this.maxBufferSize * 0.8) {
2652
+ this.config.onBufferWarning?.(this.buffer.length, this.maxBufferSize);
2653
+ }
2654
+ if (this.buffer.length >= this.maxBufferSize) {
2655
+ this.logger.warn("Buffer full, forcing sync", { size: this.buffer.length });
2656
+ this.flush().catch((err) => {
2657
+ this.logger.error("Forced sync failed", { error: err.message });
2658
+ });
2659
+ return;
2660
+ }
2661
+ if (this.strategy.type === "threshold") {
2662
+ this.checkThreshold();
2663
+ }
2664
+ }
2665
+ /**
2666
+ * Check threshold and trigger sync if needed
2667
+ */
2668
+ checkThreshold() {
2669
+ if (this.strategy.type !== "threshold") return;
2670
+ if (this.isSyncing) return;
2671
+ const { maxRecords, maxAgeMs } = this.strategy;
2672
+ if (this.buffer.length >= maxRecords) {
2673
+ this.logger.debug("Threshold reached (count)", { count: this.buffer.length });
2674
+ this.flush().catch((err) => {
2675
+ this.logger.error("Threshold sync failed", { error: err.message });
2676
+ });
2677
+ return;
2678
+ }
2679
+ const oldestRecord = this.buffer[0];
2680
+ if (oldestRecord) {
2681
+ const age = Date.now() - oldestRecord.addedAt.getTime();
2682
+ if (age >= maxAgeMs) {
2683
+ this.logger.debug("Threshold reached (age)", { ageMs: age });
2684
+ this.flush().catch((err) => {
2685
+ this.logger.error("Threshold sync failed", {
2686
+ error: err instanceof Error ? err.message : String(err)
2687
+ });
2688
+ });
2689
+ }
2690
+ }
2691
+ }
2692
+ /**
2693
+ * Flush buffer to provider
2694
+ */
2695
+ async flush() {
2696
+ if (this.isSyncing) {
2697
+ this.logger.debug("Sync already in progress, skipping");
2698
+ return 0;
2699
+ }
2700
+ if (this.buffer.length === 0) {
2701
+ return 0;
2702
+ }
2703
+ this.isSyncing = true;
2704
+ try {
2705
+ const toSync = [...this.buffer];
2706
+ this.buffer = [];
2707
+ this.logger.info("Syncing usage to provider", {
2708
+ count: toSync.length,
2709
+ provider: this.providerType
2710
+ });
2711
+ const aggregated = this.aggregateRecords(toSync);
2712
+ const failed = [];
2713
+ for (const [subscriptionItemId, records] of aggregated) {
2714
+ try {
2715
+ const totalQuantity = records.reduce((sum, r) => sum + r.record.quantity, 0);
2716
+ const syncRecord = {
2717
+ subscriptionItemId,
2718
+ quantity: totalQuantity,
2719
+ action: "increment"
2720
+ };
2721
+ const firstTimestamp = records[0]?.record.timestamp;
2722
+ if (firstTimestamp !== void 0) {
2723
+ syncRecord.timestamp = firstTimestamp;
2724
+ }
2725
+ await this.syncWithRetry(syncRecord);
2726
+ this.logger.debug("Usage synced", {
2727
+ subscriptionItemId,
2728
+ quantity: totalQuantity,
2729
+ recordCount: records.length
2730
+ });
2731
+ } catch (error) {
2732
+ this.logger.error("Failed to sync usage", {
2733
+ subscriptionItemId,
2734
+ error: error instanceof Error ? error.message : String(error)
2735
+ });
2736
+ for (const record of records) {
2737
+ record.attempts++;
2738
+ if (record.attempts < this.maxRetries) {
2739
+ failed.push(record);
2740
+ } else {
2741
+ this.logger.error("Record exceeded max retries, dropping", {
2742
+ customerId: record.customerId,
2743
+ featureKey: record.featureKey,
2744
+ attempts: record.attempts
2745
+ });
2746
+ }
2747
+ }
2748
+ }
2749
+ }
2750
+ if (failed.length > 0) {
2751
+ this.buffer.unshift(...failed);
2752
+ this.config.onSyncError?.(new Error("Some records failed to sync"), failed);
2753
+ }
2754
+ const syncedCount = toSync.length - failed.length;
2755
+ if (syncedCount > 0) {
2756
+ this.config.onSyncSuccess?.(syncedCount);
2757
+ }
2758
+ return syncedCount;
2759
+ } finally {
2760
+ this.isSyncing = false;
2761
+ }
2762
+ }
2763
+ /**
2764
+ * Aggregate records by subscription item ID
2765
+ */
2766
+ aggregateRecords(records) {
2767
+ const map = /* @__PURE__ */ new Map();
2768
+ for (const record of records) {
2769
+ const key = record.record.subscriptionItemId;
2770
+ const existing = map.get(key) ?? [];
2771
+ existing.push(record);
2772
+ map.set(key, existing);
2773
+ }
2774
+ return map;
2775
+ }
2776
+ /**
2777
+ * Sync single record with retry
2778
+ */
2779
+ async syncWithRetry(record) {
2780
+ let lastError = null;
2781
+ for (let attempt = 0; attempt < this.maxRetries; attempt++) {
2782
+ try {
2783
+ await this.reporter.reportUsage(record);
2784
+ return;
2785
+ } catch (error) {
2786
+ lastError = error instanceof Error ? error : new Error(String(error));
2787
+ this.logger.warn("Sync attempt failed, retrying", {
2788
+ attempt: attempt + 1,
2789
+ maxRetries: this.maxRetries,
2790
+ error: lastError.message
2791
+ });
2792
+ if (attempt < this.maxRetries - 1) {
2793
+ await this.delay(this.retryDelayMs * (attempt + 1));
2794
+ }
2795
+ }
2796
+ }
2797
+ throw lastError ?? new Error("Sync failed after retries");
2798
+ }
2799
+ /**
2800
+ * Delay helper
2801
+ */
2802
+ delay(ms) {
2803
+ return new Promise((resolve) => setTimeout(resolve, ms));
2804
+ }
2805
+ /**
2806
+ * Get buffer size
2807
+ */
2808
+ get bufferSize() {
2809
+ return this.buffer.length;
2810
+ }
2811
+ /**
2812
+ * Get sync status
2813
+ */
2814
+ get syncing() {
2815
+ return this.isSyncing;
2816
+ }
2817
+ /**
2818
+ * Get running status
2819
+ */
2820
+ get running() {
2821
+ return this.isRunning;
2822
+ }
2823
+ /**
2824
+ * Get buffer contents (for debugging)
2825
+ */
2826
+ getBuffer() {
2827
+ return this.buffer;
2828
+ }
2829
+ };
2830
+ function createUsageMeter(config) {
2831
+ return new UsageMeter(config);
2832
+ }
2833
+ var SubscriptionItemResolver = class {
2834
+ reporter;
2835
+ cacheTtlMs;
2836
+ logger;
2837
+ cache = /* @__PURE__ */ new Map();
2838
+ constructor(config) {
2839
+ this.reporter = config.reporter;
2840
+ this.cacheTtlMs = config.cacheTtlMs ?? 60 * 60 * 1e3;
2841
+ this.logger = config.logger ?? nullLogger6;
2842
+ }
2843
+ /**
2844
+ * Get subscription item ID
2845
+ */
2846
+ async resolve(subscriptionId, priceId) {
2847
+ const cacheKey = `${subscriptionId}:${priceId}`;
2848
+ const cached = this.cache.get(cacheKey);
2849
+ if (cached && cached.expiresAt > /* @__PURE__ */ new Date()) {
2850
+ return cached.itemId;
2851
+ }
2852
+ if (!this.reporter.getSubscriptionItemId) {
2853
+ this.logger.warn("Reporter does not support getSubscriptionItemId");
2854
+ return null;
2855
+ }
2856
+ const itemId = await this.reporter.getSubscriptionItemId(subscriptionId, priceId);
2857
+ if (itemId) {
2858
+ this.cache.set(cacheKey, {
2859
+ itemId,
2860
+ expiresAt: new Date(Date.now() + this.cacheTtlMs)
2861
+ });
2862
+ }
2863
+ return itemId;
2864
+ }
2865
+ /**
2866
+ * Set cache entry manually (useful when creating subscriptions)
2867
+ */
2868
+ setCache(subscriptionId, priceId, itemId) {
2869
+ const cacheKey = `${subscriptionId}:${priceId}`;
2870
+ this.cache.set(cacheKey, {
2871
+ itemId,
2872
+ expiresAt: new Date(Date.now() + this.cacheTtlMs)
2873
+ });
2874
+ }
2875
+ /**
2876
+ * Clear cache
2877
+ */
2878
+ clearCache() {
2879
+ this.cache.clear();
2880
+ }
2881
+ /**
2882
+ * Get cache size
2883
+ */
2884
+ get cacheSize() {
2885
+ return this.cache.size;
2886
+ }
2887
+ };
2888
+ function createSubscriptionItemResolver(config) {
2889
+ return new SubscriptionItemResolver(config);
2890
+ }
2891
+ export {
2892
+ BillingIntegration,
2893
+ DrizzleUsageStorage,
2894
+ MemoryUsageStorage,
2895
+ QuotaExceededError,
2896
+ QuotaManager,
2897
+ SubscriptionItemResolver,
2898
+ SubscriptionLifecycle,
2899
+ UsageError,
2900
+ UsageErrorCodes,
2901
+ UsageMeter,
2902
+ UsageService,
2903
+ UsageTracker,
2904
+ createBillingIntegration,
2905
+ createDrizzleUsageStorage,
2906
+ createMemoryUsageStorage,
2907
+ createQuotaManager,
2908
+ createSubscriptionItemResolver,
2909
+ createSubscriptionLifecycle,
2910
+ createUsageMeter,
2911
+ createUsageService,
2912
+ createUsageTracker,
2913
+ integrateBillingWithUsage,
2914
+ schema_exports as usageSchema
2915
+ };
2916
+ //# sourceMappingURL=index.js.map