- Activating Your Subscription
+ Finalizing Your Trial
- Please wait while we set up your new plan...
+ Almost ready! Setting up your account...
{retryCount > 0 && (
@@ -144,7 +155,7 @@ export default function CheckoutSuccess() {
Success!
- Your subscription has been activated. Taking you to your account...
+ Your trial has been activated. Taking you to your account...
)}
diff --git a/auto-analyst-frontend/components/CheckoutForm.tsx b/auto-analyst-frontend/components/CheckoutForm.tsx
index 195e56e0..3caca4c3 100644
--- a/auto-analyst-frontend/components/CheckoutForm.tsx
+++ b/auto-analyst-frontend/components/CheckoutForm.tsx
@@ -18,10 +18,10 @@ interface CheckoutFormProps {
interval: 'month' | 'year' | 'day'
clientSecret: string
isTrialSetup?: boolean
- subscriptionId?: string
+ setupIntentId?: string
}
-export default function CheckoutForm({ planName, amount, interval, clientSecret, isTrialSetup, subscriptionId }: CheckoutFormProps) {
+export default function CheckoutForm({ planName, amount, interval, clientSecret, isTrialSetup, setupIntentId }: CheckoutFormProps) {
const router = useRouter()
const { data: session } = useSession()
const stripe = useStripe()
@@ -41,35 +41,8 @@ export default function CheckoutForm({ planName, amount, interval, clientSecret,
setProcessing(true)
- // Check if this is a SetupIntent (for trials) by looking at the client secret
- const isSetupIntent = clientSecret?.startsWith('seti_') || isTrialSetup
-
- if (isSetupIntent) {
- // For trial subscriptions, use confirmSetup to collect payment method
- const { error: submitError, setupIntent } = await stripe.confirmSetup({
- elements,
- confirmParams: {
- return_url: `${window.location.origin}/checkout/success`,
- },
- redirect: 'if_required',
- })
-
- setProcessing(false)
-
- if (submitError) {
- setError(submitError.message || 'An error occurred when setting up your payment method')
- } else if (setupIntent && setupIntent.status === 'succeeded') {
- setError(null)
- setSucceeded(true)
-
- // Show success animation before redirecting
- setTimeout(() => {
- router.push(`/checkout/success?subscription_id=${subscriptionId}`)
- }, 1500)
- }
- } else {
- // For regular payments, use confirmPayment
- const { error: submitError, paymentIntent } = await stripe.confirmPayment({
+ // NEW FLOW: Always use confirmSetup for trial signups
+ const { error: submitError, setupIntent } = await stripe.confirmSetup({
elements,
confirmParams: {
return_url: `${window.location.origin}/checkout/success`,
@@ -77,21 +50,48 @@ export default function CheckoutForm({ planName, amount, interval, clientSecret,
redirect: 'if_required',
})
- setProcessing(false)
-
if (submitError) {
- setError(submitError.message || 'An error occurred when processing your payment')
- } else if (paymentIntent && (paymentIntent.status === 'succeeded' || paymentIntent.status === 'requires_capture')) {
- // For manual capture (trial), status will be 'requires_capture'
- // For normal payments, status will be 'succeeded'
+ setProcessing(false)
+ setError(submitError.message || 'An error occurred when setting up your payment method')
+ return
+ }
+
+ if (setupIntent && setupIntent.status === 'succeeded') {
setError(null)
+
+ // Now call our trial/start endpoint to create the subscription
+ try {
+ const response = await fetch('/api/trial/start', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ setupIntentId: setupIntent.id,
+ planName,
+ interval,
+ amount
+ }),
+ })
+
+ const data = await response.json()
+
+ if (!response.ok) {
+ throw new Error(data.error || 'Failed to start trial')
+ }
+
setSucceeded(true)
- // Show success animation for a second before redirecting
+ // Show success animation before redirecting
setTimeout(() => {
- router.push(`/checkout/success?payment_intent=${paymentIntent.id}`)
+ router.push(`/checkout/success?subscription_id=${data.subscriptionId}`)
}, 1500)
+
+ } catch (trialError: any) {
+ setProcessing(false)
+ setError(trialError.message || 'Failed to start trial after payment setup')
}
+ } else {
+ setProcessing(false)
+ setError('Payment setup was not completed successfully')
}
}
diff --git a/auto-analyst-frontend/docs/api-endpoints.md b/auto-analyst-frontend/docs/api-endpoints.md
new file mode 100644
index 00000000..0519ecba
--- /dev/null
+++ b/auto-analyst-frontend/docs/api-endpoints.md
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/auto-analyst-frontend/docs/authentication.md b/auto-analyst-frontend/docs/authentication.md
new file mode 100644
index 00000000..e512ec76
--- /dev/null
+++ b/auto-analyst-frontend/docs/authentication.md
@@ -0,0 +1,663 @@
+# Authentication
+
+This document covers the authentication system in Auto-Analyst, including NextAuth.js configuration, session management, and API protection.
+
+## Overview
+
+Auto-Analyst uses **NextAuth.js** for authentication with support for multiple providers and JWT-based sessions. Authentication is required for all core features including trials, subscriptions, and chat functionality.
+
+## Authentication Providers
+
+### Google OAuth
+Primary authentication method for user sign-in.
+
+```typescript
+// lib/auth.ts
+import GoogleProvider from "next-auth/providers/google"
+
+const providers = [
+ GoogleProvider({
+ clientId: process.env.GOOGLE_CLIENT_ID!,
+ clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
+ authorization: {
+ params: {
+ prompt: "consent",
+ access_type: "offline",
+ response_type: "code"
+ }
+ }
+ })
+]
+```
+
+### Required Environment Variables
+```bash
+GOOGLE_CLIENT_ID=your_google_client_id
+GOOGLE_CLIENT_SECRET=your_google_client_secret
+NEXTAUTH_SECRET=your_nextauth_secret
+NEXTAUTH_URL=http://localhost:3000 # or your production URL
+```
+
+## NextAuth Configuration
+
+### Main Configuration
+**File**: `app/api/auth/[...nextauth]/route.ts`
+
+```typescript
+import NextAuth from "next-auth"
+import GoogleProvider from "next-auth/providers/google"
+import { NextAuthOptions } from "next-auth"
+
+const authOptions: NextAuthOptions = {
+ providers: [
+ GoogleProvider({
+ clientId: process.env.GOOGLE_CLIENT_ID!,
+ clientSecret: process.env.GOOGLE_CLIENT_SECRET!
+ })
+ ],
+
+ session: {
+ strategy: "jwt",
+ maxAge: 30 * 24 * 60 * 60, // 30 days
+ updateAge: 24 * 60 * 60 // 24 hours
+ },
+
+ jwt: {
+ maxAge: 30 * 24 * 60 * 60 // 30 days
+ },
+
+ callbacks: {
+ async jwt({ token, user, account }) {
+ // Store user info in JWT token
+ if (user) {
+ token.sub = user.id
+ token.email = user.email
+ token.name = user.name
+ token.picture = user.image
+ }
+ return token
+ },
+
+ async session({ session, token }) {
+ // Send properties to the client
+ if (token) {
+ session.user.id = token.sub
+ session.user.email = token.email
+ session.user.name = token.name
+ session.user.image = token.picture
+ }
+ return session
+ },
+
+ async signIn({ user, account, profile }) {
+ // Always allow sign in for valid Google accounts
+ return true
+ },
+
+ async redirect({ url, baseUrl }) {
+ // Redirect to chat page after successful login
+ if (url.startsWith("/")) return `${baseUrl}${url}`
+ else if (new URL(url).origin === baseUrl) return url
+ return `${baseUrl}/chat`
+ }
+ },
+
+ pages: {
+ signIn: "/login",
+ signOut: "/signout",
+ error: "/login"
+ }
+}
+
+const handler = NextAuth(authOptions)
+export { handler as GET, handler as POST }
+```
+
+## Session Management
+
+### Client-Side Session Access
+
+```typescript
+'use client'
+import { useSession, signIn, signOut } from "next-auth/react"
+
+function SessionComponent() {
+ const { data: session, status } = useSession()
+
+ if (status === "loading") return
Loading...
+
+ if (status === "unauthenticated") {
+ return (
+
+ )
+ }
+
+ return (
+
+
Signed in as {session.user?.email}
+
+
+ )
+}
+```
+
+### Server-Side Session Access
+
+```typescript
+// API Routes
+import { getToken } from "next-auth/jwt"
+
+export async function GET(request: NextRequest) {
+ const token = await getToken({ req: request })
+
+ if (!token) {
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
+ }
+
+ const userId = token.sub
+ const userEmail = token.email
+
+ // Use user data...
+}
+```
+
+```typescript
+// Server Components
+import { getServerSession } from "next-auth/next"
+import { authOptions } from "@/app/api/auth/[...nextauth]/route"
+
+export default async function ServerComponent() {
+ const session = await getServerSession(authOptions)
+
+ if (!session) {
+ redirect('/login')
+ }
+
+ return
Welcome {session.user?.name}
+}
+```
+
+## Session Provider Setup
+
+### Root Layout Configuration
+**File**: `app/layout.tsx`
+
+```typescript
+import { SessionProvider } from "next-auth/react"
+import { AuthProvider } from "@/components/AuthProvider"
+
+export default function RootLayout({
+ children,
+}: {
+ children: React.ReactNode
+}) {
+ return (
+
+
+
+ {children}
+
+
+
+ )
+}
+```
+
+### Auth Provider Component
+**File**: `components/AuthProvider.tsx`
+
+```typescript
+'use client'
+import { SessionProvider } from "next-auth/react"
+
+export function AuthProvider({ children }: { children: React.ReactNode }) {
+ return
{children}
+}
+```
+
+## API Route Protection
+
+### Standard Protection Pattern
+
+```typescript
+// app/api/example/route.ts
+import { NextRequest, NextResponse } from 'next/server'
+import { getToken } from 'next-auth/jwt'
+
+export async function GET(request: NextRequest) {
+ // Authenticate user
+ const token = await getToken({ req: request })
+
+ if (!token?.sub) {
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
+ }
+
+ const userId = token.sub
+ const userEmail = token.email
+
+ try {
+ // Your API logic here
+ return NextResponse.json({ success: true })
+ } catch (error) {
+ return NextResponse.json(
+ { error: 'Internal server error' },
+ { status: 500 }
+ )
+ }
+}
+```
+
+### Reusable Auth Middleware
+
+```typescript
+// lib/auth-middleware.ts
+import { NextRequest } from 'next/server'
+import { getToken } from 'next-auth/jwt'
+
+export async function requireAuth(request: NextRequest) {
+ const token = await getToken({ req: request })
+
+ if (!token?.sub) {
+ throw new Error('Unauthorized')
+ }
+
+ return {
+ userId: token.sub,
+ userEmail: token.email,
+ userName: token.name,
+ userImage: token.picture
+ }
+}
+
+// Usage in API routes
+export async function GET(request: NextRequest) {
+ try {
+ const { userId, userEmail } = await requireAuth(request)
+ // Your protected logic here
+ } catch (error) {
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
+ }
+}
+```
+
+## Frontend Authentication Components
+
+### Login Page
+**File**: `app/login/page.tsx`
+
+```typescript
+'use client'
+import { signIn, getSession } from "next-auth/react"
+import { useEffect } from "react"
+import { useRouter } from "next/navigation"
+import { Button } from "@/components/ui/button"
+
+export default function LoginPage() {
+ const router = useRouter()
+
+ useEffect(() => {
+ // Redirect if already authenticated
+ getSession().then((session) => {
+ if (session) {
+ router.push('/chat')
+ }
+ })
+ }, [router])
+
+ const handleSignIn = () => {
+ signIn('google', { callbackUrl: '/chat' })
+ }
+
+ return (
+
+
+
+
Sign in to Auto-Analyst
+
+ Access your AI-powered analytics platform
+
+
+
+
+
+
+ )
+}
+```
+
+### Sign Out Page
+**File**: `app/signout/page.tsx`
+
+```typescript
+'use client'
+import { signOut, useSession } from "next-auth/react"
+import { useEffect } from "react"
+import { useRouter } from "next/navigation"
+
+export default function SignOutPage() {
+ const { data: session } = useSession()
+ const router = useRouter()
+
+ useEffect(() => {
+ if (session) {
+ signOut({ callbackUrl: '/' })
+ } else {
+ router.push('/')
+ }
+ }, [session, router])
+
+ return (
+
+
+
Signing you out...
+
Thank you for using Auto-Analyst
+
+
+ )
+}
+```
+
+### Navigation Auth State
+**File**: `components/layout.tsx`
+
+```typescript
+'use client'
+import { useSession, signIn, signOut } from "next-auth/react"
+import { Button } from "@/components/ui/button"
+import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
+
+function AuthButton() {
+ const { data: session, status } = useSession()
+
+ if (status === "loading") {
+ return
+ }
+
+ if (status === "unauthenticated") {
+ return (
+
+ )
+ }
+
+ return (
+
+
+
+
+ {session.user?.name?.charAt(0) || 'U'}
+
+
+
+
+
+ )
+}
+```
+
+## Route Protection
+
+### Page-Level Protection
+
+```typescript
+// app/chat/page.tsx
+import { getServerSession } from "next-auth/next"
+import { authOptions } from "@/app/api/auth/[...nextauth]/route"
+import { redirect } from "next/navigation"
+
+export default async function ChatPage() {
+ const session = await getServerSession(authOptions)
+
+ if (!session) {
+ redirect('/login')
+ }
+
+ return (
+
+
Welcome to Chat, {session.user?.name}
+ {/* Chat interface */}
+
+ )
+}
+```
+
+### Middleware Protection
+
+```typescript
+// middleware.ts
+import { withAuth } from "next-auth/middleware"
+
+export default withAuth(
+ function middleware(req) {
+ // Additional middleware logic if needed
+ },
+ {
+ callbacks: {
+ authorized: ({ token }) => !!token
+ },
+ }
+)
+
+export const config = {
+ matcher: [
+ '/chat/:path*',
+ '/account/:path*',
+ '/analytics/:path*',
+ '/api/user/:path*',
+ '/api/trial/:path*',
+ '/api/checkout-sessions/:path*'
+ ]
+}
+```
+
+## User Profile Management
+
+### Storing User Data
+
+```typescript
+// When user signs in, store profile in Redis
+export async function storeUserProfile(userId: string, userData: any) {
+ await redis.hset(KEYS.USER_PROFILE(userId), {
+ email: userData.email,
+ name: userData.name || 'User',
+ image: userData.image || '',
+ joinedDate: new Date().toISOString().split('T')[0],
+ lastLogin: new Date().toISOString()
+ })
+}
+```
+
+### Accessing User Profile
+
+```typescript
+// lib/redis.ts - profileUtils
+export const profileUtils = {
+ async getUserProfile(userId: string) {
+ const profile = await redis.hgetall(KEYS.USER_PROFILE(userId))
+ return profile || {}
+ },
+
+ async updateUserProfile(userId: string, updates: any) {
+ await redis.hset(KEYS.USER_PROFILE(userId), {
+ ...updates,
+ lastUpdated: new Date().toISOString()
+ })
+ }
+}
+```
+
+## Session Security
+
+### JWT Configuration
+
+```typescript
+// Secure JWT settings
+const jwtOptions = {
+ secret: process.env.NEXTAUTH_SECRET,
+ encryption: true,
+ maxAge: 30 * 24 * 60 * 60, // 30 days
+ algorithm: 'HS256'
+}
+```
+
+### Session Validation
+
+```typescript
+// Enhanced token validation
+export async function validateSession(request: NextRequest) {
+ const token = await getToken({ req: request })
+
+ if (!token) {
+ throw new Error('No session token')
+ }
+
+ // Check token expiration
+ if (Date.now() >= token.exp * 1000) {
+ throw new Error('Token expired')
+ }
+
+ // Validate user still exists
+ const userExists = await redis.exists(KEYS.USER_PROFILE(token.sub))
+ if (!userExists) {
+ throw new Error('User not found')
+ }
+
+ return token
+}
+```
+
+## Error Handling
+
+### Authentication Errors
+
+```typescript
+// Common authentication error patterns
+export function handleAuthError(error: any) {
+ switch (error.type) {
+ case 'OAuthAccountNotLinked':
+ return 'Account already exists with different provider'
+ case 'EmailCreateAccount':
+ return 'Account creation failed'
+ case 'Callback':
+ return 'Authentication callback failed'
+ case 'OAuthCallback':
+ return 'OAuth provider callback failed'
+ case 'OAuthCreateAccount':
+ return 'Failed to create OAuth account'
+ case 'SessionRequired':
+ return 'Please sign in to continue'
+ default:
+ return 'Authentication failed'
+ }
+}
+```
+
+### Error Pages
+
+```typescript
+// app/login/error/page.tsx
+'use client'
+import { useSearchParams } from 'next/navigation'
+
+export default function AuthError() {
+ const searchParams = useSearchParams()
+ const error = searchParams.get('error')
+
+ const errorMessage = handleAuthError({ type: error })
+
+ return (
+
+
+
+ Authentication Error
+
+
{errorMessage}
+
+
+
+ )
+}
+```
+
+## Testing Authentication
+
+### Local Development
+
+```bash
+# Set up Google OAuth for localhost
+# In Google Cloud Console:
+# - Create OAuth 2.0 credentials
+# - Add http://localhost:3000 to authorized origins
+# - Add http://localhost:3000/api/auth/callback/google to redirect URIs
+```
+
+### Test User Flows
+
+1. **Sign In Flow**
+ ```typescript
+ // Test successful sign in
+ await signIn('google')
+ // Should redirect to /chat
+ ```
+
+2. **Protected Route Access**
+ ```typescript
+ // Access protected route without auth
+ // Should redirect to /login
+ ```
+
+3. **Session Persistence**
+ ```typescript
+ // Refresh page after sign in
+ // Should maintain session
+ ```
+
+4. **Sign Out Flow**
+ ```typescript
+ // Test sign out
+ await signOut()
+ // Should clear session and redirect
+ ```
+
+## Best Practices
+
+### Security
+1. **Always validate sessions** server-side
+2. **Use HTTPS** in production
+3. **Secure environment variables** properly
+4. **Implement CSRF protection** (built into NextAuth)
+5. **Monitor authentication events**
+
+### Performance
+1. **Cache session data** appropriately
+2. **Minimize token size**
+3. **Use efficient session storage**
+4. **Implement session refresh** logic
+
+### User Experience
+1. **Clear error messages** for auth failures
+2. **Smooth redirect flows** after sign in
+3. **Loading states** during authentication
+4. **Remember user preferences** across sessions
+
+### Development
+1. **Consistent auth patterns** across the app
+2. **Centralized auth logic** in utilities
+3. **Comprehensive error handling**
+4. **Authentication testing** in CI/CD
\ No newline at end of file
diff --git a/auto-analyst-frontend/docs/cancellation-flows.md b/auto-analyst-frontend/docs/cancellation-flows.md
new file mode 100644
index 00000000..e44cdbf2
--- /dev/null
+++ b/auto-analyst-frontend/docs/cancellation-flows.md
@@ -0,0 +1,552 @@
+# Cancellation Flows
+
+This document covers all cancellation scenarios in Auto-Analyst, including trial cancellations, subscription cancellations, and the different handling logic for each case.
+
+## Overview
+
+Auto-Analyst handles two distinct cancellation scenarios with different behaviors:
+
+1. **Trial Cancellation** (0-7 days): Immediate access removal, no charges
+2. **Post-Payment Cancellation** (7+ days): Access maintained until period end
+
+## Cancellation Types
+
+### 1. Trial Cancellation (Immediate)
+
+**When**: User cancels during 7-day trial period
+**Effect**: Immediate access removal, no charges ever made
+**Implementation**: Cancel Stripe subscription immediately
+
+```typescript
+// Subscription status: 'trialing'
+await stripe.subscriptions.cancel(subscriptionId, {
+ prorate: false
+})
+
+// Immediate credit removal
+await creditUtils.setZeroCredits(userId)
+```
+
+### 2. Active Subscription Cancellation (End of Period)
+
+**When**: User cancels after trial conversion (paid subscription)
+**Effect**: Access maintained until billing period ends
+**Implementation**: Set `cancel_at_period_end: true`
+
+```typescript
+// Subscription status: 'active'
+await stripe.subscriptions.update(subscriptionId, {
+ cancel_at_period_end: true
+})
+
+// Access maintained until period_end
+// Credits preserved until cancellation date
+```
+
+## Implementation
+
+### Unified Cancellation Endpoint
+
+**File**: `app/api/trial/cancel/route.ts`
+
+```typescript
+export async function POST(request: NextRequest) {
+ const token = await getToken({ req: request })
+ const userId = token.sub
+
+ // Get user's subscription data
+ const subscriptionData = await redis.hgetall(KEYS.USER_SUBSCRIPTION(userId))
+ const subscriptionId = subscriptionData.stripeSubscriptionId
+
+ if (!subscriptionId) {
+ return NextResponse.json(
+ { error: 'No subscription found' },
+ { status: 404 }
+ )
+ }
+
+ // Retrieve current subscription from Stripe
+ const subscription = await stripe.subscriptions.retrieve(subscriptionId)
+
+ if (subscription.status === 'trialing') {
+ // TRIAL CANCELLATION: Immediate
+ return await handleTrialCancellation(userId, subscriptionId)
+ } else if (subscription.status === 'active') {
+ // ACTIVE CANCELLATION: End of period
+ return await handleActiveCancellation(userId, subscription)
+ } else {
+ return NextResponse.json(
+ { error: `Cannot cancel subscription with status: ${subscription.status}` },
+ { status: 400 }
+ )
+ }
+}
+```
+
+### Trial Cancellation Handler
+
+```typescript
+async function handleTrialCancellation(
+ userId: string,
+ subscriptionId: string
+) {
+ try {
+ // Cancel subscription immediately
+ await stripe.subscriptions.cancel(subscriptionId, {
+ prorate: false
+ })
+
+ // Mark as trial cancellation in Redis
+ await redis.hset(KEYS.USER_CREDITS(userId), {
+ trialCanceled: 'true',
+ canceledAt: new Date().toISOString()
+ })
+
+ // Update subscription status
+ await redis.hset(KEYS.USER_SUBSCRIPTION(userId), {
+ status: 'canceled',
+ displayStatus: 'canceled',
+ canceledAt: new Date().toISOString(),
+ subscriptionCanceled: 'true'
+ })
+
+ console.log(`Trial canceled for user ${userId}`)
+
+ return NextResponse.json({
+ success: true,
+ canceled: true,
+ creditsRemoved: true,
+ message: 'Trial canceled successfully. You will not be charged.'
+ })
+ } catch (error) {
+ console.error('Trial cancellation error:', error)
+ return NextResponse.json(
+ { error: 'Failed to cancel trial' },
+ { status: 500 }
+ )
+ }
+}
+```
+
+### Active Subscription Cancellation Handler
+
+```typescript
+async function handleActiveCancellation(
+ userId: string,
+ subscription: Stripe.Subscription
+) {
+ try {
+ // Set cancel at period end
+ const updatedSubscription = await stripe.subscriptions.update(
+ subscription.id,
+ { cancel_at_period_end: true }
+ )
+
+ const periodEnd = new Date(subscription.current_period_end * 1000)
+
+ // Update Redis with cancellation info
+ await redis.hset(KEYS.USER_SUBSCRIPTION(userId), {
+ cancel_at_period_end: 'true',
+ willCancelAt: periodEnd.toISOString(),
+ displayStatus: 'canceling',
+ lastUpdated: new Date().toISOString()
+ })
+
+ console.log(`Active subscription marked for cancellation: ${userId}`)
+
+ return NextResponse.json({
+ success: true,
+ canceledAtPeriodEnd: true,
+ accessUntil: periodEnd.toISOString(),
+ message: `Your subscription will cancel on ${periodEnd.toLocaleDateString()}. You'll maintain access until then.`
+ })
+ } catch (error) {
+ console.error('Active cancellation error:', error)
+ return NextResponse.json(
+ { error: 'Failed to cancel subscription' },
+ { status: 500 }
+ )
+ }
+}
+```
+
+## Webhook Handling
+
+### Subscription Deletion Webhook
+
+**Event**: `customer.subscription.deleted`
+
+```typescript
+case 'customer.subscription.deleted': {
+ const subscription = event.data.object as Stripe.Subscription
+ const userId = await getUserIdFromCustomerId(subscription.customer as string)
+
+ if (!userId) {
+ console.error('No userId found for customer:', subscription.customer)
+ break
+ }
+
+ // Update subscription status
+ await redis.hset(KEYS.USER_SUBSCRIPTION(userId), {
+ status: 'canceled',
+ stripeSubscriptionStatus: 'canceled',
+ displayStatus: 'canceled',
+ canceledAt: new Date().toISOString(),
+ subscriptionDeleted: 'true',
+ lastUpdated: new Date().toISOString(),
+ syncedAt: new Date().toISOString()
+ })
+
+ // Set credits to 0 for canceled subscriptions
+ await creditUtils.setZeroCredits(userId)
+
+ console.log(`Subscription deleted for user ${userId}, credits set to 0`)
+ break
+}
+```
+
+### Subscription Update Webhook
+
+**Event**: `customer.subscription.updated`
+
+```typescript
+case 'customer.subscription.updated': {
+ const subscription = event.data.object as Stripe.Subscription
+ const userId = await getUserIdFromCustomerId(subscription.customer as string)
+
+ // Handle cancel_at_period_end status
+ if (subscription.status === 'active' && subscription.cancel_at_period_end) {
+ await redis.hset(KEYS.USER_SUBSCRIPTION(userId), {
+ status: 'active',
+ displayStatus: 'canceling',
+ cancel_at_period_end: 'true',
+ willCancelAt: new Date(subscription.current_period_end * 1000).toISOString(),
+ lastUpdated: new Date().toISOString(),
+ syncedAt: new Date().toISOString()
+ })
+ } else {
+ await redis.hset(KEYS.USER_SUBSCRIPTION(userId), {
+ status: subscription.status,
+ displayStatus: subscription.status,
+ cancel_at_period_end: subscription.cancel_at_period_end.toString(),
+ lastUpdated: new Date().toISOString(),
+ syncedAt: new Date().toISOString()
+ })
+ }
+
+ break
+}
+```
+
+## Credit Management During Cancellation
+
+### Trial Cancellation Credit Logic
+
+```typescript
+// Immediate credit removal for trial cancellations
+async function handleTrialCreditRemoval(userId: string) {
+ await redis.hset(KEYS.USER_CREDITS(userId), {
+ total: '0',
+ used: '0',
+ trialCanceled: 'true',
+ canceledAt: new Date().toISOString(),
+ lastUpdate: new Date().toISOString()
+ })
+
+ console.log(`Credits removed for trial cancellation: ${userId}`)
+}
+```
+
+### Active Subscription Credit Logic
+
+```typescript
+// Credits preserved until period end
+async function handleActiveCreditPreservation(
+ userId: string,
+ periodEndDate: Date
+) {
+ // Credits remain active until cancellation date
+ // No immediate changes to credit allocation
+
+ await redis.hset(KEYS.USER_CREDITS(userId), {
+ willResetAt: periodEndDate.toISOString(),
+ pendingCancellation: 'true',
+ lastUpdate: new Date().toISOString()
+ })
+
+ console.log(`Credits preserved until ${periodEndDate.toISOString()} for user ${userId}`)
+}
+```
+
+## Status Display Logic
+
+### Frontend Status Mapping
+
+```typescript
+function getDisplayStatus(subscription: SubscriptionData) {
+ // Use displayStatus if available (for UI-specific states)
+ if (subscription.displayStatus) {
+ return {
+ status: subscription.displayStatus,
+ color: getStatusColor(subscription.displayStatus),
+ message: getStatusMessage(subscription.displayStatus)
+ }
+ }
+
+ // Fallback to subscription status
+ return {
+ status: subscription.status,
+ color: getStatusColor(subscription.status),
+ message: getStatusMessage(subscription.status)
+ }
+}
+
+function getStatusColor(status: string) {
+ switch (status) {
+ case 'trialing': return 'blue'
+ case 'active': return 'green'
+ case 'canceling': return 'amber' // Special UI state
+ case 'canceled': return 'red'
+ case 'past_due': return 'orange'
+ default: return 'gray'
+ }
+}
+
+function getStatusMessage(status: string) {
+ switch (status) {
+ case 'trialing': return 'Trial period active'
+ case 'active': return 'Subscription active'
+ case 'canceling': return 'Will cancel at period end'
+ case 'canceled': return 'Subscription canceled'
+ case 'past_due': return 'Payment past due'
+ default: return 'Unknown status'
+ }
+}
+```
+
+### Account Page Display
+
+```typescript
+// components/account/SubscriptionStatus.tsx
+function SubscriptionStatus({ subscription }: { subscription: SubscriptionData }) {
+ const displayStatus = getDisplayStatus(subscription)
+
+ return (
+
+
+ {displayStatus.status}
+
+
+ {subscription.status === 'canceling' && (
+
+ Cancels on {new Date(subscription.willCancelAt).toLocaleDateString()}
+
+ )}
+
+ {subscription.status === 'trialing' && (
+
+ Trial ends {new Date(subscription.trialEndDate).toLocaleDateString()}
+
+ )}
+
+ )
+}
+```
+
+## Cancellation Prevention
+
+### Retry Logic for Failed Payments
+
+```typescript
+// Webhook: invoice.payment_failed
+case 'invoice.payment_failed': {
+ const invoice = event.data.object as Stripe.Invoice
+
+ // Don't immediately cancel - Stripe will retry
+ console.log(`Payment failed for invoice ${invoice.id}`, {
+ attemptCount: invoice.attempt_count,
+ nextPaymentAttempt: invoice.next_payment_attempt
+ })
+
+ // Update status to past_due but maintain access
+ await redis.hset(KEYS.USER_SUBSCRIPTION(userId), {
+ status: 'past_due',
+ displayStatus: 'past_due',
+ lastPaymentFailure: new Date().toISOString()
+ })
+
+ break
+}
+```
+
+### Win-Back Campaigns
+
+```typescript
+// Trigger win-back email for canceling subscriptions
+async function triggerWinBackCampaign(userId: string, cancellationReason?: string) {
+ const userData = await redis.hgetall(KEYS.USER_PROFILE(userId))
+
+ // Send targeted email based on cancellation timing
+ if (subscription.status === 'trialing') {
+ await sendEmail(userData.email, 'trial-cancellation-winback')
+ } else {
+ await sendEmail(userData.email, 'subscription-cancellation-winback')
+ }
+}
+```
+
+## Reactivation
+
+### Resubscribe Flow
+
+```typescript
+// Allow users to reactivate canceled subscriptions
+async function reactivateSubscription(userId: string, subscriptionId: string) {
+ try {
+ // Remove cancel_at_period_end flag
+ const subscription = await stripe.subscriptions.update(subscriptionId, {
+ cancel_at_period_end: false
+ })
+
+ // Update Redis status
+ await redis.hset(KEYS.USER_SUBSCRIPTION(userId), {
+ status: 'active',
+ displayStatus: 'active',
+ cancel_at_period_end: 'false',
+ reactivatedAt: new Date().toISOString(),
+ lastUpdated: new Date().toISOString()
+ })
+
+ // Restore credits if needed
+ await subscriptionUtils.refreshCreditsIfNeeded(userId)
+
+ return { success: true, reactivated: true }
+ } catch (error) {
+ console.error('Reactivation failed:', error)
+ throw error
+ }
+}
+```
+
+## Error Handling
+
+### Cancellation Errors
+
+```typescript
+// Handle common cancellation errors
+async function handleCancellationError(error: any, userId: string) {
+ switch (error.code) {
+ case 'resource_missing':
+ // Subscription not found
+ await cleanupOrphanedSubscription(userId)
+ break
+
+ case 'subscription_canceled':
+ // Already canceled
+ await syncCancellationStatus(userId)
+ break
+
+ default:
+ console.error('Unexpected cancellation error:', error)
+ throw error
+ }
+}
+```
+
+### Data Consistency
+
+```typescript
+// Ensure Redis and Stripe stay in sync
+async function validateCancellationState(userId: string) {
+ const redisData = await redis.hgetall(KEYS.USER_SUBSCRIPTION(userId))
+ const stripeSubscription = await stripe.subscriptions.retrieve(
+ redisData.stripeSubscriptionId
+ )
+
+ // Check for discrepancies
+ if (redisData.status !== stripeSubscription.status) {
+ console.warn('Status mismatch detected:', {
+ userId,
+ redis: redisData.status,
+ stripe: stripeSubscription.status
+ })
+
+ // Sync with Stripe as source of truth
+ await syncSubscriptionStatus(userId, stripeSubscription)
+ }
+}
+```
+
+## Testing Cancellation Flows
+
+### Test Scenarios
+
+1. **Trial Cancellation**
+ ```bash
+ # Start trial, then cancel within 7 days
+ POST /api/trial/cancel
+ # Expected: Immediate access removal, no charge
+ ```
+
+2. **Active Cancellation**
+ ```bash
+ # Convert trial to paid, then cancel
+ POST /api/trial/cancel
+ # Expected: Access until period end
+ ```
+
+3. **Webhook Cancellation**
+ ```bash
+ stripe trigger customer.subscription.deleted
+ # Expected: Cleanup in Redis, credits zeroed
+ ```
+
+4. **Reactivation**
+ ```bash
+ # Cancel subscription, then reactivate
+ # Expected: Restored access and credits
+ ```
+
+## Monitoring
+
+### Cancellation Metrics
+
+- **Trial Cancellation Rate**: % of trials canceled before conversion
+- **Churn Rate**: % of paid subscriptions canceled per month
+- **Reactivation Rate**: % of canceled users who resubscribe
+- **Time to Cancel**: Average days from signup to cancellation
+
+### Alerting
+
+```typescript
+// Monitor for unusual cancellation patterns
+const cancellationMetrics = {
+ trialCancellations: await getTrialCancellations('last_24h'),
+ activeCancellations: await getActiveCancellations('last_24h'),
+ reactivations: await getReactivations('last_24h')
+}
+
+if (cancellationMetrics.trialCancellations > threshold.trial) {
+ await sendAlert('High trial cancellation rate detected')
+}
+```
+
+## Best Practices
+
+### Cancellation UX
+1. **Clear consequences**: Explain what happens when canceling
+2. **Retention offers**: Present alternatives before confirming
+3. **Easy reactivation**: Allow users to easily resubscribe
+4. **Feedback collection**: Understand why users are canceling
+
+### Technical Implementation
+1. **Immediate feedback**: Confirm cancellation success instantly
+2. **Data consistency**: Keep Redis and Stripe synchronized
+3. **Graceful degradation**: Handle edge cases smoothly
+4. **Comprehensive logging**: Track all cancellation events
+
+### Business Logic
+1. **Preserve trial conversion**: Don't make trials too restrictive
+2. **Honor commitments**: Maintain access for paid periods
+3. **Win-back opportunities**: Engage canceled users appropriately
+4. **Analytics driven**: Use data to improve retention
\ No newline at end of file
diff --git a/auto-analyst-frontend/docs/credit-system.md b/auto-analyst-frontend/docs/credit-system.md
new file mode 100644
index 00000000..123b6dbd
--- /dev/null
+++ b/auto-analyst-frontend/docs/credit-system.md
@@ -0,0 +1,468 @@
+# Credit System
+
+This document covers the complete credit management system in Auto-Analyst, including allocation, usage tracking, and billing logic.
+
+## Overview
+
+Auto-Analyst uses a credit-based billing system where users consume credits for various features like chat analysis, code execution, and deep analysis. Credits are allocated based on subscription plans and reset monthly.
+
+## Credit Configuration
+
+### Plan Types and Credits
+
+Located in `lib/credits-config.ts`:
+
+```typescript
+export const PLAN_CREDITS: Record
= {
+ 'Trial': {
+ total: 500,
+ displayName: 'Trial Plan',
+ type: 'TRIAL',
+ isUnlimited: false,
+ minimum: 0
+ },
+ 'Standard': {
+ total: 500,
+ displayName: 'Standard Plan',
+ type: 'STANDARD',
+ isUnlimited: false,
+ minimum: 0
+ },
+ 'Pro': {
+ total: 999999,
+ displayName: 'Pro Plan',
+ type: 'PRO',
+ isUnlimited: true,
+ minimum: 0
+ }
+}
+```
+
+### Credit Utilities
+
+```typescript
+export const CreditConfig = {
+ // Get credits by plan type
+ getCreditsByType: (type: PlanType) => PLAN_CREDITS[type],
+
+ // Get credits by plan name
+ getCreditsForPlan: (planName: string) => { /* implementation */ },
+
+ // Check if unlimited credits
+ isUnlimitedTotal: (total: number) => total >= 999999,
+
+ // Get next reset date
+ getNextResetDate: () => {
+ const now = new Date()
+ const nextMonth = new Date(now.getFullYear(), now.getMonth() + 1, 1)
+ return nextMonth.toISOString().split('T')[0]
+ }
+}
+```
+
+## Credit Lifecycle
+
+### 1. Credit Initialization
+
+**For Trial Users**:
+```typescript
+// app/api/trial/start/route.ts
+await creditUtils.initializeTrialCredits(userId, {
+ total: 500,
+ resetDate: CreditConfig.getNextResetDate(),
+ paymentIntentId: paymentIntent.id
+})
+```
+
+**For Subscription Users**:
+```typescript
+// Handled by webhook: invoice.payment_succeeded
+await subscriptionUtils.refreshCreditsIfNeeded(userId)
+```
+
+### 2. Credit Reset Schedule
+
+Credits reset monthly on the 1st of each month:
+
+```typescript
+// lib/redis.ts - refreshCreditsIfNeeded()
+const now = new Date()
+const resetDate = new Date(creditsData.resetDate)
+
+if (now >= resetDate) {
+ // Reset credits to plan amount
+ const planCredits = CreditConfig.getCreditsByType(planType)
+
+ await redis.hset(KEYS.USER_CREDITS(userId), {
+ total: planCredits.credits.toString(),
+ used: '0',
+ resetDate: CreditConfig.getNextResetDate(),
+ lastUpdate: new Date().toISOString()
+ })
+}
+```
+
+### 3. Credit Usage Tracking
+
+#### Deduction Process
+```typescript
+// app/api/user/deduct-credits/route.ts
+export async function POST(request: NextRequest) {
+ const { amount, description } = await request.json()
+ const token = await getToken({ req: request })
+ const userId = token.sub
+
+ // Attempt to deduct credits
+ const success = await creditUtils.deductCredits(userId, amount)
+
+ if (!success) {
+ return NextResponse.json(
+ {
+ error: "Insufficient credits",
+ required: amount,
+ available: await creditUtils.getRemainingCredits(userId)
+ },
+ { status: 402 }
+ )
+ }
+
+ // Return updated credit info
+ const remaining = await creditUtils.getRemainingCredits(userId)
+ return NextResponse.json({
+ success: true,
+ remainingCredits: remaining,
+ deducted: amount,
+ description
+ })
+}
+```
+
+#### Atomic Deduction Logic
+```typescript
+// lib/redis.ts - creditUtils.deductCredits()
+async deductCredits(userId: string, amount: number): Promise {
+ const creditsHash = await redis.hgetall(KEYS.USER_CREDITS(userId))
+
+ if (!creditsHash || !creditsHash.total) {
+ return false // No credits available
+ }
+
+ const total = parseInt(creditsHash.total)
+ const used = parseInt(creditsHash.used || '0')
+ const available = total - used
+
+ if (available < amount) {
+ return false // Insufficient credits
+ }
+
+ // Atomic update
+ await redis.hset(KEYS.USER_CREDITS(userId), {
+ used: (used + amount).toString(),
+ lastUpdate: new Date().toISOString()
+ })
+
+ return true
+}
+```
+
+## Feature Credit Costs
+
+### Current Credit Pricing
+
+```typescript
+export const FEATURE_COSTS = {
+ /** Cost for deep analysis - premium feature for paid users */
+ DEEP_ANALYSIS: 50,
+}
+
+// Model credit costs are based on MODEL_TIERS configuration:
+// - Basic models: 1-2 credits
+// - Standard models: 3-5 credits
+// - Premium models: 6-10 credits
+// - Premium+ models: 11-20 credits
+```
+
+### Usage Implementation
+
+```typescript
+// For deep analysis feature
+const creditsNeeded = FEATURE_COSTS.DEEP_ANALYSIS // 50 credits
+
+const deductResponse = await fetch('/api/user/deduct-credits', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ amount: creditsNeeded,
+ description: 'Deep analysis report generation'
+ })
+})
+
+if (deductResponse.status === 402) {
+ // Show upgrade prompt - insufficient credits
+ showInsufficientCreditsModal()
+ return
+}
+
+// For AI model usage (chat messages)
+const modelCost = getModelCreditCost(selectedModel) // 1-20 credits based on model tier
+// Credit deduction happens automatically in backend based on model used
+```
+
+## Credit Storage Schema
+
+### Redis Structure
+
+```typescript
+// Key: user:${userId}:credits
+{
+ total: string // "500"
+ used: string // "150"
+ resetDate: string // "2024-02-01"
+ lastUpdate: string // ISO timestamp
+ resetInterval: string // "month"
+
+ // Trial-specific fields
+ isTrialCredits?: string // "true"
+ paymentIntentId?: string // Stripe payment intent
+
+ // Cancellation tracking
+ trialCanceled?: string // "true"
+ subscriptionDeleted?: string // "true"
+
+ // Admin fields
+ nextTotalCredits?: string // For plan changes
+ pendingDowngrade?: string // "true"
+}
+```
+
+### Data Validation
+
+```typescript
+function validateCreditData(creditsData: any) {
+ const total = parseInt(creditsData.total || '0')
+ const used = parseInt(creditsData.used || '0')
+
+ // Validation rules
+ if (total < 0) throw new Error('Total credits cannot be negative')
+ if (used < 0) throw new Error('Used credits cannot be negative')
+ if (used > total && total < 999999) {
+ throw new Error('Used credits cannot exceed total')
+ }
+
+ return { total, used, remaining: total - used }
+}
+```
+
+## Credit Refresh Logic
+
+### Automatic Refresh Triggers
+
+1. **User Data API Call** (`/api/user/data`)
+2. **Credit Check API Call** (`/api/user/credits`)
+3. **Webhook Events** (payment succeeded, subscription updated)
+4. **Navigation Events** (chat page load with refresh flag)
+
+### Refresh Implementation
+
+```typescript
+// lib/redis.ts - subscriptionUtils.refreshCreditsIfNeeded()
+async refreshCreditsIfNeeded(userId: string): Promise {
+ const creditsData = await redis.hgetall(KEYS.USER_CREDITS(userId))
+ const subscriptionData = await redis.hgetall(KEYS.USER_SUBSCRIPTION(userId))
+
+ // Check if credits need reset (monthly)
+ const now = new Date()
+ const resetDate = new Date(creditsData.resetDate || now)
+
+ if (now >= resetDate) {
+ await this.resetMonthlyCredits(userId)
+ return true
+ }
+
+ // Check for plan changes
+ if (this.hasPlanChanged(creditsData, subscriptionData)) {
+ await this.updateCreditsForPlanChange(userId)
+ return true
+ }
+
+ // Check for cancellations
+ if (this.shouldZeroCredits(creditsData, subscriptionData)) {
+ await creditUtils.setZeroCredits(userId)
+ return true
+ }
+
+ return false
+}
+```
+
+### Reset Logic
+
+```typescript
+async resetMonthlyCredits(userId: string) {
+ const subscriptionData = await redis.hgetall(KEYS.USER_SUBSCRIPTION(userId))
+ const planType = subscriptionData.planType || 'STANDARD'
+ const planCredits = CreditConfig.getCreditsByType(planType)
+
+ await redis.hset(KEYS.USER_CREDITS(userId), {
+ total: planCredits.credits.toString(),
+ used: '0',
+ resetDate: CreditConfig.getNextResetDate(),
+ lastUpdate: new Date().toISOString(),
+ resetInterval: 'month'
+ })
+
+ console.log(`Credits reset for user ${userId}: ${planCredits.credits} credits`)
+}
+```
+
+## Credit Preservation
+
+### Successful Trial Conversion
+```typescript
+// Credits are preserved when trial converts to paid
+// Only genuine cancellations zero out credits
+
+function shouldZeroCredits(creditsData: any, subscriptionData: any): boolean {
+ // Genuine trial cancellation
+ if (creditsData.trialCanceled === 'true') return true
+
+ // Subscription deleted (not converted)
+ if (creditsData.subscriptionDeleted === 'true') return true
+
+ // Status is canceled and marked for cleanup
+ if (subscriptionData.status === 'canceled' &&
+ subscriptionData.subscriptionCanceled === 'true') return true
+
+ return false
+}
+```
+
+### Plan Downgrade Protection
+```typescript
+// Credits are preserved during plan changes within billing period
+// Only reset to new plan amount at next billing cycle
+
+async updateCreditsForPlanChange(userId: string) {
+ const subscriptionData = await redis.hgetall(KEYS.USER_SUBSCRIPTION(userId))
+ const newPlanType = subscriptionData.planType
+ const newPlanCredits = CreditConfig.getCreditsByType(newPlanType)
+
+ // Set next month's credit allocation
+ await redis.hset(KEYS.USER_CREDITS(userId), {
+ nextTotalCredits: newPlanCredits.credits.toString(),
+ pendingDowngrade: newPlanCredits.credits < currentTotal ? 'true' : 'false'
+ })
+}
+```
+
+## Credit Display
+
+### Frontend Components
+
+#### Credit Balance Display
+```typescript
+// components/chat/CreditBalance.tsx
+function CreditBalance() {
+ const [credits, setCredits] = useState({ used: 0, total: 0 })
+ const [isRefreshing, setIsRefreshing] = useState(false)
+
+ const refreshCredits = async () => {
+ setIsRefreshing(true)
+ const response = await fetch('/api/user/credits')
+ const data = await response.json()
+ setCredits(data)
+ setIsRefreshing(false)
+ }
+
+ const percentage = credits.total === Infinity ? 100 :
+ Math.round(((credits.total - credits.used) / credits.total) * 100)
+
+ return (
+
+ {isRefreshing ? (
+
+ ) : (
+
{credits.total - credits.used} / {credits.total}
+ )}
+
+
+ )
+}
+```
+
+#### Usage Warning
+```typescript
+function InsufficientCreditsModal({ requiredCredits, availableCredits }) {
+ return (
+
+
+
+ Insufficient Credits
+
+ You need {requiredCredits} credits but only have {availableCredits} remaining.
+ Upgrade your plan to continue using this feature.
+
+
+
+
+
+
+
+ )
+}
+```
+
+## Monitoring and Analytics
+
+### Credit Usage Tracking
+```typescript
+// Log credit usage for analytics
+console.log('Credit deduction:', {
+ userId,
+ amount,
+ feature: description,
+ remainingCredits,
+ timestamp: new Date().toISOString()
+})
+```
+
+### Key Metrics
+- Average credits per user per month
+- Feature usage patterns
+- Credit exhaustion rates
+- Conversion rates from credit exhaustion
+
+### Alerts
+- Users approaching credit limits
+- Unusual usage patterns
+- Failed credit deductions
+- System errors in credit processing
+
+## Best Practices
+
+### Credit Management
+1. **Always check credits** before feature usage
+2. **Handle insufficient credits gracefully** with upgrade prompts
+3. **Use atomic operations** for credit deduction
+4. **Preserve credits** during legitimate state transitions
+5. **Reset credits monthly** regardless of billing cycle
+
+### Error Handling
+1. **Graceful degradation** when credit system fails
+2. **Clear error messages** for users
+3. **Retry logic** for temporary failures
+4. **Logging** for debugging and monitoring
+
+### Performance
+1. **Cache credit data** in component state
+2. **Batch credit operations** when possible
+3. **Use Redis efficiently** with hash operations
+4. **Minimize API calls** with smart refresh logic
+
+### Security
+1. **Validate all credit operations** server-side
+2. **Prevent credit manipulation** via client
+3. **Audit credit changes** with timestamps
+4. **Rate limit** credit-related endpoints
\ No newline at end of file
diff --git a/auto-analyst-frontend/docs/redis-schema.md b/auto-analyst-frontend/docs/redis-schema.md
new file mode 100644
index 00000000..6626f81b
--- /dev/null
+++ b/auto-analyst-frontend/docs/redis-schema.md
@@ -0,0 +1,292 @@
+# Redis Data Schema
+
+This document describes the complete Redis data structure used in Auto-Analyst for user management, subscriptions, and credit tracking.
+
+## Overview
+
+Auto-Analyst uses **Upstash Redis** with a hash-based storage pattern for efficient data access and atomic operations. All user data is organized using consistent key patterns.
+
+## Environment Variables
+
+```bash
+UPSTASH_REDIS_REST_URL=your_redis_url
+UPSTASH_REDIS_REST_TOKEN=your_redis_token
+```
+
+## Key Patterns
+
+### Core Keys
+```typescript
+export const KEYS = {
+ USER_PROFILE: (userId: string) => `user:${userId}:profile`,
+ USER_SUBSCRIPTION: (userId: string) => `user:${userId}:subscription`,
+ USER_CREDITS: (userId: string) => `user:${userId}:credits`,
+};
+
+// Additional keys
+stripe:customer:${customerId} -> userId (string mapping)
+```
+
+## Data Structures
+
+### 1. User Profile (`user:${userId}:profile`)
+
+**Hash Structure:**
+```typescript
+{
+ email: string // User's email address
+ name: string // User's display name
+ image: string // Profile image URL
+ joinedDate: string // ISO date string (YYYY-MM-DD)
+ role: string // Plan display name (e.g., "Standard Plan")
+}
+```
+
+**Example:**
+```json
+{
+ "email": "user@example.com",
+ "name": "John Doe",
+ "image": "https://example.com/avatar.jpg",
+ "joinedDate": "2024-01-15",
+ "role": "Standard Plan"
+}
+```
+
+### 2. User Subscription (`user:${userId}:subscription`)
+
+**Hash Structure:**
+```typescript
+{
+ // Core subscription data
+ plan: string // Plan display name
+ planType: string // Plan type (TRIAL, STANDARD, PRO, etc.)
+ status: string // Subscription status
+ displayStatus?: string // UI display status (for canceling subscriptions)
+ amount: string // Amount as string (e.g., "15.00")
+ interval: string // Billing interval (month, year)
+
+ // Dates and timestamps
+ purchaseDate: string // ISO timestamp
+ renewalDate: string // Next billing date (YYYY-MM-DD)
+ lastUpdated: string // ISO timestamp
+
+ // Stripe integration
+ stripeCustomerId: string // Stripe customer ID
+ stripeSubscriptionId: string // Stripe subscription ID
+ stripeSubscriptionStatus: string // Direct Stripe status
+
+ // Trial management
+ trialStartDate?: string // ISO timestamp
+ trialEndDate?: string // ISO timestamp
+ trialEndedAt?: string // ISO timestamp
+ trialToActiveDate?: string // ISO timestamp
+
+ // Cancellation tracking
+ canceledAt?: string // ISO timestamp
+ subscriptionCanceled?: string // "true" if canceled
+ cancel_at_period_end?: string // "true"/"false"
+ willCancelAt?: string // ISO timestamp
+
+ // Yearly subscription support
+ isYearly?: boolean // True for yearly plans
+ nextMonthlyReset?: string // Next credit reset for yearly plans
+
+ // Synchronization
+ syncedAt?: string // Last sync timestamp
+}
+```
+
+**Example:**
+```json
+{
+ "plan": "Standard Plan",
+ "planType": "STANDARD",
+ "status": "active",
+ "amount": "15.00",
+ "interval": "month",
+ "purchaseDate": "2024-01-15T10:30:00.000Z",
+ "renewalDate": "2024-02-15",
+ "lastUpdated": "2024-01-20T15:45:00.000Z",
+ "stripeCustomerId": "cus_ABC123",
+ "stripeSubscriptionId": "sub_DEF456",
+ "stripeSubscriptionStatus": "active"
+}
+```
+
+### 3. User Credits (`user:${userId}:credits`)
+
+**Hash Structure:**
+```typescript
+{
+ // Core credit data
+ total: string // Total credits as string
+ used: string // Used credits as string
+ resetDate: string // Next reset date (YYYY-MM-DD)
+ lastUpdate: string // ISO timestamp
+ resetInterval: string // "month" (always monthly)
+
+ // Trial credits
+ isTrialCredits?: string // "true" if trial credits
+ paymentIntentId?: string // Associated payment intent
+
+ // Cancellation tracking
+ trialCanceled?: string // "true" if trial was canceled
+ subscriptionDeleted?: string // "true" if subscription deleted
+ downgradedAt?: string // ISO timestamp
+ canceledAt?: string // ISO timestamp
+
+ // Admin operations
+ nextTotalCredits?: string // Scheduled credit amount
+ pendingDowngrade?: string // "true" if downgrade pending
+}
+```
+
+**Example:**
+```json
+{
+ "total": "500",
+ "used": "150",
+ "resetDate": "2024-02-01",
+ "lastUpdate": "2024-01-20T15:45:00.000Z",
+ "resetInterval": "month"
+}
+```
+
+### 4. Stripe Customer Mapping (`stripe:customer:${customerId}`)
+
+**Simple String Value:**
+```typescript
+// Key: stripe:customer:cus_ABC123
+// Value: "user123"
+```
+
+This mapping allows webhooks to find the correct user ID from Stripe customer events.
+
+## Data Operations
+
+### Reading Data
+```typescript
+// Get all subscription data
+const subscriptionData = await redis.hgetall(KEYS.USER_SUBSCRIPTION(userId))
+
+// Get specific field
+const planType = await redis.hget(KEYS.USER_SUBSCRIPTION(userId), 'planType')
+
+// Get multiple fields
+const creditInfo = await redis.hmget(KEYS.USER_CREDITS(userId), ['total', 'used', 'resetDate'])
+```
+
+### Writing Data
+```typescript
+// Update multiple fields atomically
+await redis.hset(KEYS.USER_SUBSCRIPTION(userId), {
+ status: 'active',
+ lastUpdated: new Date().toISOString(),
+ stripeSubscriptionStatus: 'active'
+})
+
+// Set single field
+await redis.hset(KEYS.USER_CREDITS(userId), 'used', '160')
+```
+
+### Atomic Operations
+```typescript
+// Check and update credits atomically
+const creditsHash = await redis.hgetall(KEYS.USER_CREDITS(userId))
+if (creditsHash && creditsHash.total) {
+ const total = parseInt(creditsHash.total)
+ const used = parseInt(creditsHash.used || '0')
+
+ if (total - used >= requiredCredits) {
+ await redis.hset(KEYS.USER_CREDITS(userId), {
+ used: (used + requiredCredits).toString(),
+ lastUpdate: new Date().toISOString()
+ })
+ return true
+ }
+}
+return false
+```
+
+## Data Consistency
+
+### Webhook Synchronization
+All Stripe events are synchronized to Redis via webhooks to ensure data consistency:
+
+1. **Subscription Updates**: `customer.subscription.updated` → Update subscription status
+2. **Payment Success**: `invoice.payment_succeeded` → Activate subscription
+3. **Payment Failure**: `invoice.payment_failed` → Handle failed payments
+4. **Subscription Deletion**: `customer.subscription.deleted` → Clean up data
+
+### Error Handling
+```typescript
+try {
+ const result = await redis.hgetall(KEYS.USER_SUBSCRIPTION(userId))
+ return result || {}
+} catch (error) {
+ console.error('Redis error:', error)
+ return {} // Graceful fallback
+}
+```
+
+## Performance Considerations
+
+### Batch Operations
+```typescript
+// Use pipeline for multiple operations
+const pipeline = redis.pipeline()
+pipeline.hset(KEYS.USER_SUBSCRIPTION(userId), subscriptionData)
+pipeline.hset(KEYS.USER_CREDITS(userId), creditData)
+await pipeline.exec()
+```
+
+### Caching Strategy
+- **TTL**: No TTL set on user data (persistent)
+- **Invalidation**: Data updated via webhooks and API calls
+- **Fallback**: API endpoints provide fallback for missing data
+
+## Monitoring
+
+### Key Metrics to Monitor
+- Redis memory usage
+- Connection count
+- Operation latency
+- Error rates
+
+### Debug Endpoints
+- `/api/debug/redis-check` - Check Redis data for specific user
+- `/api/debug/sync-subscription` - Manual sync with Stripe
+
+## Migration Patterns
+
+### Adding New Fields
+```typescript
+// Safe field addition (backward compatible)
+const subscriptionData = await redis.hgetall(KEYS.USER_SUBSCRIPTION(userId))
+const newField = subscriptionData.newField || 'defaultValue'
+```
+
+### Data Migration Script Example
+```typescript
+async function migrateSubscriptions() {
+ const keys = await redis.keys('user:*:subscription')
+
+ for (const key of keys) {
+ const data = await redis.hgetall(key)
+ if (!data.planType) {
+ await redis.hset(key, 'planType', 'STANDARD')
+ }
+ }
+}
+```
+
+## Best Practices
+
+1. **Always use hash operations** for user data
+2. **Include timestamps** for debugging and auditing
+3. **Handle missing data gracefully** with fallbacks
+4. **Use atomic operations** for credit deduction
+5. **Validate data types** when reading from Redis
+6. **Log errors** but don't fail the user experience
+7. **Use consistent key patterns** across the application
\ No newline at end of file
diff --git a/auto-analyst-frontend/docs/stripe-integration.md b/auto-analyst-frontend/docs/stripe-integration.md
new file mode 100644
index 00000000..78c72a4d
--- /dev/null
+++ b/auto-analyst-frontend/docs/stripe-integration.md
@@ -0,0 +1,382 @@
+# Stripe Integration
+
+This document covers the complete Stripe integration in Auto-Analyst, including payment processing, subscription management, and webhook handling.
+
+## Overview
+
+Auto-Analyst uses Stripe for:
+- 7-day free trial with payment authorization
+- Subscription billing (monthly/yearly)
+- Payment processing
+- Subscription lifecycle management
+- Real-time event synchronization via webhooks
+
+## Environment Variables
+
+```bash
+# Required Stripe Configuration
+STRIPE_SECRET_KEY=sk_test_... # or sk_live_... for production
+STRIPE_WEBHOOK_SECRET=whsec_... # Webhook endpoint secret
+
+# Client-side Stripe Configuration
+NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_... # or pk_live_... for production
+```
+
+## Stripe Configuration
+
+### API Version
+```typescript
+const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, {
+ apiVersion: '2025-02-24.acacia',
+})
+```
+
+### Required Stripe Products
+
+#### 1. Standard Plan
+```json
+{
+ "name": "Standard Plan",
+ "prices": [
+ {
+ "amount": 1500, // $15.00
+ "currency": "usd",
+ "interval": "month"
+ }
+ ]
+}
+```
+
+#### 2. Pro Plan (if available)
+```json
+{
+ "name": "Pro Plan",
+ "prices": [
+ {
+ "amount": 2500, // $25.00
+ "currency": "usd",
+ "interval": "month"
+ }
+ ]
+}
+```
+
+## Trial System Implementation
+
+### 7-Day Trial Flow
+
+1. **Subscription Creation** (`/api/checkout-sessions`)
+2. **Payment Method Setup** (Setup intent for payment authorization)
+3. **Trial Activation** (`/api/trial/start`)
+4. **Trial Access** (500 credits immediately)
+5. **Auto-Conversion** (After 7 days, Stripe charges automatically)
+
+### Subscription Creation Code
+```typescript
+// app/api/checkout-sessions/route.ts
+// Auto-Analyst creates subscription directly, not via checkout sessions
+const subscription = await stripe.subscriptions.create({
+ customer: customerId,
+ items: [{ price: priceId }],
+ trial_end: trialEndTimestamp, // 7 days from now
+ expand: ['latest_invoice.payment_intent'],
+ payment_behavior: 'default_incomplete',
+ payment_settings: {
+ save_default_payment_method: 'on_subscription',
+ },
+ metadata: {
+ userId: userId,
+ planName,
+ interval,
+ isTrial: 'true'
+ }
+})
+```
+
+## Subscription States
+
+### State Diagram
+```
+[Trial Start] → [trialing] → [active] (payment successful)
+ ↓
+ [canceled] (trial canceled)
+ ↓
+ [subscription deleted]
+```
+
+### Status Mapping
+```typescript
+interface SubscriptionStatus {
+ stripe: 'trialing' | 'active' | 'canceled' | 'past_due' | 'unpaid'
+ redis: 'trialing' | 'active' | 'canceling' | 'canceled' | 'past_due'
+ display: 'Trial' | 'Active' | 'Canceling' | 'Canceled' | 'Past Due'
+}
+```
+
+## Key Stripe Objects
+
+### Customer
+```typescript
+interface StripeCustomer {
+ id: string // cus_ABC123
+ email: string
+ created: number
+ metadata: {
+ userId: string // Our internal user ID
+ }
+}
+```
+
+### Subscription
+```typescript
+interface StripeSubscription {
+ id: string // sub_DEF456
+ customer: string // cus_ABC123
+ status: SubscriptionStatus
+ trial_start?: number // Unix timestamp
+ trial_end?: number // Unix timestamp
+ current_period_start: number // Unix timestamp
+ current_period_end: number // Unix timestamp
+ cancel_at_period_end: boolean // true if canceling
+ canceled_at?: number // Unix timestamp
+ metadata: {
+ userId: string
+ userEmail: string
+ isTrial: string
+ }
+}
+```
+
+### Payment Intent
+```typescript
+interface StripePaymentIntent {
+ id: string // pi_GHI789
+ customer: string // cus_ABC123
+ status: PaymentIntentStatus
+ amount: number
+ metadata: {
+ userId: string
+ isTrial: string
+ }
+}
+```
+
+## Subscription Management
+
+### Creating Trial Subscription
+```typescript
+// Direct subscription creation with trial
+const subscription = await stripe.subscriptions.create({
+ customer: customerId,
+ items: [{ price: priceId }],
+ trial_end: trialEndTimestamp, // 7 days from now
+ payment_behavior: 'default_incomplete',
+ metadata: { userId, isTrial: 'true' }
+})
+```
+
+### Canceling Subscription
+```typescript
+// app/api/trial/cancel/route.ts
+if (subscription.status === 'trialing') {
+ // Immediate cancellation for trials
+ await stripe.subscriptions.cancel(subscriptionId, {
+ prorate: false
+ })
+} else {
+ // Cancel at period end for active subscriptions
+ await stripe.subscriptions.update(subscriptionId, {
+ cancel_at_period_end: true
+ })
+}
+```
+
+### Retrieving Subscription Data
+```typescript
+const subscription = await stripe.subscriptions.retrieve(subscriptionId, {
+ expand: ['latest_invoice', 'customer']
+})
+```
+
+## Payment Processing
+
+### Authorization vs. Capture
+
+#### Trial Authorization
+- **Authorization Only**: Payment method validated and authorized
+- **No Immediate Charge**: User not charged until trial ends
+- **Future Payment**: Stripe automatically charges after 7 days
+
+#### Active Subscription
+- **Immediate Charge**: Regular subscription payments charged immediately
+- **Recurring Billing**: Automatic monthly/yearly charges
+
+### Payment Method Validation
+```typescript
+// Validate payment method exists for trial
+const paymentMethods = await stripe.paymentMethods.list({
+ customer: customerId,
+ type: 'card'
+})
+
+if (paymentMethods.data.length === 0) {
+ throw new Error('No payment method found')
+}
+```
+
+## Error Handling
+
+### Common Stripe Errors
+```typescript
+try {
+ await stripe.subscriptions.create(subscriptionData)
+} catch (error) {
+ switch (error.code) {
+ case 'resource_missing':
+ // Customer or price not found
+ break
+ case 'card_declined':
+ // Payment method declined
+ break
+ case 'authentication_required':
+ // 3D Secure required
+ break
+ default:
+ // Generic error handling
+ }
+}
+```
+
+### Webhook Error Handling
+```typescript
+// app/api/webhooks/route.ts
+export async function POST(request: NextRequest) {
+ let event: Stripe.Event
+
+ try {
+ const body = await request.text()
+ const signature = request.headers.get('stripe-signature')!
+
+ event = stripe.webhooks.constructEvent(
+ body,
+ signature,
+ process.env.STRIPE_WEBHOOK_SECRET!
+ )
+ } catch (err) {
+ console.error('Webhook signature verification failed:', err)
+ return NextResponse.json({ error: 'Invalid signature' }, { status: 400 })
+ }
+
+ // Process event...
+}
+```
+
+## Testing
+
+### Test Cards
+```typescript
+// Successful payment
+'4242424242424242'
+
+// Payment requires authentication
+'4000002500003155'
+
+// Payment declined
+'4000000000000002'
+
+// Insufficient funds
+'4000000000009995'
+```
+
+### Test Scenarios
+
+#### 1. Successful Trial
+```bash
+# Create checkout session
+POST /api/checkout-sessions
+→ Redirects to Stripe checkout
+→ User enters test card 4242424242424242
+→ checkout.session.completed webhook fires
+→ User gets trial access
+```
+
+#### 2. Trial Cancellation
+```bash
+# Cancel during trial
+POST /api/trial/cancel
+→ Stripe subscription canceled
+→ customer.subscription.deleted webhook fires
+→ Credits set to 0
+```
+
+#### 3. Failed Payment Authorization
+```bash
+# Use declined card
+→ payment_intent.payment_failed webhook fires
+→ Trial access prevented
+```
+
+## Monitoring and Logs
+
+### Key Metrics
+- Trial conversion rate
+- Payment success rate
+- Churn rate
+- Revenue metrics
+
+### Logging Strategy
+```typescript
+console.log(`Trial started for user ${userId}`)
+console.log(`Payment succeeded for subscription ${subscriptionId}`)
+console.error(`Payment failed for user ${userId}:`, error)
+```
+
+### Stripe Dashboard
+- Monitor payment failures
+- Track subscription metrics
+- Review webhook deliveries
+- Analyze customer lifecycle
+
+## Security Best Practices
+
+1. **Never expose secret keys** in client-side code
+2. **Validate webhook signatures** for all incoming events
+3. **Use HTTPS** for all webhook endpoints
+4. **Implement idempotency** for webhook processing
+5. **Log security events** for auditing
+6. **Rate limit** API endpoints
+7. **Validate user permissions** before subscription operations
+
+## Production Considerations
+
+### Webhook Reliability
+```typescript
+// Implement retry logic for failed webhook processing
+const maxRetries = 3
+let retryCount = 0
+
+while (retryCount < maxRetries) {
+ try {
+ await processWebhook(event)
+ break
+ } catch (error) {
+ retryCount++
+ if (retryCount === maxRetries) {
+ throw error
+ }
+ await new Promise(resolve => setTimeout(resolve, 1000 * retryCount))
+ }
+}
+```
+
+### Database Consistency
+- Use transactions for critical operations
+- Implement eventual consistency for webhook data
+- Add data validation and sanitization
+- Monitor for data discrepancies
+
+### Performance
+- Cache frequently accessed Stripe data
+- Use webhook data to update local cache
+- Implement request deduplication
+- Monitor API rate limits
\ No newline at end of file
diff --git a/auto-analyst-frontend/docs/trial-system.md b/auto-analyst-frontend/docs/trial-system.md
new file mode 100644
index 00000000..856f4fda
--- /dev/null
+++ b/auto-analyst-frontend/docs/trial-system.md
@@ -0,0 +1,614 @@
+# Trial System
+
+This document covers the complete 7-day trial system implementation in Auto-Analyst, including payment authorization, access management, and conversion logic.
+
+## Overview
+
+Auto-Analyst uses a 7-day free trial with Stripe subscription trials. Users must authorize a payment method to access the trial but are not charged until the trial period ends.
+
+## Trial Flow Architecture
+
+### High-Level Flow
+```
+[Start Trial] → [Payment Auth] → [Trial Access] → [Auto-Convert] OR [Cancel]
+ ↓ ↓ ↓ ↓ ↓
+[Pricing Page] → [Stripe Checkout] → [500 Credits] → [Paid Plan] → [Access Removed]
+```
+
+### Detailed Flow
+1. User clicks "Start 7-Day Trial" on pricing page
+2. Redirected to Stripe checkout with trial subscription
+3. Payment method authorization required (no charge)
+4. `checkout.session.completed` webhook fired
+5. User calls `/api/trial/start` to activate trial
+6. Immediate access with 500 Standard plan credits
+7. After 7 days: Stripe automatically charges OR user cancels
+
+## Implementation Components
+
+### 1. Subscription Creation (Not Checkout Session)
+
+**File**: `app/api/checkout-sessions/route.ts`
+
+```typescript
+export async function POST(request: NextRequest) {
+ const { priceId, userId, planName, interval, promoCode } = await request.json()
+
+ // Create or retrieve customer
+ let customerId = await getOrCreateCustomer(userId)
+
+ // Calculate trial end date
+ const trialEndTimestamp = TrialUtils.getTrialEndTimestamp()
+
+ // Create subscription with trial period
+ const subscription = await stripe.subscriptions.create({
+ customer: customerId,
+ items: [{ price: priceId }],
+ trial_end: trialEndTimestamp,
+ expand: ['latest_invoice.payment_intent'],
+ payment_behavior: 'default_incomplete',
+ payment_settings: {
+ save_default_payment_method: 'on_subscription',
+ },
+ metadata: {
+ userId: userId || 'anonymous',
+ planName,
+ interval,
+ priceId,
+ isTrial: 'true',
+ trialEndDate: TrialUtils.getTrialEndDate(),
+ },
+ // Apply coupon if provided
+ ...(couponId && { coupon: couponId })
+ })
+
+ // Create setup intent for payment method collection if needed
+ let clientSecret = subscription.latest_invoice?.payment_intent?.client_secret
+
+ if (!clientSecret && subscription.status === 'trialing') {
+ const setupIntent = await stripe.setupIntents.create({
+ customer: customerId,
+ usage: 'off_session',
+ metadata: {
+ subscription_id: subscription.id,
+ is_trial_setup: 'true',
+ userId: userId || 'anonymous',
+ isTrial: 'true',
+ planName,
+ interval,
+ },
+ })
+ clientSecret = setupIntent.client_secret
+ }
+
+ return NextResponse.json({
+ subscriptionId: subscription.id,
+ clientSecret: clientSecret,
+ trialEnd: subscription.trial_end,
+ isTrialSetup: !subscription.latest_invoice?.payment_intent
+ })
+}
+```
+
+### 2. Trial Activation
+
+**File**: `app/api/trial/start/route.ts`
+
+```typescript
+export async function POST(request: NextRequest) {
+ const { subscriptionId, planName, interval, amount } = await request.json()
+ const token = await getToken({ req: request })
+ const userId = token.sub
+
+ // Retrieve and validate subscription
+ const subscription = await stripe.subscriptions.retrieve(subscriptionId)
+
+ if (subscription.status !== 'trialing') {
+ return NextResponse.json(
+ { error: 'Subscription is not in trial status' },
+ { status: 400 }
+ )
+ }
+
+ // Validate payment method is attached
+ const hasPaymentMethod = await validatePaymentMethod(subscription)
+
+ if (!hasPaymentMethod) {
+ return NextResponse.json(
+ { error: 'Payment method setup required. Please complete payment method verification.' },
+ { status: 400 }
+ )
+ }
+
+ // Store customer mapping for webhooks
+ await redis.set(`stripe:customer:${subscription.customer}`, userId)
+
+ // Store subscription data
+ const now = new Date()
+ const trialEndDate = TrialUtils.getTrialEndDate(now)
+
+ await redis.hset(KEYS.USER_SUBSCRIPTION(userId), {
+ plan: planName,
+ planType: 'STANDARD',
+ status: 'trialing',
+ amount: amount ? amount.toString() : '15',
+ interval: interval || 'month',
+ purchaseDate: now.toISOString(),
+ renewalDate: trialEndDate,
+ lastUpdated: now.toISOString(),
+ stripeCustomerId: subscription.customer as string,
+ stripeSubscriptionId: subscription.id,
+ trialEndDate: trialEndDate
+ })
+
+ // Initialize trial credits
+ await creditUtils.initializeTrialCredits(userId, {
+ total: TrialUtils.getTrialCredits(),
+ resetDate: CreditConfig.getNextResetDate()
+ })
+
+ return NextResponse.json({
+ success: true,
+ trialStarted: true,
+ credits: TrialUtils.getTrialCredits(),
+ trialEndDate: trialEndDate,
+ subscriptionId: subscription.id
+ })
+}
+```
+
+### 3. Payment Method Validation
+
+```typescript
+async function validatePaymentMethod(customerId: string): Promise {
+ try {
+ // Check for attached payment methods
+ const paymentMethods = await stripe.paymentMethods.list({
+ customer: customerId,
+ type: 'card'
+ })
+
+ if (paymentMethods.data.length > 0) return true
+
+ // Check customer's default payment method
+ const customer = await stripe.customers.retrieve(customerId)
+ if (customer.invoice_settings?.default_payment_method) return true
+
+ // Check setup intents for this customer
+ const setupIntents = await stripe.setupIntents.list({
+ customer: customerId,
+ limit: 1
+ })
+
+ return setupIntents.data.some(si => si.status === 'succeeded')
+ } catch (error) {
+ console.error('Payment method validation error:', error)
+ return false
+ }
+}
+```
+
+### 4. Trial Cancellation
+
+**File**: `app/api/trial/cancel/route.ts`
+
+```typescript
+export async function POST(request: NextRequest) {
+ const token = await getToken({ req: request })
+ const userId = token.sub
+
+ const subscriptionData = await redis.hgetall(KEYS.USER_SUBSCRIPTION(userId))
+ const subscriptionId = subscriptionData.stripeSubscriptionId
+
+ if (!subscriptionId) {
+ return NextResponse.json(
+ { error: 'No subscription found' },
+ { status: 404 }
+ )
+ }
+
+ const subscription = await stripe.subscriptions.retrieve(subscriptionId)
+
+ if (subscription.status === 'trialing') {
+ // Immediate cancellation for trials
+ await stripe.subscriptions.cancel(subscriptionId, {
+ prorate: false
+ })
+
+ // Mark as trial cancellation
+ await redis.hset(KEYS.USER_CREDITS(userId), {
+ trialCanceled: 'true'
+ })
+
+ return NextResponse.json({
+ success: true,
+ canceled: true,
+ creditsRemoved: true,
+ message: 'Trial canceled successfully'
+ })
+ } else {
+ // Cancel at period end for active subscriptions
+ await stripe.subscriptions.update(subscriptionId, {
+ cancel_at_period_end: true
+ })
+
+ const periodEnd = new Date(subscription.current_period_end * 1000)
+
+ return NextResponse.json({
+ success: true,
+ canceledAtPeriodEnd: true,
+ accessUntil: periodEnd.toISOString(),
+ message: 'Subscription will cancel at period end'
+ })
+ }
+}
+```
+
+## Trial States
+
+### State Management
+
+```typescript
+interface TrialState {
+ status: 'trialing' | 'active' | 'canceled'
+ trialStart?: string // ISO timestamp
+ trialEnd?: string // ISO timestamp
+ hasPaymentMethod: boolean
+ creditsGranted: boolean
+ autoConvertEnabled: boolean
+}
+```
+
+### State Transitions
+
+```mermaid
+graph TD
+ A[Start Trial] --> B{Payment Auth?}
+ B -->|Success| C[Trialing]
+ B -->|Failed| D[No Access]
+ C --> E{7 Days Later}
+ E -->|Auto-Convert| F[Active Subscription]
+ E -->|User Cancels| G[Canceled]
+ C -->|User Cancels| G
+ F -->|User Cancels| H[Cancel at Period End]
+ G --> I[Access Removed]
+ H --> I
+```
+
+### Status Synchronization
+
+**Redis Storage**:
+```typescript
+// user:${userId}:subscription
+{
+ status: 'trialing',
+ displayStatus: 'trialing',
+ stripeSubscriptionStatus: 'trialing',
+ trialStartDate: '2024-01-15T10:30:00.000Z',
+ trialEndDate: '2024-01-22T10:30:00.000Z'
+}
+```
+
+**Stripe to Redis Mapping**:
+```typescript
+const statusMapping = {
+ 'trialing': { status: 'trialing', displayStatus: 'Trial' },
+ 'active': { status: 'active', displayStatus: 'Active' },
+ 'canceled': { status: 'canceled', displayStatus: 'Canceled' },
+ 'past_due': { status: 'past_due', displayStatus: 'Past Due' }
+}
+```
+
+## Webhook Integration
+
+### Trial Conversion Webhook
+
+**Event**: `invoice.payment_succeeded`
+
+```typescript
+// When trial ends and first payment succeeds
+if (invoice.billing_reason === 'subscription_create') {
+ await redis.hset(KEYS.USER_SUBSCRIPTION(userId), {
+ status: 'active',
+ stripeSubscriptionStatus: 'active',
+ displayStatus: 'active',
+ trialToActiveDate: new Date().toISOString(),
+ trialEndedAt: new Date().toISOString()
+ })
+
+ // Refresh credits for new active status
+ await subscriptionUtils.refreshCreditsIfNeeded(userId)
+}
+```
+
+### Trial Cancellation Webhook
+
+**Event**: `customer.subscription.deleted`
+
+```typescript
+await redis.hset(KEYS.USER_SUBSCRIPTION(userId), {
+ status: 'canceled',
+ stripeSubscriptionStatus: 'canceled',
+ displayStatus: 'canceled',
+ canceledAt: new Date().toISOString(),
+ subscriptionDeleted: 'true'
+})
+
+// Zero out credits for canceled trials
+await creditUtils.setZeroCredits(userId)
+```
+
+## Payment Authorization
+
+### Stripe Configuration
+
+```typescript
+// No immediate charge during trial
+const subscription = await stripe.subscriptions.create({
+ customer: customerId,
+ items: [{ price: priceId }],
+ trial_period_days: 7,
+
+ // Payment method will be charged after trial
+ payment_behavior: 'default_incomplete',
+ expand: ['latest_invoice.payment_intent']
+})
+```
+
+### Authorization Validation
+
+```typescript
+// Ensure payment method is authorized before granting access
+const paymentIntent = subscription.latest_invoice?.payment_intent
+
+if (paymentIntent?.status !== 'succeeded') {
+ throw new Error('Payment authorization required')
+}
+```
+
+## Credit Management
+
+### Trial Credit Allocation
+
+```typescript
+// Initialize 500 credits for trial users
+await creditUtils.initializeTrialCredits(userId, {
+ total: 500,
+ resetDate: CreditConfig.getNextResetDate(),
+ isTrialCredits: 'true',
+ paymentIntentId: session.payment_intent.id
+})
+```
+
+### Credit Preservation Logic
+
+```typescript
+// Credits preserved during successful trial conversion
+// Only zeroed for genuine cancellations
+
+function shouldZeroCredits(creditsData: any): boolean {
+ // Genuine trial cancellation (user canceled before payment)
+ if (creditsData.trialCanceled === 'true') return true
+
+ // Subscription deleted (not converted to paid)
+ if (creditsData.subscriptionDeleted === 'true') return true
+
+ return false
+}
+```
+
+## Frontend Integration
+
+### Trial Start Button
+
+```typescript
+// components/pricing/TrialButton.tsx
+function TrialButton({ priceId, planType }: TrialButtonProps) {
+ const [isLoading, setIsLoading] = useState(false)
+
+ const startTrial = async () => {
+ setIsLoading(true)
+
+ try {
+ // Create checkout session
+ const response = await fetch('/api/checkout-sessions', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ priceId, planType })
+ })
+
+ const { url } = await response.json()
+
+ // Redirect to Stripe checkout
+ window.location.href = url
+ } catch (error) {
+ console.error('Trial start failed:', error)
+ setIsLoading(false)
+ }
+ }
+
+ return (
+
+ )
+}
+```
+
+### Checkout Success Handler
+
+```typescript
+// app/checkout/success/page.tsx
+'use client'
+
+export default function CheckoutSuccess() {
+ const searchParams = useSearchParams()
+ const sessionId = searchParams.get('session_id')
+
+ useEffect(() => {
+ if (sessionId) {
+ startTrial(sessionId)
+ }
+ }, [sessionId])
+
+ const startTrial = async (sessionId: string) => {
+ try {
+ const response = await fetch('/api/trial/start', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ sessionId })
+ })
+
+ if (response.ok) {
+ router.push('/chat?from=trial&refresh=true')
+ } else {
+ const error = await response.json()
+ console.error('Trial activation failed:', error)
+ }
+ } catch (error) {
+ console.error('Trial start error:', error)
+ }
+ }
+}
+```
+
+## Error Handling
+
+### Common Error Scenarios
+
+1. **Payment Method Declined**
+ ```typescript
+ // Webhook: payment_intent.payment_failed
+ // Action: Prevent trial access, show error message
+ ```
+
+2. **Checkout Session Expired**
+ ```typescript
+ // API Response: 400 Bad Request
+ // Message: "Checkout session expired"
+ ```
+
+3. **Trial Already Started**
+ ```typescript
+ // API Response: 400 Bad Request
+ // Message: "Trial already active for this user"
+ ```
+
+4. **3D Secure Required**
+ ```typescript
+ // Webhook: payment_intent.requires_action
+ // Action: Redirect user to complete authentication
+ ```
+
+### Error Recovery
+
+```typescript
+// Automatic retry for failed payment authorization
+const retryPaymentSetup = async (customerId: string) => {
+ const setupIntent = await stripe.setupIntents.create({
+ customer: customerId,
+ payment_method_types: ['card'],
+ usage: 'off_session'
+ })
+
+ return setupIntent.client_secret
+}
+```
+
+## Testing
+
+### Test Scenarios
+
+1. **Successful Trial Flow**
+ ```bash
+ # Use test card: 4242424242424242
+ # Expected: Trial access granted immediately
+ ```
+
+2. **Declined Payment**
+ ```bash
+ # Use test card: 4000000000000002
+ # Expected: Trial access denied
+ ```
+
+3. **3D Secure Authentication**
+ ```bash
+ # Use test card: 4000002500003155
+ # Expected: Additional authentication required
+ ```
+
+4. **Trial Cancellation**
+ ```bash
+ # Cancel within 7 days
+ # Expected: Immediate access removal, no charge
+ ```
+
+5. **Trial Conversion**
+ ```bash
+ # Wait 7 days (or use Stripe CLI)
+ # Expected: Automatic payment, continued access
+ ```
+
+### Stripe CLI Testing
+
+```bash
+# Trigger trial end event
+stripe trigger invoice.payment_succeeded \
+ --add invoice:billing_reason=subscription_create
+
+# Trigger trial cancellation
+stripe trigger customer.subscription.deleted
+```
+
+## Monitoring
+
+### Key Metrics
+
+- **Trial Conversion Rate**: % of trials that convert to paid
+- **Trial Cancellation Rate**: % of trials canceled before conversion
+- **Payment Authorization Success**: % of successful payment setups
+- **Time to Trial Start**: Average time from signup to trial access
+
+### Logging
+
+```typescript
+// Trial events logging
+console.log('Trial started:', {
+ userId,
+ trialEndDate,
+ creditsGranted: 500,
+ paymentMethodVerified: true
+})
+
+console.log('Trial converted:', {
+ userId,
+ conversionDate: new Date().toISOString(),
+ totalTrialDays: 7
+})
+
+console.log('Trial canceled:', {
+ userId,
+ cancellationDate: new Date().toISOString(),
+ daysCanceled: daysIntoTrial
+})
+```
+
+## Security Considerations
+
+### Payment Authorization Validation
+- Always verify payment method exists before granting access
+- Validate Stripe webhook signatures
+- Check session completion status
+- Prevent duplicate trial activations
+
+### User Access Control
+- Trial access only with valid subscription
+- Credit limits enforced server-side
+- Session-based authentication required
+- No client-side access control bypass
+
+### Data Protection
+- Encrypt sensitive payment data
+- Log security events
+- Monitor for unusual patterns
+- Secure API endpoints with rate limiting
\ No newline at end of file
diff --git a/auto-analyst-frontend/docs/webhooks.md b/auto-analyst-frontend/docs/webhooks.md
new file mode 100644
index 00000000..272e5db2
--- /dev/null
+++ b/auto-analyst-frontend/docs/webhooks.md
@@ -0,0 +1,433 @@
+# Webhooks
+
+This document covers all Stripe webhooks implemented in Auto-Analyst, their purposes, and data synchronization logic.
+
+## Overview
+
+Auto-Analyst uses Stripe webhooks to maintain real-time synchronization between Stripe and our Redis database. All webhooks are processed through a single endpoint: `/api/webhooks/route.ts`
+
+## Webhook Configuration
+
+### Endpoint URL
+```
+https://your-domain.com/api/webhooks
+```
+
+### Required Events
+The following events must be configured in your Stripe dashboard:
+
+```typescript
+const requiredEvents = [
+ 'checkout.session.completed',
+ 'customer.subscription.updated',
+ 'customer.subscription.deleted',
+ 'customer.subscription.trial_will_end',
+ 'invoice.payment_succeeded',
+ 'invoice.payment_failed',
+ 'payment_intent.payment_failed',
+ 'payment_intent.canceled',
+ 'setup_intent.setup_failed',
+ 'payment_intent.requires_action'
+]
+```
+
+## Webhook Security
+
+### Signature Verification
+```typescript
+export async function POST(request: NextRequest) {
+ const signature = request.headers.get('stripe-signature')
+
+ if (!signature) {
+ return NextResponse.json({ error: 'No Stripe signature found' }, { status: 400 })
+ }
+
+ const rawBody = await getRawBody(request.body as unknown as Readable)
+
+ try {
+ event = stripe.webhooks.constructEvent(rawBody, signature, webhookSecret)
+ } catch (err: any) {
+ console.error(`⚠️ Webhook signature verification failed.`, err.message)
+ return NextResponse.json({ error: `Webhook Error: ${err.message}` }, { status: 400 })
+ }
+
+ // Process event...
+}
+```
+
+### Environment Variables
+```bash
+STRIPE_WEBHOOK_SECRET=whsec_... # From Stripe Dashboard
+```
+
+## Webhook Events
+
+### 1. `checkout.session.completed`
+
+**Purpose**: Logs successful checkout completion (legacy - now handled by trial flow)
+
+**Processing Logic**:
+```typescript
+case 'checkout.session.completed': {
+ const session = event.data.object as Stripe.Checkout.Session
+ console.log(`Checkout session completed: ${session.id} - handled by trial flow`)
+ return NextResponse.json({ received: true })
+}
+```
+
+**Redis Updates**: None (logging only)
+
+### 2. `customer.subscription.updated`
+
+**Purpose**: Synchronizes subscription status changes
+
+**Processing Logic**:
+```typescript
+case 'customer.subscription.updated': {
+ const subscription = event.data.object as Stripe.Subscription
+ const userId = await getUserIdFromCustomerId(subscription.customer as string)
+
+ const updateData: any = {
+ status: subscription.status,
+ lastUpdated: new Date().toISOString(),
+ stripeSubscriptionStatus: subscription.status
+ }
+
+ // Handle specific status transitions
+ if (currentStatus === 'trialing' && subscription.status === 'active') {
+ updateData.trialEndedAt = new Date().toISOString()
+ updateData.trialToActiveDate = new Date().toISOString()
+ }
+
+ if (subscription.status === 'canceled') {
+ updateData.canceledAt = new Date().toISOString()
+ }
+
+ await redis.hset(KEYS.USER_SUBSCRIPTION(userId), updateData)
+ break
+}
+```
+
+**Redis Updates**:
+- Subscription status
+- Status transition timestamps
+- Stripe synchronization data
+
+### 3. `customer.subscription.deleted`
+
+**Purpose**: Handles subscription cancellation and cleanup
+
+**Processing Logic**:
+```typescript
+case 'customer.subscription.deleted': {
+ const subscription = event.data.object as Stripe.Subscription
+ const userId = await getUserIdFromCustomerId(subscription.customer as string)
+
+ // Set credits to 0 immediately when subscription is canceled
+ await creditUtils.setZeroCredits(userId)
+
+ // Update subscription data to reflect cancellation
+ await redis.hset(KEYS.USER_SUBSCRIPTION(userId), {
+ plan: 'No Active Plan',
+ planType: 'NONE',
+ status: 'canceled',
+ amount: '0',
+ interval: 'month',
+ lastUpdated: now.toISOString(),
+ canceledAt: now.toISOString(),
+ stripeCustomerId: '',
+ stripeSubscriptionId: ''
+ })
+
+ // Mark credits as subscription deleted
+ await redis.hset(KEYS.USER_CREDITS(userId), {
+ total: '0',
+ used: '0',
+ resetDate: '',
+ lastUpdate: now.toISOString(),
+ subscriptionDeleted: 'true'
+ })
+
+ break
+}
+```
+
+**Redis Updates**:
+- Subscription status → 'canceled'
+- Credits → 0
+- Plan → 'No Active Plan'
+- Cleanup Stripe IDs
+
+### 4. `customer.subscription.trial_will_end`
+
+**Purpose**: Logs upcoming trial expiration (for potential email notifications)
+
+**Processing Logic**:
+```typescript
+case 'customer.subscription.trial_will_end': {
+ const subscription = event.data.object as Stripe.Subscription
+ const userId = await getUserIdFromCustomerId(subscription.customer as string)
+
+ // Optional: Send reminder email about trial ending
+ // You can add email notification logic here
+
+ return NextResponse.json({ received: true })
+}
+```
+
+**Redis Updates**: None (logging only)
+
+### 5. `invoice.payment_succeeded`
+
+**Purpose**: Handles successful payments and trial conversions
+
+**Processing Logic**:
+```typescript
+case 'invoice.payment_succeeded': {
+ const invoice = event.data.object as Stripe.Invoice
+
+ if (invoice.subscription) {
+ const subscription = await stripe.subscriptions.retrieve(invoice.subscription as string)
+ const userId = await getUserIdFromCustomerId(subscription.customer as string)
+
+ if (invoice.billing_reason === 'subscription_cycle') {
+ // Trial ended, first payment successful
+ await redis.hset(KEYS.USER_SUBSCRIPTION(userId), {
+ status: 'active',
+ lastUpdated: new Date().toISOString(),
+ trialEndedAt: new Date().toISOString(),
+ lastPaymentDate: new Date().toISOString(),
+ stripeSubscriptionStatus: subscription.status
+ })
+ } else if (invoice.billing_reason === 'subscription_create') {
+ // Initial subscription creation payment
+ await redis.hset(KEYS.USER_SUBSCRIPTION(userId), {
+ status: subscription.status,
+ lastUpdated: new Date().toISOString(),
+ initialPaymentDate: new Date().toISOString(),
+ stripeSubscriptionStatus: subscription.status
+ })
+ }
+ }
+
+ return NextResponse.json({ received: true })
+}
+```
+
+**Redis Updates**:
+- Status activation
+- Payment timestamps
+- Trial conversion tracking
+
+### 6. `invoice.payment_failed`
+
+**Purpose**: Handles failed recurring payments
+
+**Processing Logic**:
+```typescript
+case 'invoice.payment_failed': {
+ const invoice = event.data.object as Stripe.Invoice
+
+ if (invoice.subscription && invoice.billing_reason === 'subscription_cycle') {
+ const subscription = await stripe.subscriptions.retrieve(invoice.subscription as string)
+ const userId = await getUserIdFromCustomerId(subscription.customer as string)
+
+ // Set credits to 0 and mark subscription as past_due
+ await creditUtils.setZeroCredits(userId)
+
+ await redis.hset(KEYS.USER_SUBSCRIPTION(userId), {
+ status: 'past_due',
+ lastUpdated: new Date().toISOString(),
+ paymentFailedAt: new Date().toISOString()
+ })
+ }
+
+ return NextResponse.json({ received: true })
+}
+```
+
+**Redis Updates**:
+- Status → 'past_due'
+- Credits → 0
+- Payment failure timestamp
+
+### 7. Payment Protection Events
+
+These events prevent trial access when payment authorization fails:
+
+#### `payment_intent.payment_failed`
+```typescript
+case 'payment_intent.payment_failed': {
+ const paymentIntent = event.data.object as Stripe.PaymentIntent
+
+ if (paymentIntent.metadata?.isTrial === 'true') {
+ const userId = paymentIntent.metadata?.userId
+
+ if (userId) {
+ // Prevent trial access by ensuring credits remain at 0
+ await creditUtils.setZeroCredits(userId)
+
+ await redis.hset(KEYS.USER_SUBSCRIPTION(userId), {
+ status: 'payment_failed',
+ lastUpdated: new Date().toISOString(),
+ paymentFailedAt: new Date().toISOString(),
+ failureReason: 'Payment authorization failed during trial signup'
+ })
+ }
+ }
+ break
+}
+```
+
+#### `payment_intent.canceled`
+```typescript
+case 'payment_intent.canceled': {
+ const paymentIntent = event.data.object as Stripe.PaymentIntent
+
+ if (paymentIntent.metadata?.isTrial === 'true') {
+ const userId = paymentIntent.metadata?.userId
+
+ if (userId) {
+ await creditUtils.setZeroCredits(userId)
+
+ await redis.hset(KEYS.USER_SUBSCRIPTION(userId), {
+ status: 'canceled',
+ lastUpdated: new Date().toISOString(),
+ canceledAt: new Date().toISOString(),
+ cancelReason: 'Payment intent canceled during trial signup'
+ })
+ }
+ }
+ break
+}
+```
+
+#### `setup_intent.setup_failed`
+```typescript
+case 'setup_intent.setup_failed': {
+ const setupIntent = event.data.object as Stripe.SetupIntent
+
+ if (setupIntent.metadata?.is_trial_setup === 'true') {
+ const subscriptionId = setupIntent.metadata?.subscription_id
+
+ if (subscriptionId) {
+ const subscription = await stripe.subscriptions.retrieve(subscriptionId)
+ const userId = await getUserIdFromCustomerId(subscription.customer as string)
+
+ // Cancel the trial subscription since setup failed
+ await stripe.subscriptions.cancel(subscriptionId)
+
+ // Ensure user doesn't get trial access
+ await creditUtils.setZeroCredits(userId)
+
+ await redis.hset(KEYS.USER_SUBSCRIPTION(userId), {
+ status: 'setup_failed',
+ lastUpdated: new Date().toISOString(),
+ setupFailedAt: new Date().toISOString(),
+ failureReason: 'Payment method setup failed during trial signup'
+ })
+ }
+ }
+ break
+}
+```
+
+#### `payment_intent.requires_action`
+```typescript
+case 'payment_intent.requires_action': {
+ const paymentIntent = event.data.object as Stripe.PaymentIntent
+
+ if (paymentIntent.metadata?.isTrial === 'true') {
+ const userId = paymentIntent.metadata?.userId
+ console.log(`Trial payment requires 3D Secure authentication for user ${userId}`)
+
+ // Don't grant trial access until authentication is complete
+ }
+ break
+}
+```
+
+## Helper Functions
+
+### User ID Resolution
+```typescript
+async function getUserIdFromCustomerId(customerId: string): Promise {
+ try {
+ const userId = await redis.get(`stripe:customer:${customerId}`)
+ return userId
+ } catch (error) {
+ console.error('Error getting userId from Redis:', error)
+ return null
+ }
+}
+```
+
+### Error Handling
+```typescript
+try {
+ // Process webhook event
+ await processWebhookEvent(event)
+ return NextResponse.json({ received: true })
+} catch (error: any) {
+ console.error('Webhook error:', error)
+ return NextResponse.json({ error: error.message || 'Webhook handler failed' }, { status: 500 })
+}
+```
+
+## Webhook Testing
+
+### Local Development
+```bash
+# Install Stripe CLI
+stripe listen --forward-to localhost:3000/api/webhooks
+
+# Trigger test events
+stripe trigger customer.subscription.updated
+stripe trigger invoice.payment_succeeded
+```
+
+### Test Event Data
+```typescript
+// Mock subscription update
+{
+ "id": "evt_test_webhook",
+ "object": "event",
+ "type": "customer.subscription.updated",
+ "data": {
+ "object": {
+ "id": "sub_test123",
+ "customer": "cus_test123",
+ "status": "active",
+ "trial_end": 1643723400
+ }
+ }
+}
+```
+
+## Monitoring
+
+### Webhook Delivery
+- Check Stripe Dashboard → Webhooks → Endpoint logs
+- Monitor delivery success/failure rates
+- Review retry attempts
+
+### Error Tracking
+```typescript
+// Log all webhook events for debugging
+console.log('Webhook received:', {
+ type: event.type,
+ id: event.id,
+ created: event.created,
+ livemode: event.livemode
+})
+```
+
+## Best Practices
+
+1. **Idempotency**: Handle duplicate events gracefully
+2. **Fast Response**: Return 200 quickly, process asynchronously if needed
+3. **Error Handling**: Return 5xx for retriable errors, 4xx for permanent failures
+4. **Logging**: Log all events for debugging and monitoring
+5. **Validation**: Verify event structure before processing
+6. **Security**: Always verify webhook signatures
+7. **Data Consistency**: Use atomic operations for Redis updates
\ No newline at end of file
diff --git a/auto-analyst-frontend/lib/README-Credits.md b/auto-analyst-frontend/lib/README-Credits.md
index 1aef88ad..6c787a74 100644
--- a/auto-analyst-frontend/lib/README-Credits.md
+++ b/auto-analyst-frontend/lib/README-Credits.md
@@ -95,7 +95,7 @@ const shouldWarn = CreditConfig.shouldWarnLowCredits(85, 100) // true (85% usage
```
## Files Updated
-
+git
The following files have been updated to use the centralized configuration:
### Frontend
@@ -105,7 +105,6 @@ The following files have been updated to use the centralized configuration:
### Backend APIs
- `app/api/user/data/route.ts` - User data retrieval
- `app/api/update-credits/route.ts` - Credit updates after payment
-- `app/api/user/downgrade-plan/route.ts` - Plan downgrades
- `app/api/webhooks/route.ts` - Stripe webhook handling
- `app/api/user/cancel-subscription/route.ts` - Subscription cancellation
- `app/api/initialize-credits/route.ts` - Credit initialization
diff --git a/auto-analyst-frontend/lib/credits-config.ts b/auto-analyst-frontend/lib/credits-config.ts
index 6d4afb55..8b84ce38 100644
--- a/auto-analyst-frontend/lib/credits-config.ts
+++ b/auto-analyst-frontend/lib/credits-config.ts
@@ -49,9 +49,9 @@ export interface TrialConfig {
* Trial period configuration - Change here to update across the entire app
*/
export const TRIAL_CONFIG: TrialConfig = {
- duration: 5,
+ duration: 10,
unit: 'minutes',
- displayText: '5-Minute Trial',
+ displayText: '10-Minute Trial',
credits: 500
}
diff --git a/auto-analyst-frontend/lib/redis.ts b/auto-analyst-frontend/lib/redis.ts
index b704b334..3efb7ffb 100644
--- a/auto-analyst-frontend/lib/redis.ts
+++ b/auto-analyst-frontend/lib/redis.ts
@@ -212,10 +212,10 @@ export const subscriptionUtils = {
const creditsData = await redis.hgetall(KEYS.USER_CREDITS(userId));
// Default values using centralized config
- const defaultCredits = CreditConfig.getCreditsForPlan('Free')
+ const defaultCredits = CreditConfig.getCreditsByType('STANDARD')
let plan = defaultCredits.displayName;
let isPro = false;
- let creditsTotal = defaultCredits.total;
+ let creditsTotal = 0; // No more free credits
let creditsUsed = 0;
// Parse subscription data if found
@@ -248,13 +248,12 @@ export const subscriptionUtils = {
} catch (error) {
console.error('Error getting user subscription data:', error);
// Return fallback defaults using centralized config
- const defaultCredits = CreditConfig.getCreditsForPlan('Free')
return {
- plan: defaultCredits.displayName,
+ plan: 'No Plan',
credits: {
used: 0,
- total: defaultCredits.total,
- remaining: defaultCredits.total
+ total: 0,
+ remaining: 0
},
isPro: false
};
@@ -266,16 +265,9 @@ export const subscriptionUtils = {
try {
const subscriptionData = await redis.hgetall(KEYS.USER_SUBSCRIPTION(userId));
- // Check if this is a Free plan (missing data is treated as Free)
- const isFree =
- !subscriptionData ||
- !subscriptionData.planType ||
- subscriptionData.planType === 'FREE' ||
- (subscriptionData.plan && (subscriptionData.plan as string).includes('Free'));
-
- // Free plans are always considered active
- if (isFree) {
- return true;
+ // No free plans anymore - users without valid subscription are inactive
+ if (!subscriptionData || !subscriptionData.planType || !subscriptionData.status) {
+ return false;
}
// For paid plans, check status and expiration