From 270defc17f7d0c5772fc4b56a0e2385e7df6e824 Mon Sep 17 00:00:00 2001 From: Test Date: Tue, 7 Apr 2026 14:32:03 +0100 Subject: [PATCH] Fix credit enforcement and pricing fallback --- app/api/location/currency/route.ts | 34 +++++++++ app/layout.tsx | 7 +- app/pricing/page.tsx | 13 ++-- lib/free-plan-credits.ts | 117 ++++++++++++++++++++--------- 4 files changed, 130 insertions(+), 41 deletions(-) create mode 100644 app/api/location/currency/route.ts diff --git a/app/api/location/currency/route.ts b/app/api/location/currency/route.ts new file mode 100644 index 0000000..299c4c4 --- /dev/null +++ b/app/api/location/currency/route.ts @@ -0,0 +1,34 @@ +import { NextResponse } from 'next/server' + +const FALLBACK_CURRENCY = 'USD' + +export async function GET() { + const controller = new AbortController() + const timeout = setTimeout(() => controller.abort(), 2500) + + try { + const response = await fetch('https://ipapi.co/json/', { + cache: 'no-store', + headers: { + Accept: 'application/json', + 'User-Agent': 'tera-pricing/1.0', + }, + signal: controller.signal, + }) + + if (!response.ok) { + return NextResponse.json({ currency: FALLBACK_CURRENCY, countryCode: '' }) + } + + const data = await response.json() + + return NextResponse.json({ + currency: typeof data?.currency === 'string' ? data.currency : FALLBACK_CURRENCY, + countryCode: typeof data?.country_code === 'string' ? data.country_code : '', + }) + } catch { + return NextResponse.json({ currency: FALLBACK_CURRENCY, countryCode: '' }) + } finally { + clearTimeout(timeout) + } +} diff --git a/app/layout.tsx b/app/layout.tsx index 38692ea..c11fbc4 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -5,6 +5,11 @@ import AppLayout from '@/components/AppLayout' import { Analytics } from '@vercel/analytics/react' import { ThemeProvider } from '@/components/ThemeProvider' +const shouldRenderAnalytics = + process.env.VERCEL === '1' || + process.env.VERCEL === 'true' || + Boolean(process.env.VERCEL_URL) + export const viewport = { width: 'device-width', initialScale: 1, @@ -39,7 +44,7 @@ export default function RootLayout({ children }: { children: ReactNode }) { {children} - + {shouldRenderAnalytics ? : null} diff --git a/app/pricing/page.tsx b/app/pricing/page.tsx index 6efa551..78943c8 100644 --- a/app/pricing/page.tsx +++ b/app/pricing/page.tsx @@ -92,13 +92,14 @@ export default function PricingPage() { useEffect(() => { const loadUserAndCurrency = async () => { try { - const response = await fetch('https://ipapi.co/json/') - const data = await response.json() - const currencyCode = data.currency || 'USD' - setCurrency(CURRENCY_CODES[currencyCode] || CURRENCY_CODES.USD) - setCountryCode(data.country_code || '') + const response = await fetch('/api/location/currency', { cache: 'no-store' }) + if (response.ok) { + const data = await response.json() + const currencyCode = data.currency || 'USD' + setCurrency(CURRENCY_CODES[currencyCode] || CURRENCY_CODES.USD) + setCountryCode(data.countryCode || '') + } } catch (error) { - console.error('Error fetching currency:', error) setCurrency(CURRENCY_CODES.USD) } diff --git a/lib/free-plan-credits.ts b/lib/free-plan-credits.ts index c320aa4..cfcc9e1 100644 --- a/lib/free-plan-credits.ts +++ b/lib/free-plan-credits.ts @@ -7,6 +7,7 @@ const PLAN_MONTHLY_CREDIT_CAPS: Record = { plus: 5000 } const RESET_INTERVAL_DAYS = 30 +const RESET_INTERVAL_MS = RESET_INTERVAL_DAYS * 24 * 60 * 60 * 1000 type CreditState = { used: number @@ -16,6 +17,12 @@ type CreditState = { plan: PlanType } +type UserCreditRecord = { + plan: PlanType + used: number + resetDate: Date | null +} + export function getFreePlanCreditCap() { return PLAN_MONTHLY_CREDIT_CAPS.free } @@ -24,61 +31,103 @@ export function getPlanCreditCap(plan: PlanType): number { return PLAN_MONTHLY_CREDIT_CAPS[plan] } -export async function getUserCreditsRemaining(userId: string): Promise { - const nextResetFromNow = () => { - const date = new Date() - date.setDate(date.getDate() + RESET_INTERVAL_DAYS) - return date.toISOString() +function getNextResetDate(from: Date = new Date()) { + const date = new Date(from) + date.setDate(date.getDate() + RESET_INTERVAL_DAYS) + return date +} + +function normalizePlan(plan: string | null | undefined): PlanType { + if (plan === 'pro' || plan === 'plus') { + return plan } - try { - const { data, error } = await supabaseServer - .from('users') - .select('subscription_plan, free_plan_credits_used, free_plan_credits_reset_date') - .eq('id', userId) - .single() + return 'free' +} - if (error || !data) { - return { used: 0, remaining: PLAN_MONTHLY_CREDIT_CAPS.free, total: PLAN_MONTHLY_CREDIT_CAPS.free, resetDate: nextResetFromNow(), plan: 'free' } - } +async function getUserCreditRecord(userId: string): Promise { + const { data, error } = await supabaseServer + .from('users') + .select('subscription_plan, free_plan_credits_used, free_plan_credits_reset_date') + .eq('id', userId) + .maybeSingle() - const plan = (data.subscription_plan || 'free') as PlanType - const total = getPlanCreditCap(plan) + if (error || !data) { + return null + } + + return { + plan: normalizePlan(data.subscription_plan), + used: Math.max(0, Number(data.free_plan_credits_used || 0)), + resetDate: data.free_plan_credits_reset_date ? new Date(data.free_plan_credits_reset_date) : null, + } +} +async function getUserPlan(userId: string): Promise { + const { data, error } = await supabaseServer + .from('users') + .select('subscription_plan') + .eq('id', userId) + .maybeSingle() + + if (error || !data) { + return 'free' + } + + return normalizePlan(data.subscription_plan) +} + +async function getSessionCreditUsage(userId: string, windowStart: Date): Promise { + const { data, error } = await supabaseServer + .from('chat_sessions') + .select('token_usage') + .eq('user_id', userId) + .gte('created_at', windowStart.toISOString()) + + if (error || !data) { + return 0 + } + + return data.reduce((sum: number, row: { token_usage?: number | null }) => sum + Math.max(0, Number(row.token_usage || 0)), 0) +} + +export async function getUserCreditsRemaining(userId: string): Promise { + try { const now = new Date() - const resetDate = data.free_plan_credits_reset_date ? new Date(data.free_plan_credits_reset_date) : null - if (!resetDate || now > resetDate) { - return { used: 0, remaining: total, total, resetDate: nextResetFromNow(), plan } - } + const record = await getUserCreditRecord(userId) + const plan = record?.plan ?? await getUserPlan(userId) + const total = getPlanCreditCap(plan) - const used = data.free_plan_credits_used || 0 + const activeResetDate = record?.resetDate && now <= record.resetDate + ? record.resetDate + : getNextResetDate(now) + const windowStart = new Date(activeResetDate.getTime() - RESET_INTERVAL_MS) + const sessionUsage = await getSessionCreditUsage(userId, windowStart) + const storedUsage = record?.resetDate && now <= record.resetDate ? record.used : 0 + const used = Math.max(storedUsage, sessionUsage) const remaining = Math.max(0, total - used) - return { used, remaining, total, resetDate: resetDate.toISOString(), plan } + return { used, remaining, total, resetDate: activeResetDate.toISOString(), plan } } catch (error) { - return { used: 0, remaining: PLAN_MONTHLY_CREDIT_CAPS.free, total: PLAN_MONTHLY_CREDIT_CAPS.free, resetDate: nextResetFromNow(), plan: 'free' } + const resetDate = getNextResetDate().toISOString() + return { used: 0, remaining: PLAN_MONTHLY_CREDIT_CAPS.free, total: PLAN_MONTHLY_CREDIT_CAPS.free, resetDate, plan: 'free' } } } export async function incrementUserCredits(userId: string, cost: number): Promise { try { - const { data, error } = await supabaseServer - .from('users') - .select('subscription_plan, free_plan_credits_used, free_plan_credits_reset_date') - .eq('id', userId) - .single() + const record = await getUserCreditRecord(userId) - if (error || !data) return false + if (!record) return true const now = new Date() - const resetDate = data.free_plan_credits_reset_date ? new Date(data.free_plan_credits_reset_date) : null + const resetDate = record.resetDate - let used = data.free_plan_credits_used || 0 + let used = record.used const updatePayload: { free_plan_credits_used: number; free_plan_credits_reset_date?: string } = { free_plan_credits_used: used } if (!resetDate || now > resetDate) { used = 0 - const nextReset = new Date() - nextReset.setDate(nextReset.getDate() + RESET_INTERVAL_DAYS) + const nextReset = getNextResetDate(now) updatePayload.free_plan_credits_reset_date = nextReset.toISOString() } @@ -91,6 +140,6 @@ export async function incrementUserCredits(userId: string, cost: number): Promis return !updateError } catch (error) { - return false + return true } }