- 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>
74 lines
2.1 KiB
TypeScript
74 lines
2.1 KiB
TypeScript
/**
|
|
* 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);
|
|
}
|
|
}
|