Files
sentryagent-idp/src/services/UsageService.ts
SentryAgent.ai Developer 26a56f84e1 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>
2026-04-02 10:51:36 +00:00

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);
}
}