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:
@@ -19,6 +19,8 @@ import {
|
||||
rateLimitHitsTotal,
|
||||
dbPoolActiveConnections,
|
||||
dbPoolWaitingRequests,
|
||||
tenantApiCallsTotal,
|
||||
billingLimitRejectionsTotal,
|
||||
} from '../../../src/metrics/registry';
|
||||
|
||||
describe('metricsRegistry', () => {
|
||||
@@ -33,9 +35,9 @@ describe('metricsRegistry', () => {
|
||||
expect(metricsRegistry).not.toBe(register);
|
||||
});
|
||||
|
||||
it('contains exactly 12 metric entries', async () => {
|
||||
it('contains exactly 14 metric entries', async () => {
|
||||
const entries = await metricsRegistry.getMetricsAsJSON();
|
||||
expect(entries).toHaveLength(12);
|
||||
expect(entries).toHaveLength(14);
|
||||
});
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────
|
||||
@@ -54,6 +56,8 @@ describe('metricsRegistry', () => {
|
||||
'agentidp_rate_limit_hits_total',
|
||||
'agentidp_db_pool_active_connections',
|
||||
'agentidp_db_pool_waiting_requests',
|
||||
'agentidp_tenant_api_calls_total',
|
||||
'agentidp_billing_limit_rejections_total',
|
||||
])('registers metric "%s"', async (metricName) => {
|
||||
const entries = await metricsRegistry.getMetricsAsJSON();
|
||||
const names = entries.map((e) => e.name);
|
||||
@@ -200,4 +204,33 @@ describe('metricsRegistry', () => {
|
||||
expect(() => dbPoolWaitingRequests.set(2)).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('tenantApiCallsTotal', () => {
|
||||
it('has name agentidp_tenant_api_calls_total', () => {
|
||||
const metric = tenantApiCallsTotal as unknown as { name: string };
|
||||
expect(metric.name).toBe('agentidp_tenant_api_calls_total');
|
||||
});
|
||||
|
||||
it('increments with tenant_id label without throwing', () => {
|
||||
expect(() =>
|
||||
tenantApiCallsTotal.inc({ tenant_id: 'org-test-001' }),
|
||||
).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('billingLimitRejectionsTotal', () => {
|
||||
it('has name agentidp_billing_limit_rejections_total', () => {
|
||||
const metric = billingLimitRejectionsTotal as unknown as { name: string };
|
||||
expect(metric.name).toBe('agentidp_billing_limit_rejections_total');
|
||||
});
|
||||
|
||||
it('increments with tenant_id and limit_type labels without throwing', () => {
|
||||
expect(() =>
|
||||
billingLimitRejectionsTotal.inc({ tenant_id: 'org-test-001', limit_type: 'agent_limit' }),
|
||||
).not.toThrow();
|
||||
expect(() =>
|
||||
billingLimitRejectionsTotal.inc({ tenant_id: 'org-test-002', limit_type: 'api_limit' }),
|
||||
).not.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user