@trendyvideo/billing 1.0.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/dist/__tests__/billing-coverage.test.d.ts +2 -0
- package/dist/__tests__/billing-coverage.test.d.ts.map +1 -0
- package/dist/__tests__/billing-coverage.test.js +170 -0
- package/dist/__tests__/billing-coverage.test.js.map +1 -0
- package/dist/__tests__/plan-limits.test.d.ts +2 -0
- package/dist/__tests__/plan-limits.test.d.ts.map +1 -0
- package/dist/__tests__/plan-limits.test.js +139 -0
- package/dist/__tests__/plan-limits.test.js.map +1 -0
- package/dist/__tests__/pricing.test.d.ts +2 -0
- package/dist/__tests__/pricing.test.d.ts.map +1 -0
- package/dist/__tests__/pricing.test.js +146 -0
- package/dist/__tests__/pricing.test.js.map +1 -0
- package/dist/__tests__/usage-tracker.test.d.ts +2 -0
- package/dist/__tests__/usage-tracker.test.d.ts.map +1 -0
- package/dist/__tests__/usage-tracker.test.js +210 -0
- package/dist/__tests__/usage-tracker.test.js.map +1 -0
- package/dist/alerts.d.ts +19 -0
- package/dist/alerts.d.ts.map +1 -0
- package/dist/alerts.js +90 -0
- package/dist/alerts.js.map +1 -0
- package/dist/guards/project-guard.d.ts +6 -0
- package/dist/guards/project-guard.d.ts.map +1 -0
- package/dist/guards/project-guard.js +56 -0
- package/dist/guards/project-guard.js.map +1 -0
- package/dist/guards/resolution-guard.d.ts +6 -0
- package/dist/guards/resolution-guard.d.ts.map +1 -0
- package/dist/guards/resolution-guard.js +40 -0
- package/dist/guards/resolution-guard.js.map +1 -0
- package/dist/guards/seat-guard.d.ts +8 -0
- package/dist/guards/seat-guard.d.ts.map +1 -0
- package/dist/guards/seat-guard.js +55 -0
- package/dist/guards/seat-guard.js.map +1 -0
- package/dist/guards/social-guard.d.ts +6 -0
- package/dist/guards/social-guard.d.ts.map +1 -0
- package/dist/guards/social-guard.js +52 -0
- package/dist/guards/social-guard.js.map +1 -0
- package/dist/guards/storage-guard.d.ts +14 -0
- package/dist/guards/storage-guard.d.ts.map +1 -0
- package/dist/guards/storage-guard.js +85 -0
- package/dist/guards/storage-guard.js.map +1 -0
- package/dist/guards/stream-guard.d.ts +7 -0
- package/dist/guards/stream-guard.d.ts.map +1 -0
- package/dist/guards/stream-guard.js +73 -0
- package/dist/guards/stream-guard.js.map +1 -0
- package/dist/guards/watch-guard.d.ts +13 -0
- package/dist/guards/watch-guard.d.ts.map +1 -0
- package/dist/guards/watch-guard.js +71 -0
- package/dist/guards/watch-guard.js.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +49 -0
- package/dist/index.js.map +1 -0
- package/dist/plan-limits.d.ts +25 -0
- package/dist/plan-limits.d.ts.map +1 -0
- package/dist/plan-limits.js +138 -0
- package/dist/plan-limits.js.map +1 -0
- package/dist/pricing.d.ts +32 -0
- package/dist/pricing.d.ts.map +1 -0
- package/dist/pricing.js +126 -0
- package/dist/pricing.js.map +1 -0
- package/dist/types.d.ts +188 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/dist/usage-checker.d.ts +26 -0
- package/dist/usage-checker.d.ts.map +1 -0
- package/dist/usage-checker.js +295 -0
- package/dist/usage-checker.js.map +1 -0
- package/dist/usage-tracker.d.ts +25 -0
- package/dist/usage-tracker.d.ts.map +1 -0
- package/dist/usage-tracker.js +181 -0
- package/dist/usage-tracker.js.map +1 -0
- package/package.json +41 -0
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const vitest_1 = require("vitest");
|
|
4
|
+
const usage_tracker_1 = require("../usage-tracker");
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
// Helpers
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
const silentLogger = {
|
|
9
|
+
debug: vitest_1.vi.fn(),
|
|
10
|
+
info: vitest_1.vi.fn(),
|
|
11
|
+
warn: vitest_1.vi.fn(),
|
|
12
|
+
error: vitest_1.vi.fn(),
|
|
13
|
+
};
|
|
14
|
+
function makeEvent(overrides = {}) {
|
|
15
|
+
return {
|
|
16
|
+
organizationId: 'org-123',
|
|
17
|
+
service: 'agent',
|
|
18
|
+
model: 'claude-sonnet-4-6',
|
|
19
|
+
inputTokens: 1000,
|
|
20
|
+
outputTokens: 500,
|
|
21
|
+
...overrides,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
function makeSupabaseMock(options = {}) {
|
|
25
|
+
const insertFn = vitest_1.vi.fn().mockResolvedValue({
|
|
26
|
+
error: options.insertError ? { message: options.insertError } : null,
|
|
27
|
+
});
|
|
28
|
+
const singleFn = vitest_1.vi.fn().mockResolvedValue({
|
|
29
|
+
data: options.subData ?? {
|
|
30
|
+
current_period_start: '2026-03-01',
|
|
31
|
+
current_period_end: '2026-04-01',
|
|
32
|
+
},
|
|
33
|
+
error: options.subError ? { message: options.subError } : null,
|
|
34
|
+
});
|
|
35
|
+
const limitFn = vitest_1.vi.fn().mockReturnValue({ single: singleFn });
|
|
36
|
+
const orderFn = vitest_1.vi.fn().mockReturnValue({ limit: limitFn });
|
|
37
|
+
const eqFn = vitest_1.vi.fn().mockReturnValue({ order: orderFn });
|
|
38
|
+
const selectFn = vitest_1.vi.fn().mockReturnValue({ eq: eqFn });
|
|
39
|
+
const fromFn = vitest_1.vi.fn().mockImplementation((table) => {
|
|
40
|
+
if (table === 'usage_events')
|
|
41
|
+
return { insert: insertFn };
|
|
42
|
+
if (table === 'organization_subscriptions')
|
|
43
|
+
return { select: selectFn };
|
|
44
|
+
if (table === 'usage_records') {
|
|
45
|
+
return {
|
|
46
|
+
upsert: vitest_1.vi.fn().mockResolvedValue({
|
|
47
|
+
error: options.upsertError ? { message: options.upsertError } : null,
|
|
48
|
+
}),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
return {};
|
|
52
|
+
});
|
|
53
|
+
const rpcFn = vitest_1.vi.fn().mockImplementation((fnName) => {
|
|
54
|
+
if (fnName === 'record_usage_atomic') {
|
|
55
|
+
if (options.rpcAtomicError) {
|
|
56
|
+
return Promise.resolve({
|
|
57
|
+
data: null,
|
|
58
|
+
error: { message: options.rpcAtomicError },
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
return Promise.resolve({
|
|
62
|
+
data: 'evt-uuid-1234',
|
|
63
|
+
error: null,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
if (fnName === 'upsert_usage_record') {
|
|
67
|
+
return Promise.resolve({
|
|
68
|
+
error: options.rpcError ? { message: options.rpcError } : null,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
return Promise.resolve({ data: null, error: null });
|
|
72
|
+
});
|
|
73
|
+
return { from: fromFn, rpc: rpcFn };
|
|
74
|
+
}
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
// Tests
|
|
77
|
+
// ---------------------------------------------------------------------------
|
|
78
|
+
(0, vitest_1.describe)('recordUsage', () => {
|
|
79
|
+
(0, vitest_1.beforeEach)(() => {
|
|
80
|
+
vitest_1.vi.restoreAllMocks();
|
|
81
|
+
});
|
|
82
|
+
// =========================================================================
|
|
83
|
+
// Happy path: atomic RPC succeeds
|
|
84
|
+
// =========================================================================
|
|
85
|
+
(0, vitest_1.it)('calls record_usage_atomic RPC and does NOT use from() on success', async () => {
|
|
86
|
+
const supabase = makeSupabaseMock();
|
|
87
|
+
await (0, usage_tracker_1.recordUsage)(supabase, makeEvent(), silentLogger);
|
|
88
|
+
// Should call the atomic RPC
|
|
89
|
+
(0, vitest_1.expect)(supabase.rpc).toHaveBeenCalledWith('record_usage_atomic', vitest_1.expect.objectContaining({
|
|
90
|
+
p_organization_id: 'org-123',
|
|
91
|
+
p_service: 'agent',
|
|
92
|
+
p_model: 'claude-sonnet-4-6',
|
|
93
|
+
p_input_tokens: 1000,
|
|
94
|
+
p_output_tokens: 500,
|
|
95
|
+
}));
|
|
96
|
+
// Should NOT fall back to from() calls
|
|
97
|
+
(0, vitest_1.expect)(supabase.from).not.toHaveBeenCalled();
|
|
98
|
+
});
|
|
99
|
+
(0, vitest_1.it)('passes creditsConsumed to the atomic RPC', async () => {
|
|
100
|
+
const supabase = makeSupabaseMock();
|
|
101
|
+
await (0, usage_tracker_1.recordUsage)(supabase, makeEvent(), silentLogger);
|
|
102
|
+
// claude-sonnet-4-6: 1000*3 + 500*15 = 10500 microdollars
|
|
103
|
+
// 10500 / 10000 = 1.05 credits
|
|
104
|
+
(0, vitest_1.expect)(supabase.rpc).toHaveBeenCalledWith('record_usage_atomic', vitest_1.expect.objectContaining({
|
|
105
|
+
p_cost_microdollars: 10500,
|
|
106
|
+
p_credits_consumed: 1.05,
|
|
107
|
+
}));
|
|
108
|
+
});
|
|
109
|
+
(0, vitest_1.it)('passes conversationId to atomic RPC when provided', async () => {
|
|
110
|
+
const supabase = makeSupabaseMock();
|
|
111
|
+
const event = makeEvent({ conversationId: 'conv-456' });
|
|
112
|
+
await (0, usage_tracker_1.recordUsage)(supabase, event, silentLogger);
|
|
113
|
+
(0, vitest_1.expect)(supabase.rpc).toHaveBeenCalledWith('record_usage_atomic', vitest_1.expect.objectContaining({
|
|
114
|
+
p_conversation_id: 'conv-456',
|
|
115
|
+
}));
|
|
116
|
+
});
|
|
117
|
+
(0, vitest_1.it)('passes metadata to atomic RPC when provided', async () => {
|
|
118
|
+
const supabase = makeSupabaseMock();
|
|
119
|
+
const event = makeEvent({ metadata: { stream_id: 's-789' } });
|
|
120
|
+
await (0, usage_tracker_1.recordUsage)(supabase, event, silentLogger);
|
|
121
|
+
(0, vitest_1.expect)(supabase.rpc).toHaveBeenCalledWith('record_usage_atomic', vitest_1.expect.objectContaining({
|
|
122
|
+
p_metadata: { stream_id: 's-789' },
|
|
123
|
+
}));
|
|
124
|
+
});
|
|
125
|
+
(0, vitest_1.it)('uses costMicrodollarsOverride when provided', async () => {
|
|
126
|
+
const supabase = makeSupabaseMock();
|
|
127
|
+
const event = makeEvent({
|
|
128
|
+
model: 'chirp-tts',
|
|
129
|
+
inputTokens: 0,
|
|
130
|
+
outputTokens: 0,
|
|
131
|
+
costMicrodollarsOverride: 30000,
|
|
132
|
+
});
|
|
133
|
+
await (0, usage_tracker_1.recordUsage)(supabase, event, silentLogger);
|
|
134
|
+
(0, vitest_1.expect)(supabase.rpc).toHaveBeenCalledWith('record_usage_atomic', vitest_1.expect.objectContaining({
|
|
135
|
+
p_cost_microdollars: 30000,
|
|
136
|
+
}));
|
|
137
|
+
});
|
|
138
|
+
(0, vitest_1.it)('handles missing orgId without crashing', async () => {
|
|
139
|
+
const supabase = makeSupabaseMock();
|
|
140
|
+
const event = makeEvent({ organizationId: '' });
|
|
141
|
+
await (0, vitest_1.expect)((0, usage_tracker_1.recordUsage)(supabase, event, silentLogger)).resolves.toBeUndefined();
|
|
142
|
+
});
|
|
143
|
+
// =========================================================================
|
|
144
|
+
// Fallback path: atomic RPC fails → insert + upsert
|
|
145
|
+
// =========================================================================
|
|
146
|
+
(0, vitest_1.it)('falls back to insert + upsert_usage_record when atomic RPC fails', async () => {
|
|
147
|
+
const supabase = makeSupabaseMock({ rpcAtomicError: 'function not found' });
|
|
148
|
+
await (0, usage_tracker_1.recordUsage)(supabase, makeEvent(), silentLogger);
|
|
149
|
+
// Should have tried atomic RPC first
|
|
150
|
+
(0, vitest_1.expect)(supabase.rpc).toHaveBeenCalledWith('record_usage_atomic', vitest_1.expect.any(Object));
|
|
151
|
+
// Should warn about falling back
|
|
152
|
+
(0, vitest_1.expect)(silentLogger.warn).toHaveBeenCalledWith('record_usage_atomic RPC failed, falling back to two-step', vitest_1.expect.any(Object));
|
|
153
|
+
// Should insert into usage_events
|
|
154
|
+
(0, vitest_1.expect)(supabase.from).toHaveBeenCalledWith('usage_events');
|
|
155
|
+
// Should look up subscription
|
|
156
|
+
(0, vitest_1.expect)(supabase.from).toHaveBeenCalledWith('organization_subscriptions');
|
|
157
|
+
// Should call upsert_usage_record RPC
|
|
158
|
+
(0, vitest_1.expect)(supabase.rpc).toHaveBeenCalledWith('upsert_usage_record', vitest_1.expect.objectContaining({
|
|
159
|
+
p_organization_id: 'org-123',
|
|
160
|
+
p_input_tokens: 1000,
|
|
161
|
+
p_output_tokens: 500,
|
|
162
|
+
}));
|
|
163
|
+
});
|
|
164
|
+
(0, vitest_1.it)('skips usage_records upsert when no subscription found (fallback path)', async () => {
|
|
165
|
+
const supabase = makeSupabaseMock({
|
|
166
|
+
rpcAtomicError: 'function not found',
|
|
167
|
+
subData: null,
|
|
168
|
+
subError: 'no rows',
|
|
169
|
+
});
|
|
170
|
+
await (0, usage_tracker_1.recordUsage)(supabase, makeEvent(), silentLogger);
|
|
171
|
+
// Atomic RPC was called (and failed)
|
|
172
|
+
(0, vitest_1.expect)(supabase.rpc).toHaveBeenCalledWith('record_usage_atomic', vitest_1.expect.any(Object));
|
|
173
|
+
// upsert_usage_record should NOT be called when no subscription
|
|
174
|
+
(0, vitest_1.expect)(supabase.rpc).not.toHaveBeenCalledWith('upsert_usage_record', vitest_1.expect.any(Object));
|
|
175
|
+
// Warning should be logged
|
|
176
|
+
(0, vitest_1.expect)(silentLogger.warn).toHaveBeenCalledWith('No subscription found, skipping usage_records upsert', vitest_1.expect.any(Object));
|
|
177
|
+
});
|
|
178
|
+
// =========================================================================
|
|
179
|
+
// Error resilience
|
|
180
|
+
// =========================================================================
|
|
181
|
+
(0, vitest_1.it)('never throws even if supabase insert fails (fallback path)', async () => {
|
|
182
|
+
const supabase = makeSupabaseMock({
|
|
183
|
+
rpcAtomicError: 'function not found',
|
|
184
|
+
insertError: 'DB connection failed',
|
|
185
|
+
});
|
|
186
|
+
// Should not throw
|
|
187
|
+
await (0, vitest_1.expect)((0, usage_tracker_1.recordUsage)(supabase, makeEvent(), silentLogger)).resolves.toBeUndefined();
|
|
188
|
+
// Error should be logged
|
|
189
|
+
(0, vitest_1.expect)(silentLogger.error).toHaveBeenCalled();
|
|
190
|
+
});
|
|
191
|
+
(0, vitest_1.it)('never throws even if both fallback RPCs fail', async () => {
|
|
192
|
+
const supabase = makeSupabaseMock({
|
|
193
|
+
rpcAtomicError: 'function not found',
|
|
194
|
+
rpcError: 'RPC not found',
|
|
195
|
+
upsertError: 'upsert failed',
|
|
196
|
+
});
|
|
197
|
+
await (0, vitest_1.expect)((0, usage_tracker_1.recordUsage)(supabase, makeEvent(), silentLogger)).resolves.toBeUndefined();
|
|
198
|
+
});
|
|
199
|
+
(0, vitest_1.it)('never throws if supabase throws an unexpected error', async () => {
|
|
200
|
+
const supabase = {
|
|
201
|
+
rpc: vitest_1.vi.fn().mockImplementation(() => {
|
|
202
|
+
throw new Error('Unexpected crash');
|
|
203
|
+
}),
|
|
204
|
+
from: vitest_1.vi.fn(),
|
|
205
|
+
};
|
|
206
|
+
await (0, vitest_1.expect)((0, usage_tracker_1.recordUsage)(supabase, makeEvent(), silentLogger)).resolves.toBeUndefined();
|
|
207
|
+
(0, vitest_1.expect)(silentLogger.error).toHaveBeenCalledWith('Unexpected error recording usage', vitest_1.expect.objectContaining({ error: 'Unexpected crash' }));
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
//# sourceMappingURL=usage-tracker.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"usage-tracker.test.js","sourceRoot":"","sources":["../../src/__tests__/usage-tracker.test.ts"],"names":[],"mappings":"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"}
|
package/dist/alerts.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { SupabaseClient, UsageAlertResult, BillingLogger } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Threshold at which a usage alert should be sent (80%).
|
|
4
|
+
*/
|
|
5
|
+
export declare const USAGE_ALERT_THRESHOLD = 80;
|
|
6
|
+
/**
|
|
7
|
+
* Check if an organization has crossed the usage alert threshold and
|
|
8
|
+
* whether an alert has already been sent this billing period.
|
|
9
|
+
*
|
|
10
|
+
* Returns `{ shouldSend, percentUsed, alreadySent }`.
|
|
11
|
+
* Email sending is NOT handled here — that stays in the frontend/service layer.
|
|
12
|
+
*
|
|
13
|
+
* If `shouldSend` is true, this function also marks `usage_alert_sent = true`
|
|
14
|
+
* on the usage record to ensure idempotency.
|
|
15
|
+
*
|
|
16
|
+
* Never throws — all errors are caught and logged.
|
|
17
|
+
*/
|
|
18
|
+
export declare function shouldSendUsageAlert(supabase: SupabaseClient, organizationId: string, logger?: BillingLogger): Promise<UsageAlertResult>;
|
|
19
|
+
//# sourceMappingURL=alerts.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"alerts.d.ts","sourceRoot":"","sources":["../src/alerts.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAW1E;;GAEG;AACH,eAAO,MAAM,qBAAqB,KAAK,CAAC;AAExC;;;;;;;;;;;GAWG;AACH,wBAAsB,oBAAoB,CACxC,QAAQ,EAAE,cAAc,EACxB,cAAc,EAAE,MAAM,EACtB,MAAM,GAAE,aAA6B,GACpC,OAAO,CAAC,gBAAgB,CAAC,CAwE3B"}
|
package/dist/alerts.js
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.USAGE_ALERT_THRESHOLD = void 0;
|
|
4
|
+
exports.shouldSendUsageAlert = shouldSendUsageAlert;
|
|
5
|
+
const pricing_1 = require("./pricing");
|
|
6
|
+
const plan_limits_1 = require("./plan-limits");
|
|
7
|
+
const defaultLogger = {
|
|
8
|
+
debug: () => { },
|
|
9
|
+
info: (msg, meta) => console.log(`[billing] ${msg}`, meta ?? ''),
|
|
10
|
+
warn: (msg, meta) => console.warn(`[billing] ${msg}`, meta ?? ''),
|
|
11
|
+
error: (msg, meta) => console.error(`[billing] ${msg}`, meta ?? ''),
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Threshold at which a usage alert should be sent (80%).
|
|
15
|
+
*/
|
|
16
|
+
exports.USAGE_ALERT_THRESHOLD = 80;
|
|
17
|
+
/**
|
|
18
|
+
* Check if an organization has crossed the usage alert threshold and
|
|
19
|
+
* whether an alert has already been sent this billing period.
|
|
20
|
+
*
|
|
21
|
+
* Returns `{ shouldSend, percentUsed, alreadySent }`.
|
|
22
|
+
* Email sending is NOT handled here — that stays in the frontend/service layer.
|
|
23
|
+
*
|
|
24
|
+
* If `shouldSend` is true, this function also marks `usage_alert_sent = true`
|
|
25
|
+
* on the usage record to ensure idempotency.
|
|
26
|
+
*
|
|
27
|
+
* Never throws — all errors are caught and logged.
|
|
28
|
+
*/
|
|
29
|
+
async function shouldSendUsageAlert(supabase, organizationId, logger = defaultLogger) {
|
|
30
|
+
try {
|
|
31
|
+
// 1. Get subscription + plan
|
|
32
|
+
const { data: sub } = await supabase
|
|
33
|
+
.from('organization_subscriptions')
|
|
34
|
+
.select(`
|
|
35
|
+
plan_id,
|
|
36
|
+
current_period_start,
|
|
37
|
+
current_period_end,
|
|
38
|
+
subscription_plans (
|
|
39
|
+
name
|
|
40
|
+
)
|
|
41
|
+
`)
|
|
42
|
+
.eq('organization_id', organizationId)
|
|
43
|
+
.single();
|
|
44
|
+
if (!sub) {
|
|
45
|
+
return { shouldSend: false, percentUsed: 0, alreadySent: false };
|
|
46
|
+
}
|
|
47
|
+
const plan = sub.subscription_plans;
|
|
48
|
+
const planLimits = (0, plan_limits_1.getPlanLimits)((0, plan_limits_1.normalizePlanName)(plan?.name));
|
|
49
|
+
if (planLimits.includedCredits <= 0) {
|
|
50
|
+
return { shouldSend: false, percentUsed: 0, alreadySent: false };
|
|
51
|
+
}
|
|
52
|
+
// 2. Get current usage
|
|
53
|
+
const { data: usage } = await supabase
|
|
54
|
+
.from('usage_records')
|
|
55
|
+
.select('usage_alert_sent')
|
|
56
|
+
.eq('organization_id', organizationId)
|
|
57
|
+
.eq('period_start', sub.current_period_start)
|
|
58
|
+
.maybeSingle();
|
|
59
|
+
if (!usage) {
|
|
60
|
+
return { shouldSend: false, percentUsed: 0, alreadySent: false };
|
|
61
|
+
}
|
|
62
|
+
const alreadySent = !!usage.usage_alert_sent;
|
|
63
|
+
// Credit-based threshold check (sole mechanism)
|
|
64
|
+
const costMicrodollars = usage.cost_microdollars ?? 0;
|
|
65
|
+
const creditsUsed = (0, pricing_1.microdollarsToCredits)(costMicrodollars);
|
|
66
|
+
const creditPercentUsed = planLimits.includedCredits > 0
|
|
67
|
+
? (creditsUsed / planLimits.includedCredits) * 100
|
|
68
|
+
: 0;
|
|
69
|
+
// Trigger alert only when credit threshold is crossed
|
|
70
|
+
const thresholdCrossed = creditPercentUsed >= exports.USAGE_ALERT_THRESHOLD;
|
|
71
|
+
if (!thresholdCrossed || alreadySent) {
|
|
72
|
+
return { shouldSend: false, percentUsed: creditPercentUsed, alreadySent };
|
|
73
|
+
}
|
|
74
|
+
// 3. Mark alert as sent (idempotency)
|
|
75
|
+
await supabase
|
|
76
|
+
.from('usage_records')
|
|
77
|
+
.update({ usage_alert_sent: true })
|
|
78
|
+
.eq('organization_id', organizationId)
|
|
79
|
+
.eq('period_start', sub.current_period_start);
|
|
80
|
+
return { shouldSend: true, percentUsed: creditPercentUsed, alreadySent: false };
|
|
81
|
+
}
|
|
82
|
+
catch (err) {
|
|
83
|
+
logger.error('Failed to check usage alert', {
|
|
84
|
+
orgId: organizationId,
|
|
85
|
+
error: err instanceof Error ? err.message : String(err),
|
|
86
|
+
});
|
|
87
|
+
return { shouldSend: false, percentUsed: 0, alreadySent: false };
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
//# sourceMappingURL=alerts.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"alerts.js","sourceRoot":"","sources":["../src/alerts.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { SupabaseClient, ProjectLimitResult, BillingLogger } from '../types';
|
|
2
|
+
/**
|
|
3
|
+
* Check whether an organization is allowed to create a new project.
|
|
4
|
+
*/
|
|
5
|
+
export declare function checkProjectLimit(supabase: SupabaseClient, organizationId: string, logger?: BillingLogger): Promise<ProjectLimitResult>;
|
|
6
|
+
//# sourceMappingURL=project-guard.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"project-guard.d.ts","sourceRoot":"","sources":["../../src/guards/project-guard.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,kBAAkB,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAU7E;;GAEG;AACH,wBAAsB,iBAAiB,CACrC,QAAQ,EAAE,cAAc,EACxB,cAAc,EAAE,MAAM,EACtB,MAAM,GAAE,aAA6B,GACpC,OAAO,CAAC,kBAAkB,CAAC,CA+C7B"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.checkProjectLimit = checkProjectLimit;
|
|
4
|
+
const plan_limits_1 = require("../plan-limits");
|
|
5
|
+
const defaultLogger = {
|
|
6
|
+
debug: () => { },
|
|
7
|
+
info: (msg, meta) => console.log(`[billing] ${msg}`, meta ?? ''),
|
|
8
|
+
warn: (msg, meta) => console.warn(`[billing] ${msg}`, meta ?? ''),
|
|
9
|
+
error: (msg, meta) => console.error(`[billing] ${msg}`, meta ?? ''),
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Check whether an organization is allowed to create a new project.
|
|
13
|
+
*/
|
|
14
|
+
async function checkProjectLimit(supabase, organizationId, logger = defaultLogger) {
|
|
15
|
+
// 1. Fetch subscription + plan
|
|
16
|
+
const { data: subscription } = await supabase
|
|
17
|
+
.from('organization_subscriptions')
|
|
18
|
+
.select(`
|
|
19
|
+
plan_id,
|
|
20
|
+
subscription_plans ( id, name )
|
|
21
|
+
`)
|
|
22
|
+
.eq('organization_id', organizationId)
|
|
23
|
+
.single();
|
|
24
|
+
const plan = subscription?.subscription_plans;
|
|
25
|
+
const planName = (0, plan_limits_1.normalizePlanName)(plan?.name);
|
|
26
|
+
const displayName = plan?.name ?? 'Free';
|
|
27
|
+
const limits = (0, plan_limits_1.getPlanLimits)(planName);
|
|
28
|
+
const maxProjects = limits.projects;
|
|
29
|
+
// Infinity means unlimited
|
|
30
|
+
if (!isFinite(maxProjects)) {
|
|
31
|
+
return { allowed: true, currentProjects: 0, maxProjects };
|
|
32
|
+
}
|
|
33
|
+
// 2. Count current projects (non-archived)
|
|
34
|
+
const { count, error } = await supabase
|
|
35
|
+
.from('projects')
|
|
36
|
+
.select('id', { count: 'exact', head: true })
|
|
37
|
+
.eq('organization_id', organizationId)
|
|
38
|
+
.neq('status', 'archived');
|
|
39
|
+
if (error) {
|
|
40
|
+
logger.error('Failed to count projects', { error: error.message });
|
|
41
|
+
// Fail open
|
|
42
|
+
return { allowed: true, currentProjects: 0, maxProjects };
|
|
43
|
+
}
|
|
44
|
+
const currentProjects = count ?? 0;
|
|
45
|
+
const allowed = currentProjects < maxProjects;
|
|
46
|
+
return {
|
|
47
|
+
allowed,
|
|
48
|
+
currentProjects,
|
|
49
|
+
maxProjects,
|
|
50
|
+
reason: allowed
|
|
51
|
+
? undefined
|
|
52
|
+
: `Your ${displayName} plan allows ${maxProjects} project${maxProjects === 1 ? '' : 's'}. ` +
|
|
53
|
+
`You have ${currentProjects}. Archive existing projects or upgrade your plan.`,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
//# sourceMappingURL=project-guard.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"project-guard.js","sourceRoot":"","sources":["../../src/guards/project-guard.ts"],"names":[],"mappings":";;AAaA,8CAmDC;AA/DD,gDAAkE;AAElE,MAAM,aAAa,GAAkB;IACnC,KAAK,EAAE,GAAG,EAAE,GAAE,CAAC;IACf,IAAI,EAAE,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,aAAa,GAAG,EAAE,EAAE,IAAI,IAAI,EAAE,CAAC;IAChE,IAAI,EAAE,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,aAAa,GAAG,EAAE,EAAE,IAAI,IAAI,EAAE,CAAC;IACjE,KAAK,EAAE,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,aAAa,GAAG,EAAE,EAAE,IAAI,IAAI,EAAE,CAAC;CACpE,CAAC;AAEF;;GAEG;AACI,KAAK,UAAU,iBAAiB,CACrC,QAAwB,EACxB,cAAsB,EACtB,SAAwB,aAAa;IAErC,+BAA+B;IAC/B,MAAM,EAAE,IAAI,EAAE,YAAY,EAAE,GAAG,MAAM,QAAQ;SAC1C,IAAI,CAAC,4BAA4B,CAAC;SAClC,MAAM,CAAC;;;KAGP,CAAC;SACD,EAAE,CAAC,iBAAiB,EAAE,cAAc,CAAC;SACrC,MAAM,EAAE,CAAC;IAEZ,MAAM,IAAI,GAAG,YAAY,EAAE,kBAAoE,CAAC;IAChG,MAAM,QAAQ,GAAG,IAAA,+BAAiB,EAAC,IAAI,EAAE,IAAI,CAAC,CAAC;IAC/C,MAAM,WAAW,GAAG,IAAI,EAAE,IAAI,IAAI,MAAM,CAAC;IACzC,MAAM,MAAM,GAAG,IAAA,2BAAa,EAAC,QAAQ,CAAC,CAAC;IACvC,MAAM,WAAW,GAAG,MAAM,CAAC,QAAQ,CAAC;IAEpC,2BAA2B;IAC3B,IAAI,CAAC,QAAQ,CAAC,WAAW,CAAC,EAAE,CAAC;QAC3B,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,eAAe,EAAE,CAAC,EAAE,WAAW,EAAE,CAAC;IAC5D,CAAC;IAED,2CAA2C;IAC3C,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,GAAG,MAAM,QAAQ;SACpC,IAAI,CAAC,UAAU,CAAC;SAChB,MAAM,CAAC,IAAI,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;SAC5C,EAAE,CAAC,iBAAiB,EAAE,cAAc,CAAC;SACrC,GAAG,CAAC,QAAQ,EAAE,UAAU,CAAC,CAAC;IAE7B,IAAI,KAAK,EAAE,CAAC;QACV,MAAM,CAAC,KAAK,CAAC,0BAA0B,EAAE,EAAE,KAAK,EAAE,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC;QACnE,YAAY;QACZ,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,eAAe,EAAE,CAAC,EAAE,WAAW,EAAE,CAAC;IAC5D,CAAC;IAED,MAAM,eAAe,GAAG,KAAK,IAAI,CAAC,CAAC;IACnC,MAAM,OAAO,GAAG,eAAe,GAAG,WAAW,CAAC;IAE9C,OAAO;QACL,OAAO;QACP,eAAe;QACf,WAAW;QACX,MAAM,EAAE,OAAO;YACb,CAAC,CAAC,SAAS;YACX,CAAC,CAAC,QAAQ,WAAW,gBAAgB,WAAW,WAAW,WAAW,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,IAAI;gBACzF,YAAY,eAAe,mDAAmD;KACnF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { SupabaseClient, ResolutionLimitResult, BillingLogger } from '../types';
|
|
2
|
+
/**
|
|
3
|
+
* Check whether an organization's plan allows a given export resolution.
|
|
4
|
+
*/
|
|
5
|
+
export declare function checkResolutionLimit(supabase: SupabaseClient, organizationId: string, requestedResolution: string, logger?: BillingLogger): Promise<ResolutionLimitResult>;
|
|
6
|
+
//# sourceMappingURL=resolution-guard.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolution-guard.d.ts","sourceRoot":"","sources":["../../src/guards/resolution-guard.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,qBAAqB,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAUhF;;GAEG;AACH,wBAAsB,oBAAoB,CACxC,QAAQ,EAAE,cAAc,EACxB,cAAc,EAAE,MAAM,EACtB,mBAAmB,EAAE,MAAM,EAC3B,MAAM,GAAE,aAA6B,GACpC,OAAO,CAAC,qBAAqB,CAAC,CA4BhC"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.checkResolutionLimit = checkResolutionLimit;
|
|
4
|
+
const plan_limits_1 = require("../plan-limits");
|
|
5
|
+
const defaultLogger = {
|
|
6
|
+
debug: () => { },
|
|
7
|
+
info: (msg, meta) => console.log(`[billing] ${msg}`, meta ?? ''),
|
|
8
|
+
warn: (msg, meta) => console.warn(`[billing] ${msg}`, meta ?? ''),
|
|
9
|
+
error: (msg, meta) => console.error(`[billing] ${msg}`, meta ?? ''),
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Check whether an organization's plan allows a given export resolution.
|
|
13
|
+
*/
|
|
14
|
+
async function checkResolutionLimit(supabase, organizationId, requestedResolution, logger = defaultLogger) {
|
|
15
|
+
// 1. Fetch subscription + plan
|
|
16
|
+
const { data: subscription } = await supabase
|
|
17
|
+
.from('organization_subscriptions')
|
|
18
|
+
.select(`
|
|
19
|
+
plan_id,
|
|
20
|
+
subscription_plans ( id, name )
|
|
21
|
+
`)
|
|
22
|
+
.eq('organization_id', organizationId)
|
|
23
|
+
.single();
|
|
24
|
+
const plan = subscription?.subscription_plans;
|
|
25
|
+
const planName = (0, plan_limits_1.normalizePlanName)(plan?.name);
|
|
26
|
+
const displayName = plan?.name ?? 'Free';
|
|
27
|
+
const limits = (0, plan_limits_1.getPlanLimits)(planName);
|
|
28
|
+
const maxResolution = limits.maxResolution;
|
|
29
|
+
const allowed = (0, plan_limits_1.isResolutionAllowed)(requestedResolution, maxResolution);
|
|
30
|
+
return {
|
|
31
|
+
allowed,
|
|
32
|
+
requestedResolution,
|
|
33
|
+
maxResolution,
|
|
34
|
+
reason: allowed
|
|
35
|
+
? undefined
|
|
36
|
+
: `Your ${displayName} plan supports up to ${maxResolution} resolution. ` +
|
|
37
|
+
`Upgrade your plan to export at ${requestedResolution}.`,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=resolution-guard.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolution-guard.js","sourceRoot":"","sources":["../../src/guards/resolution-guard.ts"],"names":[],"mappings":";;AAaA,oDAiCC;AA7CD,gDAAuF;AAEvF,MAAM,aAAa,GAAkB;IACnC,KAAK,EAAE,GAAG,EAAE,GAAE,CAAC;IACf,IAAI,EAAE,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,aAAa,GAAG,EAAE,EAAE,IAAI,IAAI,EAAE,CAAC;IAChE,IAAI,EAAE,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,aAAa,GAAG,EAAE,EAAE,IAAI,IAAI,EAAE,CAAC;IACjE,KAAK,EAAE,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,aAAa,GAAG,EAAE,EAAE,IAAI,IAAI,EAAE,CAAC;CACpE,CAAC;AAEF;;GAEG;AACI,KAAK,UAAU,oBAAoB,CACxC,QAAwB,EACxB,cAAsB,EACtB,mBAA2B,EAC3B,SAAwB,aAAa;IAErC,+BAA+B;IAC/B,MAAM,EAAE,IAAI,EAAE,YAAY,EAAE,GAAG,MAAM,QAAQ;SAC1C,IAAI,CAAC,4BAA4B,CAAC;SAClC,MAAM,CAAC;;;KAGP,CAAC;SACD,EAAE,CAAC,iBAAiB,EAAE,cAAc,CAAC;SACrC,MAAM,EAAE,CAAC;IAEZ,MAAM,IAAI,GAAG,YAAY,EAAE,kBAAoE,CAAC;IAChG,MAAM,QAAQ,GAAG,IAAA,+BAAiB,EAAC,IAAI,EAAE,IAAI,CAAC,CAAC;IAC/C,MAAM,WAAW,GAAG,IAAI,EAAE,IAAI,IAAI,MAAM,CAAC;IACzC,MAAM,MAAM,GAAG,IAAA,2BAAa,EAAC,QAAQ,CAAC,CAAC;IACvC,MAAM,aAAa,GAAG,MAAM,CAAC,aAAa,CAAC;IAE3C,MAAM,OAAO,GAAG,IAAA,iCAAmB,EAAC,mBAAmB,EAAE,aAAa,CAAC,CAAC;IAExE,OAAO;QACL,OAAO;QACP,mBAAmB;QACnB,aAAa;QACb,MAAM,EAAE,OAAO;YACb,CAAC,CAAC,SAAS;YACX,CAAC,CAAC,QAAQ,WAAW,wBAAwB,aAAa,eAAe;gBACvE,kCAAkC,mBAAmB,GAAG;KAC7D,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { SupabaseClient, SeatLimitResult, BillingLogger } from '../types';
|
|
2
|
+
/**
|
|
3
|
+
* Check whether an organization is allowed to add a new team member.
|
|
4
|
+
*
|
|
5
|
+
* Total allowed seats = plan base seats + additional_seats purchased.
|
|
6
|
+
*/
|
|
7
|
+
export declare function checkTeamSeatLimit(supabase: SupabaseClient, organizationId: string, logger?: BillingLogger): Promise<SeatLimitResult>;
|
|
8
|
+
//# sourceMappingURL=seat-guard.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"seat-guard.d.ts","sourceRoot":"","sources":["../../src/guards/seat-guard.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAU1E;;;;GAIG;AACH,wBAAsB,kBAAkB,CACtC,QAAQ,EAAE,cAAc,EACxB,cAAc,EAAE,MAAM,EACtB,MAAM,GAAE,aAA6B,GACpC,OAAO,CAAC,eAAe,CAAC,CA2C1B"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.checkTeamSeatLimit = checkTeamSeatLimit;
|
|
4
|
+
const plan_limits_1 = require("../plan-limits");
|
|
5
|
+
const defaultLogger = {
|
|
6
|
+
debug: () => { },
|
|
7
|
+
info: (msg, meta) => console.log(`[billing] ${msg}`, meta ?? ''),
|
|
8
|
+
warn: (msg, meta) => console.warn(`[billing] ${msg}`, meta ?? ''),
|
|
9
|
+
error: (msg, meta) => console.error(`[billing] ${msg}`, meta ?? ''),
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Check whether an organization is allowed to add a new team member.
|
|
13
|
+
*
|
|
14
|
+
* Total allowed seats = plan base seats + additional_seats purchased.
|
|
15
|
+
*/
|
|
16
|
+
async function checkTeamSeatLimit(supabase, organizationId, logger = defaultLogger) {
|
|
17
|
+
// 1. Fetch subscription + plan + additional_seats
|
|
18
|
+
const { data: subscription } = await supabase
|
|
19
|
+
.from('organization_subscriptions')
|
|
20
|
+
.select(`
|
|
21
|
+
plan_id,
|
|
22
|
+
additional_seats,
|
|
23
|
+
subscription_plans ( id, name )
|
|
24
|
+
`)
|
|
25
|
+
.eq('organization_id', organizationId)
|
|
26
|
+
.single();
|
|
27
|
+
const plan = subscription?.subscription_plans;
|
|
28
|
+
const planName = (0, plan_limits_1.normalizePlanName)(plan?.name);
|
|
29
|
+
const displayName = plan?.name ?? 'Free';
|
|
30
|
+
const limits = (0, plan_limits_1.getPlanLimits)(planName);
|
|
31
|
+
const additionalSeats = subscription?.additional_seats ?? 0;
|
|
32
|
+
const maxSeats = limits.seats + additionalSeats;
|
|
33
|
+
// 2. Count current members
|
|
34
|
+
const { count, error } = await supabase
|
|
35
|
+
.from('organization_members')
|
|
36
|
+
.select('id', { count: 'exact', head: true })
|
|
37
|
+
.eq('organization_id', organizationId);
|
|
38
|
+
if (error) {
|
|
39
|
+
logger.error('Failed to count organization members', { error: error.message });
|
|
40
|
+
// Fail open
|
|
41
|
+
return { allowed: true, currentSeats: 0, maxSeats };
|
|
42
|
+
}
|
|
43
|
+
const currentSeats = count ?? 0;
|
|
44
|
+
const allowed = currentSeats < maxSeats;
|
|
45
|
+
return {
|
|
46
|
+
allowed,
|
|
47
|
+
currentSeats,
|
|
48
|
+
maxSeats,
|
|
49
|
+
reason: allowed
|
|
50
|
+
? undefined
|
|
51
|
+
: `Your ${displayName} plan allows ${maxSeats} team seat${maxSeats === 1 ? '' : 's'} (${limits.seats} included + ${additionalSeats} purchased). ` +
|
|
52
|
+
`You have ${currentSeats} members. Purchase additional seats or remove a member.`,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=seat-guard.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"seat-guard.js","sourceRoot":"","sources":["../../src/guards/seat-guard.ts"],"names":[],"mappings":";;AAeA,gDA+CC;AA7DD,gDAAkE;AAElE,MAAM,aAAa,GAAkB;IACnC,KAAK,EAAE,GAAG,EAAE,GAAE,CAAC;IACf,IAAI,EAAE,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,aAAa,GAAG,EAAE,EAAE,IAAI,IAAI,EAAE,CAAC;IAChE,IAAI,EAAE,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,aAAa,GAAG,EAAE,EAAE,IAAI,IAAI,EAAE,CAAC;IACjE,KAAK,EAAE,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,aAAa,GAAG,EAAE,EAAE,IAAI,IAAI,EAAE,CAAC;CACpE,CAAC;AAEF;;;;GAIG;AACI,KAAK,UAAU,kBAAkB,CACtC,QAAwB,EACxB,cAAsB,EACtB,SAAwB,aAAa;IAErC,kDAAkD;IAClD,MAAM,EAAE,IAAI,EAAE,YAAY,EAAE,GAAG,MAAM,QAAQ;SAC1C,IAAI,CAAC,4BAA4B,CAAC;SAClC,MAAM,CAAC;;;;KAIP,CAAC;SACD,EAAE,CAAC,iBAAiB,EAAE,cAAc,CAAC;SACrC,MAAM,EAAE,CAAC;IAEZ,MAAM,IAAI,GAAG,YAAY,EAAE,kBAAoE,CAAC;IAChG,MAAM,QAAQ,GAAG,IAAA,+BAAiB,EAAC,IAAI,EAAE,IAAI,CAAC,CAAC;IAC/C,MAAM,WAAW,GAAG,IAAI,EAAE,IAAI,IAAI,MAAM,CAAC;IACzC,MAAM,MAAM,GAAG,IAAA,2BAAa,EAAC,QAAQ,CAAC,CAAC;IACvC,MAAM,eAAe,GAAY,YAAoB,EAAE,gBAAgB,IAAI,CAAC,CAAC;IAC7E,MAAM,QAAQ,GAAG,MAAM,CAAC,KAAK,GAAG,eAAe,CAAC;IAEhD,2BAA2B;IAC3B,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,GAAG,MAAM,QAAQ;SACpC,IAAI,CAAC,sBAAsB,CAAC;SAC5B,MAAM,CAAC,IAAI,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;SAC5C,EAAE,CAAC,iBAAiB,EAAE,cAAc,CAAC,CAAC;IAEzC,IAAI,KAAK,EAAE,CAAC;QACV,MAAM,CAAC,KAAK,CAAC,sCAAsC,EAAE,EAAE,KAAK,EAAE,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC;QAC/E,YAAY;QACZ,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC;IACtD,CAAC;IAED,MAAM,YAAY,GAAG,KAAK,IAAI,CAAC,CAAC;IAChC,MAAM,OAAO,GAAG,YAAY,GAAG,QAAQ,CAAC;IAExC,OAAO;QACL,OAAO;QACP,YAAY;QACZ,QAAQ;QACR,MAAM,EAAE,OAAO;YACb,CAAC,CAAC,SAAS;YACX,CAAC,CAAC,QAAQ,WAAW,gBAAgB,QAAQ,aAAa,QAAQ,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,KAAK,MAAM,CAAC,KAAK,eAAe,eAAe,eAAe;gBAC/I,YAAY,YAAY,yDAAyD;KACtF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { SupabaseClient, SocialAccountLimitResult, BillingLogger } from '../types';
|
|
2
|
+
/**
|
|
3
|
+
* Check whether an organization is allowed to connect a new social media account.
|
|
4
|
+
*/
|
|
5
|
+
export declare function checkSocialAccountLimit(supabase: SupabaseClient, organizationId: string, logger?: BillingLogger): Promise<SocialAccountLimitResult>;
|
|
6
|
+
//# sourceMappingURL=social-guard.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"social-guard.d.ts","sourceRoot":"","sources":["../../src/guards/social-guard.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,wBAAwB,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAUnF;;GAEG;AACH,wBAAsB,uBAAuB,CAC3C,QAAQ,EAAE,cAAc,EACxB,cAAc,EAAE,MAAM,EACtB,MAAM,GAAE,aAA6B,GACpC,OAAO,CAAC,wBAAwB,CAAC,CA0CnC"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.checkSocialAccountLimit = checkSocialAccountLimit;
|
|
4
|
+
const plan_limits_1 = require("../plan-limits");
|
|
5
|
+
const defaultLogger = {
|
|
6
|
+
debug: () => { },
|
|
7
|
+
info: (msg, meta) => console.log(`[billing] ${msg}`, meta ?? ''),
|
|
8
|
+
warn: (msg, meta) => console.warn(`[billing] ${msg}`, meta ?? ''),
|
|
9
|
+
error: (msg, meta) => console.error(`[billing] ${msg}`, meta ?? ''),
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Check whether an organization is allowed to connect a new social media account.
|
|
13
|
+
*/
|
|
14
|
+
async function checkSocialAccountLimit(supabase, organizationId, logger = defaultLogger) {
|
|
15
|
+
// 1. Fetch subscription + plan
|
|
16
|
+
const { data: subscription } = await supabase
|
|
17
|
+
.from('organization_subscriptions')
|
|
18
|
+
.select(`
|
|
19
|
+
plan_id,
|
|
20
|
+
subscription_plans ( id, name )
|
|
21
|
+
`)
|
|
22
|
+
.eq('organization_id', organizationId)
|
|
23
|
+
.single();
|
|
24
|
+
const plan = subscription?.subscription_plans;
|
|
25
|
+
const planName = (0, plan_limits_1.normalizePlanName)(plan?.name);
|
|
26
|
+
const displayName = plan?.name ?? 'Free';
|
|
27
|
+
const limits = (0, plan_limits_1.getPlanLimits)(planName);
|
|
28
|
+
const maxAccounts = limits.socialAccounts;
|
|
29
|
+
// 2. Count current connected accounts
|
|
30
|
+
const { count, error } = await supabase
|
|
31
|
+
.from('social_media_accounts')
|
|
32
|
+
.select('id', { count: 'exact', head: true })
|
|
33
|
+
.eq('organization_id', organizationId)
|
|
34
|
+
.eq('is_active', true);
|
|
35
|
+
if (error) {
|
|
36
|
+
logger.error('Failed to count social accounts', { error: error.message });
|
|
37
|
+
// Fail open
|
|
38
|
+
return { allowed: true, currentAccounts: 0, maxAccounts };
|
|
39
|
+
}
|
|
40
|
+
const currentAccounts = count ?? 0;
|
|
41
|
+
const allowed = currentAccounts < maxAccounts;
|
|
42
|
+
return {
|
|
43
|
+
allowed,
|
|
44
|
+
currentAccounts,
|
|
45
|
+
maxAccounts,
|
|
46
|
+
reason: allowed
|
|
47
|
+
? undefined
|
|
48
|
+
: `Your ${displayName} plan allows ${maxAccounts} social account${maxAccounts === 1 ? '' : 's'}. ` +
|
|
49
|
+
`You have ${currentAccounts} connected. Disconnect an account or upgrade your plan.`,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=social-guard.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"social-guard.js","sourceRoot":"","sources":["../../src/guards/social-guard.ts"],"names":[],"mappings":";;AAaA,0DA8CC;AA1DD,gDAAkE;AAElE,MAAM,aAAa,GAAkB;IACnC,KAAK,EAAE,GAAG,EAAE,GAAE,CAAC;IACf,IAAI,EAAE,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,aAAa,GAAG,EAAE,EAAE,IAAI,IAAI,EAAE,CAAC;IAChE,IAAI,EAAE,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,aAAa,GAAG,EAAE,EAAE,IAAI,IAAI,EAAE,CAAC;IACjE,KAAK,EAAE,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,aAAa,GAAG,EAAE,EAAE,IAAI,IAAI,EAAE,CAAC;CACpE,CAAC;AAEF;;GAEG;AACI,KAAK,UAAU,uBAAuB,CAC3C,QAAwB,EACxB,cAAsB,EACtB,SAAwB,aAAa;IAErC,+BAA+B;IAC/B,MAAM,EAAE,IAAI,EAAE,YAAY,EAAE,GAAG,MAAM,QAAQ;SAC1C,IAAI,CAAC,4BAA4B,CAAC;SAClC,MAAM,CAAC;;;KAGP,CAAC;SACD,EAAE,CAAC,iBAAiB,EAAE,cAAc,CAAC;SACrC,MAAM,EAAE,CAAC;IAEZ,MAAM,IAAI,GAAG,YAAY,EAAE,kBAAoE,CAAC;IAChG,MAAM,QAAQ,GAAG,IAAA,+BAAiB,EAAC,IAAI,EAAE,IAAI,CAAC,CAAC;IAC/C,MAAM,WAAW,GAAG,IAAI,EAAE,IAAI,IAAI,MAAM,CAAC;IACzC,MAAM,MAAM,GAAG,IAAA,2BAAa,EAAC,QAAQ,CAAC,CAAC;IACvC,MAAM,WAAW,GAAG,MAAM,CAAC,cAAc,CAAC;IAE1C,sCAAsC;IACtC,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,GAAG,MAAM,QAAQ;SACpC,IAAI,CAAC,uBAAuB,CAAC;SAC7B,MAAM,CAAC,IAAI,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;SAC5C,EAAE,CAAC,iBAAiB,EAAE,cAAc,CAAC;SACrC,EAAE,CAAC,WAAW,EAAE,IAAI,CAAC,CAAC;IAEzB,IAAI,KAAK,EAAE,CAAC;QACV,MAAM,CAAC,KAAK,CAAC,iCAAiC,EAAE,EAAE,KAAK,EAAE,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC;QAC1E,YAAY;QACZ,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,eAAe,EAAE,CAAC,EAAE,WAAW,EAAE,CAAC;IAC5D,CAAC;IAED,MAAM,eAAe,GAAG,KAAK,IAAI,CAAC,CAAC;IACnC,MAAM,OAAO,GAAG,eAAe,GAAG,WAAW,CAAC;IAE9C,OAAO;QACL,OAAO;QACP,eAAe;QACf,WAAW;QACX,MAAM,EAAE,OAAO;YACb,CAAC,CAAC,SAAS;YACX,CAAC,CAAC,QAAQ,WAAW,gBAAgB,WAAW,kBAAkB,WAAW,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,IAAI;gBAChG,YAAY,eAAe,yDAAyD;KACzF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { SupabaseClient, StorageQuotaResult, BillingLogger } from '../types';
|
|
2
|
+
/**
|
|
3
|
+
* Check whether an organization has enough storage quota remaining to
|
|
4
|
+
* accept a file of the given size.
|
|
5
|
+
*
|
|
6
|
+
* Storage usage is calculated via the `get_org_storage_usage_bytes` RPC
|
|
7
|
+
* (server-side SUM in Postgres). Falls back to client-side summation of
|
|
8
|
+
* `organization_files.metadata->file_size` if the RPC is not available.
|
|
9
|
+
*
|
|
10
|
+
* On error, fails OPEN (allows upload) since storage quota is a
|
|
11
|
+
* cost-protection measure, not a security boundary.
|
|
12
|
+
*/
|
|
13
|
+
export declare function checkStorageQuota(supabase: SupabaseClient, organizationId: string, fileSizeBytes: number, logger?: BillingLogger): Promise<StorageQuotaResult>;
|
|
14
|
+
//# sourceMappingURL=storage-guard.d.ts.map
|