import { PrismaClient } from '@prisma/client'; import { GoogleGenAI } from "@google/genai"; const prisma = new PrismaClient() as any; // COST CONSTANTS (USD) const COSTS = { GENERATE_PROMPT: 0.001, // Gemini Pro Text GENERATE_MASTER: 0.040, // Imagen 3 (High Res) GENERATE_VARIANT: 0.020, // Imagen 3 (Standard/Fast) GENERATE_MOCKUP: 0.020, // Imagen 3 (Standard/Fast) REFINE_PROJECT: 0.040, // Imagen 3 (High Res) - treat same as Master MOCKUP_REMOVAL: 0.005 // Background Removal (Hypothetical) }; // CREDIT PRICES const PRICES = { GENERATE_PROMPT: 1, GENERATE_MASTER: 10, GENERATE_VARIANT: 5, GENERATE_MOCKUP: 5, REFINE_PROJECT: 10, // Same as Master MOCKUP_REMOVAL: 2 }; export type ActionType = keyof typeof COSTS; // CACHE: In-memory store for pricing config to reduce DB hits // Default TTL: 60 seconds let configCache: { [key: string]: number } | null = null; let cacheExpiry = 0; const CACHE_TTL_MS = 60 * 1000; async function getPricing(action: ActionType): Promise<{ cost: number, credits: number }> { const now = Date.now(); // 1. Refresh Cache if expired if (!configCache || now > cacheExpiry) { try { const configs = await prisma.systemConfig.findMany(); configCache = {}; // Populate cache configs.forEach((c: any) => { const val = parseFloat(c.value); if (!isNaN(val)) { configCache![c.key] = val; } }); cacheExpiry = now + CACHE_TTL_MS; // console.log("[UsageService] Pricing cache refreshed."); } catch (e) { console.error("[UsageService] Failed to fetch system config, using defaults.", e); // If DB fails, fallback to empty cache (which triggers defaults below) if (!configCache) configCache = {}; } } // 2. Resolve Values (DB > Default) // Keys in DB: COST_GENERATE_MASTER, PRICE_GENERATE_MASTER const costKey = `COST_${action}`; const priceKey = `PRICE_${action}`; const cost = (configCache && configCache[costKey] !== undefined) ? configCache[costKey] : COSTS[action]; const credits = (configCache && configCache[priceKey] !== undefined) ? configCache[priceKey] : PRICES[action]; return { cost, credits }; } export const usageService = { /** * Deducts credits using DYNAMIC pricing. */ async deductCredits(userId: string, action: ActionType) { const { cost, credits } = await getPricing(action); return await prisma.$transaction(async (tx: any) => { const user = await tx.user.findUnique({ where: { id: userId } }); if (!user) throw new Error("User not found"); // ADMIN OVERRIDE: Unlimited Credits // God Mode: Admins bypass all checks. if (user.role === 'ADMIN' || user.role === 'VIP') { console.log(`[UsageService] ⚡ GOD MODE: User is ${user.role}, bypassing credit deduction.`); return user; } if (user.credits < credits) { // Determine if we should show the 'cost' in error message throw new Error(`Insufficient credits for ${action}. Needed: ${credits}, Balance: ${user.credits}`); } const updatedUser = await tx.user.update({ where: { id: userId }, data: { credits: { decrement: credits }, totalCost: { increment: cost } } }); await tx.usageLog.create({ data: { userId, action, cost, credits } }); return updatedUser; }); }, /** * Helper to get current price for UI (without deducting) */ async getActionPrice(action: ActionType) { return await getPricing(action); }, async recordPurchase(userId: string, amountUSD: number, creditsGiven: number) { return await prisma.$transaction(async (tx: any) => { await tx.user.update({ where: { id: userId }, data: { credits: { increment: creditsGiven }, totalRevenue: { increment: amountUSD } } }); await tx.transaction.create({ data: { userId, amount: amountUSD, credits: creditsGiven, type: "PURCHASE" } }); }); }, /** * Validates a Gemini API Key by making a minimal test call. */ async validateApiKey(apiKey: string): Promise<{ valid: boolean, error?: string, code?: string }> { if (!apiKey || apiKey.length < 20) return { valid: false, error: "API Key is too short or missing.", code: "INVALID_FORMAT" }; try { const genAI = new GoogleGenAI({ apiKey }); // Minimal token generation to test validity & quota await genAI.models.generateContent({ model: "gemini-3-flash-preview", // Updated to a confirmed available model contents: [{ role: "user", parts: [{ text: "Hi" }] }], config: { maxOutputTokens: 1 } }); return { valid: true }; } catch (error: any) { console.warn("[UsageService] API Key Validation Failed:", error.message); const msg = error.message || ""; if (msg.includes("403") || msg.includes("API key not valid")) { return { valid: false, error: "Invalid API Key. Please check characters.", code: "INVALID_KEY" }; } if (msg.includes("429") || msg.includes("quota")) { return { valid: false, error: "API Key Quota Exceeded. You may need to enable billing.", code: "QUOTA_EXCEEDED" }; } if (msg.includes("400")) { return { valid: false, error: "Bad Request. Key might be malformed.", code: "BAD_REQUEST" }; } return { valid: false, error: "Validator Error: " + msg, code: "UNKNOWN_ERROR" }; } } };