This commit is contained in:
180
services/usageService.ts
Normal file
180
services/usageService.ts
Normal file
@@ -0,0 +1,180 @@
|
||||
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" };
|
||||
}
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user