learning_ai_common_plat/services/extraction-service/src/modules/extract/usage.ts
saravanakumardb1 9c8a3169dc feat(extraction): Phase 5 caching + cost controls (5.1-5.6)
- 5.1: Python sidecar LRU cache (cache.py) with configurable TTL + max size
- 5.2: Fastify-level cache with X-Extraction-Cache HIT/MISS header + /extract/cache-stats
- 5.3-5.5: Per-user daily quota (free=10, pro=100, enterprise=unlimited) with 429 response
- 5.6: GET /extract/usage endpoint for admin usage reporting
- Both Python + TS caches use sha256(taskId:modelId:text) keys
- 46 TS tests + 29 Python tests still passing
2026-02-14 14:02:21 -08:00

115 lines
2.9 KiB
TypeScript

/**
* Per-user daily extraction quota enforcement.
*
* Plan tiers:
* free: 10 extractions/day
* pro: 100 extractions/day
* enterprise: unlimited
*
* Usage tracked in Cosmos `extraction_usage` container (partition: /userId).
*/
import { z } from 'zod';
// ── Quota tiers ──────────────────────────────────────────────────
const PLAN_QUOTAS: Record<string, number> = {
free: 10,
pro: 100,
enterprise: Infinity,
};
export function getQuota(plan: string): number {
return PLAN_QUOTAS[plan] ?? PLAN_QUOTAS.free;
}
// ── Usage document schema ────────────────────────────────────────
export const ExtractionUsageSchema = z.object({
id: z.string(),
userId: z.string(),
productId: z.string(),
date: z.string(), // YYYY-MM-DD
count: z.number().int().min(0),
plan: z.string(),
updatedAt: z.string(),
});
export type ExtractionUsage = z.infer<typeof ExtractionUsageSchema>;
// ── In-memory usage tracker (no Cosmos dependency for now) ───────
const usageStore = new Map<string, { count: number; date: string }>();
function todayKey(): string {
return new Date().toISOString().slice(0, 10);
}
function storeKey(userId: string): string {
return `${userId}:${todayKey()}`;
}
/**
* Check if user is within their daily quota.
* Returns { allowed, remaining, limit, used }.
*/
export function checkQuota(
userId: string,
plan: string = 'free'
): { allowed: boolean; remaining: number; limit: number; used: number } {
const limit = getQuota(plan);
if (limit === Infinity) {
return { allowed: true, remaining: Infinity, limit, used: 0 };
}
const key = storeKey(userId);
const entry = usageStore.get(key);
const today = todayKey();
// Reset if new day
const used = entry && entry.date === today ? entry.count : 0;
const remaining = Math.max(0, limit - used);
return { allowed: used < limit, remaining, limit, used };
}
/**
* Increment usage counter for user. Call after successful extraction.
*/
export function incrementUsage(userId: string, _plan: string = 'free'): void {
const key = storeKey(userId);
const today = todayKey();
const entry = usageStore.get(key);
if (entry && entry.date === today) {
entry.count++;
} else {
usageStore.set(key, { count: 1, date: today });
}
}
/**
* Get usage summary for a user (for the usage reporting endpoint).
*/
export function getUsageSummary(
userId: string,
plan: string = 'free'
): {
userId: string;
date: string;
used: number;
limit: number;
remaining: number;
plan: string;
} {
const { used, limit, remaining } = checkQuota(userId, plan);
return {
userId,
date: todayKey(),
used,
limit: limit === Infinity ? -1 : limit,
remaining: remaining === Infinity ? -1 : remaining,
plan,
};
}