feat(phase-4): WS6 — Billing & Usage Metering (Stripe, free tier enforcement)
- DB migration 023: tenant_subscriptions and usage_events tables - UsageMeteringMiddleware: in-memory counters, 60s flush to DB via UPSERT - FreeTierEnforcementMiddleware: 10 agents / 1,000 calls/day limits, Redis cache - UsageService: getDailyUsage and getActiveAgentCount - BillingService: Stripe checkout sessions, webhook verification, subscription status - POST /billing/checkout, POST /billing/webhook, GET /billing/usage endpoints - BILLING_ENABLED=false disables enforcement without breaking metering - Dashboard: Usage tab with Free Tier/Pro badges and metric cards - 19 unit tests passing across billing services and middleware Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
73
src/services/UsageService.ts
Normal file
73
src/services/UsageService.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
/**
|
||||
* Usage metering service for SentryAgent.ai AgentIdP.
|
||||
* Provides daily usage summaries and active agent counts per tenant.
|
||||
*/
|
||||
|
||||
import { Pool } from 'pg';
|
||||
|
||||
/**
|
||||
* Daily usage summary for a tenant.
|
||||
*/
|
||||
export interface IUsageSummary {
|
||||
/** The tenant (organization) UUID. */
|
||||
tenantId: string;
|
||||
/** Date string in YYYY-MM-DD format. */
|
||||
date: string;
|
||||
/** Number of API calls made on the given date. */
|
||||
apiCalls: number;
|
||||
/** Number of active (non-decommissioned) agents for the tenant. */
|
||||
agentCount: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Service for retrieving per-tenant usage data.
|
||||
* Reads from the `usage_events` and `agents` tables.
|
||||
*/
|
||||
export class UsageService {
|
||||
/**
|
||||
* @param pool - PostgreSQL connection pool.
|
||||
*/
|
||||
constructor(private readonly pool: Pool) {}
|
||||
|
||||
/**
|
||||
* Returns the daily usage summary for a tenant on a given date.
|
||||
* If no usage row exists for the date, apiCalls defaults to 0.
|
||||
*
|
||||
* @param tenantId - The tenant UUID.
|
||||
* @param date - Date string in 'YYYY-MM-DD' format.
|
||||
* @returns A resolved IUsageSummary with api_calls and agent count.
|
||||
*/
|
||||
async getDailyUsage(tenantId: string, date: string): Promise<IUsageSummary> {
|
||||
const usageResult = await this.pool.query<{ count: string }>(
|
||||
`SELECT COALESCE(SUM(count), 0) AS count
|
||||
FROM usage_events
|
||||
WHERE tenant_id = $1
|
||||
AND date = $2
|
||||
AND metric_type = 'api_calls'`,
|
||||
[tenantId, date],
|
||||
);
|
||||
|
||||
const agentCount = await this.getActiveAgentCount(tenantId);
|
||||
const apiCalls = parseInt(usageResult.rows[0]?.count ?? '0', 10);
|
||||
|
||||
return { tenantId, date, apiCalls, agentCount };
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of non-decommissioned agents for a tenant.
|
||||
*
|
||||
* @param tenantId - The tenant UUID.
|
||||
* @returns The count of active agents (status != 'decommissioned').
|
||||
*/
|
||||
async getActiveAgentCount(tenantId: string): Promise<number> {
|
||||
const result = await this.pool.query<{ count: string }>(
|
||||
`SELECT COUNT(*) AS count
|
||||
FROM agents
|
||||
WHERE organization_id = $1
|
||||
AND status != 'decommissioned'`,
|
||||
[tenantId],
|
||||
);
|
||||
|
||||
return parseInt(result.rows[0]?.count ?? '0', 10);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user