Files
digicraft-be/services/usageService.ts
Fahri Can Seçer 80dcf4d04a
Some checks failed
Deploy Backend / deploy (push) Has been cancelled
main
2026-02-05 01:29:22 +03:00

181 lines
6.2 KiB
TypeScript

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" };
}
}
};