- 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
115 lines
2.9 KiB
TypeScript
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,
|
|
};
|
|
}
|