@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.
- package/README.md +184 -0
- package/dist/billing/index.d.ts +121 -0
- package/dist/billing/index.js +1082 -0
- package/dist/billing/index.js.map +1 -0
- package/dist/billing-service-LsAFesou.d.ts +578 -0
- package/dist/dunning/index.d.ts +310 -0
- package/dist/dunning/index.js +2677 -0
- package/dist/dunning/index.js.map +1 -0
- package/dist/index.d.ts +185 -0
- package/dist/index.js +7698 -0
- package/dist/index.js.map +1 -0
- package/dist/providers/index.d.ts +5 -0
- package/dist/providers/index.js +1396 -0
- package/dist/providers/index.js.map +1 -0
- package/dist/providers/iyzico.d.ts +250 -0
- package/dist/providers/iyzico.js +469 -0
- package/dist/providers/iyzico.js.map +1 -0
- package/dist/providers/paddle.d.ts +66 -0
- package/dist/providers/paddle.js +437 -0
- package/dist/providers/paddle.js.map +1 -0
- package/dist/providers/stripe.d.ts +122 -0
- package/dist/providers/stripe.js +586 -0
- package/dist/providers/stripe.js.map +1 -0
- package/dist/schema-C5Zcju_j.d.ts +4191 -0
- package/dist/types.d.ts +388 -0
- package/dist/types.js +74 -0
- package/dist/types.js.map +1 -0
- package/dist/usage/index.d.ts +2674 -0
- package/dist/usage/index.js +2916 -0
- package/dist/usage/index.js.map +1 -0
- package/dist/webhooks/index.d.ts +89 -0
- package/dist/webhooks/index.js +188 -0
- package/dist/webhooks/index.js.map +1 -0
- package/package.json +91 -0
|
@@ -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
|