From 7a11daa87812fb46dc847daf8ef8ddf79fdf3ddc Mon Sep 17 00:00:00 2001 From: Marc Mintel Date: Mon, 15 Dec 2025 22:11:55 +0100 Subject: [PATCH] module creation --- .../app/api/auth/complete-onboarding/route.ts | 90 -------- apps/website/app/api/auth/login/route.ts | 26 --- apps/website/app/api/auth/logout/route.ts | 11 - apps/website/app/api/auth/session/route.ts | 11 - apps/website/app/api/auth/signup/route.ts | 26 --- apps/website/app/api/avatar/generate/route.ts | 84 -------- .../app/api/avatar/validate-face/route.ts | 40 ---- .../app/api/leagues/schedule-preview/route.ts | 100 --------- .../app/api/payments/membership-fees/route.ts | 171 --------------- apps/website/app/api/payments/prizes/route.ts | 180 ---------------- apps/website/app/api/payments/route.ts | 141 ------------ .../website/app/api/payments/wallets/route.ts | 142 ------------ apps/website/app/api/signup/route.ts | 114 ---------- .../app/api/sponsors/dashboard/route.ts | 37 ---- apps/website/app/api/sponsors/route.ts | 50 ----- .../app/api/sponsors/sponsorships/route.ts | 37 ---- .../api/wallets/[leagueId]/withdraw/route.ts | 202 ------------------ 17 files changed, 1462 deletions(-) delete mode 100644 apps/website/app/api/auth/complete-onboarding/route.ts delete mode 100644 apps/website/app/api/auth/login/route.ts delete mode 100644 apps/website/app/api/auth/logout/route.ts delete mode 100644 apps/website/app/api/auth/session/route.ts delete mode 100644 apps/website/app/api/auth/signup/route.ts delete mode 100644 apps/website/app/api/avatar/generate/route.ts delete mode 100644 apps/website/app/api/avatar/validate-face/route.ts delete mode 100644 apps/website/app/api/leagues/schedule-preview/route.ts delete mode 100644 apps/website/app/api/payments/membership-fees/route.ts delete mode 100644 apps/website/app/api/payments/prizes/route.ts delete mode 100644 apps/website/app/api/payments/route.ts delete mode 100644 apps/website/app/api/payments/wallets/route.ts delete mode 100644 apps/website/app/api/signup/route.ts delete mode 100644 apps/website/app/api/sponsors/dashboard/route.ts delete mode 100644 apps/website/app/api/sponsors/route.ts delete mode 100644 apps/website/app/api/sponsors/sponsorships/route.ts delete mode 100644 apps/website/app/api/wallets/[leagueId]/withdraw/route.ts diff --git a/apps/website/app/api/auth/complete-onboarding/route.ts b/apps/website/app/api/auth/complete-onboarding/route.ts deleted file mode 100644 index d0cd10038..000000000 --- a/apps/website/app/api/auth/complete-onboarding/route.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server'; -import { getAuthService } from '@/lib/auth'; -import { getDriverRepository } from '@/lib/di-container'; -import { Driver } from '@gridpilot/racing'; - -export async function POST(request: NextRequest) { - try { - const authService = getAuthService(); - const session = await authService.getCurrentSession(); - - if (!session) { - return NextResponse.json( - { error: 'Not authenticated' }, - { status: 401 } - ); - } - - const body = await request.json(); - const { firstName, lastName, displayName, country, timezone, bio } = body; - - // Validation - if (!firstName || !firstName.trim()) { - return NextResponse.json( - { error: 'First name is required' }, - { status: 400 } - ); - } - - if (!lastName || !lastName.trim()) { - return NextResponse.json( - { error: 'Last name is required' }, - { status: 400 } - ); - } - - if (!displayName || displayName.trim().length < 3) { - return NextResponse.json( - { error: 'Display name must be at least 3 characters' }, - { status: 400 } - ); - } - - if (!country) { - return NextResponse.json( - { error: 'Country is required' }, - { status: 400 } - ); - } - - const driverRepo = getDriverRepository(); - - // Check if user already has a driver profile - if (session.user.primaryDriverId) { - const existingDriver = await driverRepo.findById(session.user.primaryDriverId); - if (existingDriver) { - return NextResponse.json( - { error: 'Driver profile already exists' }, - { status: 400 } - ); - } - } - - // Create the driver profile - const driverId = crypto.randomUUID(); - const driver = Driver.create({ - id: driverId, - iracingId: '', // Will be set later via OAuth - name: displayName.trim(), - country: country, - bio: bio || undefined, - }); - - await driverRepo.create(driver); - - // Update user's primary driver ID in session - // Note: This would typically update the user record and refresh the session - // For now we'll just return success and let the client handle navigation - - return NextResponse.json({ - success: true, - driverId: driverId, - }); - } catch (error) { - console.error('Onboarding error:', error); - return NextResponse.json( - { error: error instanceof Error ? error.message : 'Failed to complete onboarding' }, - { status: 500 } - ); - } -} \ No newline at end of file diff --git a/apps/website/app/api/auth/login/route.ts b/apps/website/app/api/auth/login/route.ts deleted file mode 100644 index fa415ef48..000000000 --- a/apps/website/app/api/auth/login/route.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { NextResponse } from 'next/server'; -import { getAuthService } from '@/lib/auth'; - -export async function POST(request: Request) { - try { - const body = await request.json(); - const { email, password } = body; - - if (!email || !password) { - return NextResponse.json( - { error: 'Email and password are required' }, - { status: 400 } - ); - } - - const authService = getAuthService(); - const session = await authService.loginWithEmail({ email, password }); - - return NextResponse.json({ success: true, session }); - } catch (error) { - return NextResponse.json( - { error: error instanceof Error ? error.message : 'Login failed' }, - { status: 401 } - ); - } -} \ No newline at end of file diff --git a/apps/website/app/api/auth/logout/route.ts b/apps/website/app/api/auth/logout/route.ts deleted file mode 100644 index c2a4170e9..000000000 --- a/apps/website/app/api/auth/logout/route.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { NextResponse } from 'next/server'; -import { getAuthService } from '@/lib/auth'; - -export async function POST(request: Request) { - const authService = getAuthService(); - await authService.logout(); - - const url = new URL(request.url); - const redirectUrl = new URL('/', url.origin); - return NextResponse.redirect(redirectUrl); -} \ No newline at end of file diff --git a/apps/website/app/api/auth/session/route.ts b/apps/website/app/api/auth/session/route.ts deleted file mode 100644 index e2f2dbac9..000000000 --- a/apps/website/app/api/auth/session/route.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { NextResponse } from 'next/server'; -import { getAuthService } from '@/lib/auth'; - -export async function GET() { - const authService = getAuthService(); - const session = await authService.getCurrentSession(); - - return NextResponse.json({ - session, - }); -} \ No newline at end of file diff --git a/apps/website/app/api/auth/signup/route.ts b/apps/website/app/api/auth/signup/route.ts deleted file mode 100644 index 776a2e0f8..000000000 --- a/apps/website/app/api/auth/signup/route.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { NextResponse } from 'next/server'; -import { getAuthService } from '@/lib/auth'; - -export async function POST(request: Request) { - try { - const body = await request.json(); - const { email, password, displayName } = body; - - if (!email || !password || !displayName) { - return NextResponse.json( - { error: 'Email, password, and display name are required' }, - { status: 400 } - ); - } - - const authService = getAuthService(); - const session = await authService.signupWithEmail({ email, password, displayName }); - - return NextResponse.json({ success: true, session }); - } catch (error) { - return NextResponse.json( - { error: error instanceof Error ? error.message : 'Signup failed' }, - { status: 400 } - ); - } -} \ No newline at end of file diff --git a/apps/website/app/api/avatar/generate/route.ts b/apps/website/app/api/avatar/generate/route.ts deleted file mode 100644 index 866f1451d..000000000 --- a/apps/website/app/api/avatar/generate/route.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server'; -import { getAuthService } from '@/lib/auth'; -import { - DemoFaceValidationAdapter, - DemoAvatarGenerationAdapter, - InMemoryAvatarGenerationRepository -} from '@gridpilot/testing-support'; -import { RequestAvatarGenerationUseCase } from '@gridpilot/media'; - -// Create singleton instances -const faceValidation = new DemoFaceValidationAdapter(); -const avatarGeneration = new DemoAvatarGenerationAdapter(); -const avatarRepository = new InMemoryAvatarGenerationRepository(); - -const requestAvatarGenerationUseCase = new RequestAvatarGenerationUseCase( - avatarRepository, - faceValidation, - avatarGeneration -); - -export async function POST(request: NextRequest) { - try { - const authService = getAuthService(); - const session = await authService.getCurrentSession(); - - if (!session) { - return NextResponse.json( - { success: false, errorMessage: 'Not authenticated' }, - { status: 401 } - ); - } - - const body = await request.json(); - const { facePhotoData, suitColor } = body; - - if (!facePhotoData) { - return NextResponse.json( - { success: false, errorMessage: 'No face photo provided' }, - { status: 400 } - ); - } - - if (!suitColor) { - return NextResponse.json( - { success: false, errorMessage: 'No suit color selected' }, - { status: 400 } - ); - } - - // Extract base64 data if it's a data URL - let base64Data = facePhotoData; - if (facePhotoData.startsWith('data:')) { - base64Data = facePhotoData.split(',')[1] || facePhotoData; - } - - const result = await requestAvatarGenerationUseCase.execute({ - userId: session.user.id, - facePhotoData: base64Data, - suitColor, - }); - - if (result.status === 'failed') { - return NextResponse.json({ - success: false, - errorMessage: result.errorMessage, - }); - } - - return NextResponse.json({ - success: true, - requestId: result.requestId, - avatarUrls: result.avatarUrls, - }); - } catch (error) { - console.error('Avatar generation error:', error); - return NextResponse.json( - { - success: false, - errorMessage: error instanceof Error ? error.message : 'Failed to generate avatars' - }, - { status: 500 } - ); - } -} \ No newline at end of file diff --git a/apps/website/app/api/avatar/validate-face/route.ts b/apps/website/app/api/avatar/validate-face/route.ts deleted file mode 100644 index 2b7152ca5..000000000 --- a/apps/website/app/api/avatar/validate-face/route.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server'; -import { DemoFaceValidationAdapter } from '@gridpilot/testing-support'; - -const faceValidation = new DemoFaceValidationAdapter(); - -export async function POST(request: NextRequest) { - try { - const body = await request.json(); - const { imageData } = body; - - if (!imageData) { - return NextResponse.json( - { isValid: false, errorMessage: 'No image data provided' }, - { status: 400 } - ); - } - - // Extract base64 data if it's a data URL - let base64Data = imageData; - if (imageData.startsWith('data:')) { - base64Data = imageData.split(',')[1] || imageData; - } - - const result = await faceValidation.validateFacePhoto(base64Data); - - return NextResponse.json(result); - } catch (error) { - console.error('Face validation error:', error); - return NextResponse.json( - { - isValid: false, - hasFace: false, - faceCount: 0, - confidence: 0, - errorMessage: 'Failed to validate photo' - }, - { status: 500 } - ); - } -} \ No newline at end of file diff --git a/apps/website/app/api/leagues/schedule-preview/route.ts b/apps/website/app/api/leagues/schedule-preview/route.ts deleted file mode 100644 index 53a9e2ddc..000000000 --- a/apps/website/app/api/leagues/schedule-preview/route.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { NextResponse } from 'next/server'; -import type { NextRequest } from 'next/server'; -import { - type LeagueScheduleDTO, - type LeagueSchedulePreviewDTO, -} from '@gridpilot/racing/application'; -import { getPreviewLeagueScheduleUseCase } from '@/lib/di-container'; -import { LeagueSchedulePreviewPresenter } from '@/lib/presenters/LeagueSchedulePreviewPresenter'; - -interface RequestBody { - seasonStartDate?: string; - raceStartTime?: string; - timezoneId?: string; - recurrenceStrategy?: LeagueScheduleDTO['recurrenceStrategy']; - intervalWeeks?: number; - weekdays?: LeagueScheduleDTO['weekdays']; - monthlyOrdinal?: LeagueScheduleDTO['monthlyOrdinal']; - monthlyWeekday?: LeagueScheduleDTO['monthlyWeekday']; - plannedRounds?: number; -} - -function toLeagueScheduleDTO(body: RequestBody): LeagueScheduleDTO { - const { - seasonStartDate, - raceStartTime, - timezoneId, - recurrenceStrategy, - intervalWeeks, - weekdays, - monthlyOrdinal, - monthlyWeekday, - plannedRounds, - } = body; - - if ( - !seasonStartDate || - !raceStartTime || - !timezoneId || - !recurrenceStrategy || - plannedRounds == null - ) { - throw new Error( - 'seasonStartDate, raceStartTime, timezoneId, recurrenceStrategy, and plannedRounds are required', - ); - } - - const dto: LeagueScheduleDTO = { - seasonStartDate, - raceStartTime, - timezoneId, - recurrenceStrategy, - plannedRounds, - }; - - if (intervalWeeks != null) { - dto.intervalWeeks = intervalWeeks; - } - if (weekdays && weekdays.length > 0) { - dto.weekdays = weekdays; - } - if (monthlyOrdinal != null) { - dto.monthlyOrdinal = monthlyOrdinal; - } - if (monthlyWeekday != null) { - dto.monthlyWeekday = monthlyWeekday; - } - - return dto; -} - -export async function POST(request: NextRequest) { - try { - const json = (await request.json()) as RequestBody; - - const schedule = toLeagueScheduleDTO(json); - - const presenter = new LeagueSchedulePreviewPresenter(); - const useCase = getPreviewLeagueScheduleUseCase(); - useCase.execute({ - schedule, - maxRounds: 10, - }); - const preview = presenter.getData(); - if (!preview) { - return NextResponse.json({ error: 'Failed to generate preview' }, { status: 500 }); - } - - return NextResponse.json(preview, { status: 200 }); - } catch (error) { - const message = - error instanceof Error ? error.message : 'Failed to preview schedule'; - - return NextResponse.json( - { - error: message, - }, - { status: 400 }, - ); - } -} \ No newline at end of file diff --git a/apps/website/app/api/payments/membership-fees/route.ts b/apps/website/app/api/payments/membership-fees/route.ts deleted file mode 100644 index 7c650c1e5..000000000 --- a/apps/website/app/api/payments/membership-fees/route.ts +++ /dev/null @@ -1,171 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server'; - -// Alpha: In-memory membership fee storage -const membershipFees: Map = new Map(); - -const memberPayments: Map = new Map(); - -const PLATFORM_FEE_RATE = 0.10; - -export async function GET(request: NextRequest) { - const { searchParams } = new URL(request.url); - const leagueId = searchParams.get('leagueId'); - const driverId = searchParams.get('driverId'); - - if (!leagueId) { - return NextResponse.json( - { error: 'leagueId is required' }, - { status: 400 } - ); - } - - const fee = Array.from(membershipFees.values()).find(f => f.leagueId === leagueId); - - let payments: typeof memberPayments extends Map ? V[] : never[] = []; - if (driverId) { - payments = Array.from(memberPayments.values()).filter( - p => membershipFees.get(p.feeId)?.leagueId === leagueId && p.driverId === driverId - ); - } - - return NextResponse.json({ fee, payments }); -} - -export async function POST(request: NextRequest) { - try { - const body = await request.json(); - const { leagueId, seasonId, type, amount } = body; - - if (!leagueId || !type || amount === undefined) { - return NextResponse.json( - { error: 'Missing required fields: leagueId, type, amount' }, - { status: 400 } - ); - } - - if (!['season', 'monthly', 'per_race'].includes(type)) { - return NextResponse.json( - { error: 'Type must be "season", "monthly", or "per_race"' }, - { status: 400 } - ); - } - - // Check for existing fee config - const existingFee = Array.from(membershipFees.values()).find(f => f.leagueId === leagueId); - - if (existingFee) { - // Update existing fee - existingFee.type = type; - existingFee.amount = amount; - existingFee.seasonId = seasonId || existingFee.seasonId; - existingFee.enabled = amount > 0; - existingFee.updatedAt = new Date(); - membershipFees.set(existingFee.id, existingFee); - return NextResponse.json({ fee: existingFee }); - } - - const id = `fee-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; - const fee = { - id, - leagueId, - seasonId: seasonId || undefined, - type, - amount, - enabled: amount > 0, - createdAt: new Date(), - updatedAt: new Date(), - }; - - membershipFees.set(id, fee); - - return NextResponse.json({ fee }, { status: 201 }); - } catch (err) { - console.error('Membership fee creation failed:', err); - return NextResponse.json( - { error: 'Failed to create membership fee' }, - { status: 500 } - ); - } -} - -// Record a member payment -export async function PATCH(request: NextRequest) { - try { - const body = await request.json(); - const { feeId, driverId, status, paidAt } = body; - - if (!feeId || !driverId) { - return NextResponse.json( - { error: 'Missing required fields: feeId, driverId' }, - { status: 400 } - ); - } - - const fee = membershipFees.get(feeId); - if (!fee) { - return NextResponse.json( - { error: 'Membership fee configuration not found' }, - { status: 404 } - ); - } - - // Find or create payment record - let payment = Array.from(memberPayments.values()).find( - p => p.feeId === feeId && p.driverId === driverId - ); - - if (!payment) { - const platformFee = fee.amount * PLATFORM_FEE_RATE; - const netAmount = fee.amount - platformFee; - - const paymentId = `mp-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; - payment = { - id: paymentId, - feeId, - driverId, - amount: fee.amount, - platformFee, - netAmount, - status: 'pending', - dueDate: new Date(), - }; - memberPayments.set(paymentId, payment); - } - - if (status) { - payment.status = status; - } - if (paidAt || status === 'paid') { - payment.paidAt = paidAt ? new Date(paidAt) : new Date(); - } - - memberPayments.set(payment.id, payment); - - return NextResponse.json({ payment }); - } catch (err) { - console.error('Member payment update failed:', err); - return NextResponse.json( - { error: 'Failed to update member payment' }, - { status: 500 } - ); - } -} \ No newline at end of file diff --git a/apps/website/app/api/payments/prizes/route.ts b/apps/website/app/api/payments/prizes/route.ts deleted file mode 100644 index 9d932bb11..000000000 --- a/apps/website/app/api/payments/prizes/route.ts +++ /dev/null @@ -1,180 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server'; - -// Alpha: In-memory prize storage -const prizes: Map = new Map(); - -export async function GET(request: NextRequest) { - const { searchParams } = new URL(request.url); - const leagueId = searchParams.get('leagueId'); - const seasonId = searchParams.get('seasonId'); - - if (!leagueId) { - return NextResponse.json( - { error: 'leagueId is required' }, - { status: 400 } - ); - } - - let results = Array.from(prizes.values()).filter(p => p.leagueId === leagueId); - - if (seasonId) { - results = results.filter(p => p.seasonId === seasonId); - } - - results.sort((a, b) => a.position - b.position); - - return NextResponse.json({ prizes: results }); -} - -export async function POST(request: NextRequest) { - try { - const body = await request.json(); - const { leagueId, seasonId, position, name, amount, type, description } = body; - - if (!leagueId || !seasonId || !position || !name || amount === undefined || !type) { - return NextResponse.json( - { error: 'Missing required fields: leagueId, seasonId, position, name, amount, type' }, - { status: 400 } - ); - } - - if (!['cash', 'merchandise', 'other'].includes(type)) { - return NextResponse.json( - { error: 'Type must be \"cash\", \"merchandise\", or \"other\"' }, - { status: 400 } - ); - } - - // Check for duplicate position - const existingPrize = Array.from(prizes.values()).find( - p => p.leagueId === leagueId && p.seasonId === seasonId && p.position === position - ); - - if (existingPrize) { - return NextResponse.json( - { error: `Prize for position ${position} already exists` }, - { status: 409 } - ); - } - - const id = `prize-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; - const prize = { - id, - leagueId, - seasonId, - position, - name, - amount, - type, - description: description || undefined, - awarded: false, - createdAt: new Date(), - }; - - prizes.set(id, prize); - - return NextResponse.json({ prize }, { status: 201 }); - } catch (err) { - console.error('Prize creation failed:', err); - return NextResponse.json( - { error: 'Failed to create prize' }, - { status: 500 } - ); - } -} - -// Award a prize -export async function PATCH(request: NextRequest) { - try { - const body = await request.json(); - const { prizeId, driverId } = body; - - if (!prizeId || !driverId) { - return NextResponse.json( - { error: 'Missing required fields: prizeId, driverId' }, - { status: 400 } - ); - } - - const prize = prizes.get(prizeId); - if (!prize) { - return NextResponse.json( - { error: 'Prize not found' }, - { status: 404 } - ); - } - - if (prize.awarded) { - return NextResponse.json( - { error: 'Prize has already been awarded' }, - { status: 400 } - ); - } - - prize.awarded = true; - prize.awardedTo = driverId; - prize.awardedAt = new Date(); - - prizes.set(prizeId, prize); - - return NextResponse.json({ prize }); - } catch (err) { - console.error('Prize awarding failed:', err); - return NextResponse.json( - { error: 'Failed to award prize' }, - { status: 500 } - ); - } -} - -export async function DELETE(request: NextRequest) { - try { - const { searchParams } = new URL(request.url); - const prizeId = searchParams.get('prizeId'); - - if (!prizeId) { - return NextResponse.json( - { error: 'prizeId is required' }, - { status: 400 } - ); - } - - const prize = prizes.get(prizeId); - if (!prize) { - return NextResponse.json( - { error: 'Prize not found' }, - { status: 404 } - ); - } - - if (prize.awarded) { - return NextResponse.json( - { error: 'Cannot delete an awarded prize' }, - { status: 400 } - ); - } - - prizes.delete(prizeId); - - return NextResponse.json({ success: true }); - } catch (err) { - console.error('Prize deletion failed:', err); - return NextResponse.json( - { error: 'Failed to delete prize' }, - { status: 500 } - ); - } -} \ No newline at end of file diff --git a/apps/website/app/api/payments/route.ts b/apps/website/app/api/payments/route.ts deleted file mode 100644 index 2359f2438..000000000 --- a/apps/website/app/api/payments/route.ts +++ /dev/null @@ -1,141 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server'; - -// Alpha: In-memory payment storage (mock payment gateway) -const payments: Map = new Map(); - -const PLATFORM_FEE_RATE = 0.10; - -export async function GET(request: NextRequest) { - const { searchParams } = new URL(request.url); - const leagueId = searchParams.get('leagueId'); - const payerId = searchParams.get('payerId'); - const type = searchParams.get('type'); - - let results = Array.from(payments.values()); - - if (leagueId) { - results = results.filter(p => p.leagueId === leagueId); - } - if (payerId) { - results = results.filter(p => p.payerId === payerId); - } - if (type) { - results = results.filter(p => p.type === type); - } - - return NextResponse.json({ payments: results }); -} - -export async function POST(request: NextRequest) { - try { - const body = await request.json(); - const { type, amount, payerId, payerType, leagueId, seasonId } = body; - - if (!type || !amount || !payerId || !payerType || !leagueId) { - return NextResponse.json( - { error: 'Missing required fields: type, amount, payerId, payerType, leagueId' }, - { status: 400 } - ); - } - - if (!['sponsorship', 'membership_fee'].includes(type)) { - return NextResponse.json( - { error: 'Type must be "sponsorship" or "membership_fee"' }, - { status: 400 } - ); - } - - if (!['sponsor', 'driver'].includes(payerType)) { - return NextResponse.json( - { error: 'PayerType must be "sponsor" or "driver"' }, - { status: 400 } - ); - } - - const platformFee = amount * PLATFORM_FEE_RATE; - const netAmount = amount - platformFee; - - const id = `payment-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; - const payment = { - id, - type, - amount, - platformFee, - netAmount, - payerId, - payerType, - leagueId, - seasonId: seasonId || undefined, - status: 'pending' as const, - createdAt: new Date(), - }; - - payments.set(id, payment); - - return NextResponse.json({ payment }, { status: 201 }); - } catch (err) { - console.error('Payment creation failed:', err); - return NextResponse.json( - { error: 'Failed to create payment' }, - { status: 500 } - ); - } -} - -// Complete a payment (mock payment gateway callback) -export async function PATCH(request: NextRequest) { - try { - const body = await request.json(); - const { paymentId, status } = body; - - if (!paymentId || !status) { - return NextResponse.json( - { error: 'Missing required fields: paymentId, status' }, - { status: 400 } - ); - } - - if (!['completed', 'failed', 'refunded'].includes(status)) { - return NextResponse.json( - { error: 'Status must be "completed", "failed", or "refunded"' }, - { status: 400 } - ); - } - - const payment = payments.get(paymentId); - if (!payment) { - return NextResponse.json( - { error: 'Payment not found' }, - { status: 404 } - ); - } - - payment.status = status; - if (status === 'completed') { - payment.completedAt = new Date(); - } - - payments.set(paymentId, payment); - - return NextResponse.json({ payment }); - } catch (err) { - console.error('Payment update failed:', err); - return NextResponse.json( - { error: 'Failed to update payment' }, - { status: 500 } - ); - } -} \ No newline at end of file diff --git a/apps/website/app/api/payments/wallets/route.ts b/apps/website/app/api/payments/wallets/route.ts deleted file mode 100644 index 0d1b5159b..000000000 --- a/apps/website/app/api/payments/wallets/route.ts +++ /dev/null @@ -1,142 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server'; - -// Alpha: In-memory wallet storage -const wallets: Map = new Map(); - -const transactions: Map = new Map(); - -export async function GET(request: NextRequest) { - const { searchParams } = new URL(request.url); - const leagueId = searchParams.get('leagueId'); - - if (!leagueId) { - return NextResponse.json( - { error: 'leagueId is required' }, - { status: 400 } - ); - } - - let wallet = Array.from(wallets.values()).find(w => w.leagueId === leagueId); - - if (!wallet) { - // Create wallet if doesn't exist - const id = `wallet-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; - wallet = { - id, - leagueId, - balance: 0, - totalRevenue: 0, - totalPlatformFees: 0, - totalWithdrawn: 0, - createdAt: new Date(), - }; - wallets.set(id, wallet); - } - - const walletTransactions = Array.from(transactions.values()) - .filter(t => t.walletId === wallet!.id) - .sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime()); - - return NextResponse.json({ wallet, transactions: walletTransactions }); -} - -export async function POST(request: NextRequest) { - try { - const body = await request.json(); - const { leagueId, type, amount, description, referenceId, referenceType } = body; - - if (!leagueId || !type || !amount || !description) { - return NextResponse.json( - { error: 'Missing required fields: leagueId, type, amount, description' }, - { status: 400 } - ); - } - - if (!['deposit', 'withdrawal'].includes(type)) { - return NextResponse.json( - { error: 'Type must be "deposit" or "withdrawal"' }, - { status: 400 } - ); - } - - // Get or create wallet - let wallet = Array.from(wallets.values()).find(w => w.leagueId === leagueId); - - if (!wallet) { - const id = `wallet-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; - wallet = { - id, - leagueId, - balance: 0, - totalRevenue: 0, - totalPlatformFees: 0, - totalWithdrawn: 0, - createdAt: new Date(), - }; - wallets.set(id, wallet); - } - - if (type === 'withdrawal') { - if (amount > wallet.balance) { - return NextResponse.json( - { error: 'Insufficient balance' }, - { status: 400 } - ); - } - - // Alpha: In production, check if season is over before allowing withdrawal - // For now we allow it for demo purposes - } - - // Create transaction - const transactionId = `txn-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; - const transaction = { - id: transactionId, - walletId: wallet.id, - type, - amount, - description, - referenceId: referenceId || undefined, - referenceType: referenceType || undefined, - createdAt: new Date(), - }; - - transactions.set(transactionId, transaction); - - // Update wallet balance - if (type === 'deposit') { - wallet.balance += amount; - wallet.totalRevenue += amount; - } else { - wallet.balance -= amount; - wallet.totalWithdrawn += amount; - } - - wallets.set(wallet.id, wallet); - - return NextResponse.json({ wallet, transaction }, { status: 201 }); - } catch (err) { - console.error('Wallet transaction failed:', err); - return NextResponse.json( - { error: 'Failed to process wallet transaction' }, - { status: 500 } - ); - } -} \ No newline at end of file diff --git a/apps/website/app/api/signup/route.ts b/apps/website/app/api/signup/route.ts deleted file mode 100644 index 7087bebe8..000000000 --- a/apps/website/app/api/signup/route.ts +++ /dev/null @@ -1,114 +0,0 @@ -import { NextResponse } from 'next/server'; - -import { validateEmail, isDisposableEmail } from '@gridpilot/identity/domain/value-objects/EmailAddress'; -import { checkRateLimit, getClientIp } from '@/lib/rate-limit'; - -const SIGNUP_DEV_STORE = new Map(); -const SIGNUP_KV_HASH_KEY = 'signups:emails'; - -const isDev = !process.env.KV_REST_API_URL; - -function jsonError(status: number, message: string, extra: Record = {}) { - return NextResponse.json( - { - error: message, - ...extra, - }, - { status }, - ); -} - -export async function POST(request: Request) { - let body: unknown; - - try { - body = await request.json(); - } catch { - return jsonError(400, 'Invalid request body'); - } - - const email = - typeof body === 'object' && body !== null && 'email' in body - ? (body as { email: unknown }).email - : undefined; - - if (typeof email !== 'string' || !email.trim()) { - return jsonError(400, 'Invalid email address'); - } - - const validation = validateEmail(email); - - if (!validation.success) { - return jsonError(400, validation.error || 'Invalid email address'); - } - - const normalizedEmail = validation.email; - - if (isDisposableEmail(normalizedEmail)) { - return jsonError(400, 'Disposable email addresses are not allowed'); - } - - const ip = getClientIp(request); - - try { - const rateResult = await checkRateLimit(ip); - - if (!rateResult.allowed) { - const retryAfterSeconds = Math.max(0, Math.round((rateResult.resetAt - Date.now()) / 1000)); - - return jsonError(429, 'Too many signups, please try again later.', { - retryAfter: retryAfterSeconds, - }); - } - } catch { - return jsonError(503, 'Temporarily unable to accept signups.'); - } - - try { - if (isDev) { - const existing = SIGNUP_DEV_STORE.get(normalizedEmail); - - if (existing) { - return jsonError(409, 'You are already on the list.'); - } - - SIGNUP_DEV_STORE.set(normalizedEmail, { - email: normalizedEmail, - createdAt: Date.now(), - ip, - }); - } else { - const { kv } = await import('@vercel/kv'); - - const existing = await kv.hget<{ email: string; createdAt: number; ip: string }>( - SIGNUP_KV_HASH_KEY, - normalizedEmail, - ); - - if (existing) { - return jsonError(409, 'You are already on the list.'); - } - - await kv.hset(SIGNUP_KV_HASH_KEY, { - [normalizedEmail]: { - email: normalizedEmail, - createdAt: Date.now(), - ip, - }, - }); - } - } catch (error) { - console.error('Signup storage error:', error); - return jsonError(503, 'Temporarily unable to accept signups.'); - } - - return NextResponse.json( - { - ok: true, - message: 'You are on the grid! We will be in touch soon.', - }, - { - status: 201, - }, - ); -} \ No newline at end of file diff --git a/apps/website/app/api/sponsors/dashboard/route.ts b/apps/website/app/api/sponsors/dashboard/route.ts deleted file mode 100644 index e13180d53..000000000 --- a/apps/website/app/api/sponsors/dashboard/route.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { NextResponse } from 'next/server'; -import type { NextRequest } from 'next/server'; -import { cookies } from 'next/headers'; -import { getGetSponsorDashboardUseCase } from '@/lib/di-container'; -import { SponsorDashboardPresenter } from '@/lib/presenters/SponsorDashboardPresenter'; - -export async function GET(request: NextRequest) { - try { - // Get sponsor ID from cookie (set during demo login) - const cookieStore = await cookies(); - const sponsorId = cookieStore.get('gridpilot_sponsor_id')?.value; - - if (!sponsorId) { - return NextResponse.json( - { error: 'Not authenticated as sponsor' }, - { status: 401 } - ); - } - - const presenter = new SponsorDashboardPresenter(); - const useCase = getGetSponsorDashboardUseCase(); - await useCase.execute({ sponsorId }, presenter); - const dashboard = presenter.getData(); - - if (!dashboard) { - return NextResponse.json( - { error: 'Sponsor not found' }, - { status: 404 } - ); - } - - return NextResponse.json(dashboard); - } catch (error) { - const message = error instanceof Error ? error.message : 'Failed to get sponsor dashboard'; - return NextResponse.json({ error: message }, { status: 500 }); - } -} \ No newline at end of file diff --git a/apps/website/app/api/sponsors/route.ts b/apps/website/app/api/sponsors/route.ts deleted file mode 100644 index 9244b7b96..000000000 --- a/apps/website/app/api/sponsors/route.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server'; - -// Alpha: In-memory sponsor storage -const sponsors: Map = new Map(); - -export async function GET() { - const allSponsors = Array.from(sponsors.values()); - return NextResponse.json({ sponsors: allSponsors }); -} - -export async function POST(request: NextRequest) { - try { - const body = await request.json(); - const { name, contactEmail, websiteUrl, logoUrl } = body; - - if (!name || !contactEmail) { - return NextResponse.json( - { error: 'Name and contact email are required' }, - { status: 400 } - ); - } - - const id = `sponsor-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; - const sponsor = { - id, - name, - contactEmail, - websiteUrl: websiteUrl || undefined, - logoUrl: logoUrl || undefined, - createdAt: new Date(), - }; - - sponsors.set(id, sponsor); - - return NextResponse.json({ sponsor }, { status: 201 }); - } catch (err) { - console.error('Sponsor creation failed:', err); - return NextResponse.json( - { error: 'Failed to create sponsor' }, - { status: 500 } - ); - } -} \ No newline at end of file diff --git a/apps/website/app/api/sponsors/sponsorships/route.ts b/apps/website/app/api/sponsors/sponsorships/route.ts deleted file mode 100644 index e5093f0bd..000000000 --- a/apps/website/app/api/sponsors/sponsorships/route.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { NextResponse } from 'next/server'; -import type { NextRequest } from 'next/server'; -import { cookies } from 'next/headers'; -import { getGetSponsorSponsorshipsUseCase } from '@/lib/di-container'; -import { SponsorSponsorshipsPresenter } from '@/lib/presenters/SponsorSponsorshipsPresenter'; - -export async function GET(request: NextRequest) { - try { - // Get sponsor ID from cookie (set during demo login) - const cookieStore = await cookies(); - const sponsorId = cookieStore.get('gridpilot_sponsor_id')?.value; - - if (!sponsorId) { - return NextResponse.json( - { error: 'Not authenticated as sponsor' }, - { status: 401 } - ); - } - - const presenter = new SponsorSponsorshipsPresenter(); - const useCase = getGetSponsorSponsorshipsUseCase(); - await useCase.execute({ sponsorId }, presenter); - const sponsorships = presenter.getData(); - - if (!sponsorships) { - return NextResponse.json( - { error: 'Sponsor not found' }, - { status: 404 } - ); - } - - return NextResponse.json(sponsorships); - } catch (error) { - const message = error instanceof Error ? error.message : 'Failed to get sponsor sponsorships'; - return NextResponse.json({ error: message }, { status: 500 }); - } -} \ No newline at end of file diff --git a/apps/website/app/api/wallets/[leagueId]/withdraw/route.ts b/apps/website/app/api/wallets/[leagueId]/withdraw/route.ts deleted file mode 100644 index 2e3818d9c..000000000 --- a/apps/website/app/api/wallets/[leagueId]/withdraw/route.ts +++ /dev/null @@ -1,202 +0,0 @@ -/** - * API Route: Wallet Withdrawal - * - * POST /api/wallets/:leagueId/withdraw - * - * Handles withdrawal requests from league wallets. - * Enforces the rule that withdrawals are only allowed after season is completed. - */ - -import { NextRequest, NextResponse } from 'next/server'; - -interface WithdrawRequest { - amount: number; - currency: string; - seasonId: string; - destinationAccount: string; -} - -// Mock season status lookup -const MOCK_SEASONS: Record = { - 'season-1': { status: 'completed', name: 'Season 1' }, - 'season-2': { status: 'active', name: 'Season 2' }, - 'season-3': { status: 'planned', name: 'Season 3' }, -}; - -// Mock wallet balances -const MOCK_WALLETS: Record = { - 'league-1': { balance: 2500, currency: 'USD' }, - 'league-2': { balance: 1200, currency: 'USD' }, -}; - -export async function POST( - request: NextRequest, - { params }: { params: Promise<{ leagueId: string }> } -) { - try { - const { leagueId } = await params; - const body: WithdrawRequest = await request.json(); - - // Validate required fields - if (!body.amount || body.amount <= 0) { - return NextResponse.json( - { error: 'Invalid withdrawal amount' }, - { status: 400 } - ); - } - - if (!body.seasonId) { - return NextResponse.json( - { error: 'Season ID is required' }, - { status: 400 } - ); - } - - if (!body.destinationAccount) { - return NextResponse.json( - { error: 'Destination account is required' }, - { status: 400 } - ); - } - - // Get season status - const season = MOCK_SEASONS[body.seasonId]; - if (!season) { - return NextResponse.json( - { error: 'Season not found' }, - { status: 404 } - ); - } - - // CRITICAL: Enforce withdrawal restriction based on season status - // Withdrawals are ONLY allowed when the season is completed - if (season.status !== 'completed') { - return NextResponse.json( - { - error: 'Withdrawal not allowed', - reason: `Withdrawals are only permitted after the season is completed. "${season.name}" is currently ${season.status}.`, - seasonStatus: season.status, - }, - { status: 403 } - ); - } - - // Get wallet - const wallet = MOCK_WALLETS[leagueId]; - if (!wallet) { - return NextResponse.json( - { error: 'Wallet not found' }, - { status: 404 } - ); - } - - // Check sufficient balance - if (wallet.balance < body.amount) { - return NextResponse.json( - { - error: 'Insufficient balance', - available: wallet.balance, - requested: body.amount, - }, - { status: 400 } - ); - } - - // Check currency match - if (wallet.currency !== body.currency) { - return NextResponse.json( - { - error: 'Currency mismatch', - walletCurrency: wallet.currency, - requestedCurrency: body.currency, - }, - { status: 400 } - ); - } - - // Process withdrawal (in-memory mock) - const newBalance = wallet.balance - body.amount; - MOCK_WALLETS[leagueId] = { ...wallet, balance: newBalance }; - - // Generate transaction ID - const transactionId = `txn-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; - - return NextResponse.json({ - success: true, - transactionId, - amount: body.amount, - currency: body.currency, - newBalance, - destinationAccount: body.destinationAccount, - processedAt: new Date().toISOString(), - }); - - } catch (error) { - console.error('Wallet withdrawal error:', error); - return NextResponse.json( - { error: 'Internal server error' }, - { status: 500 } - ); - } -} - -/** - * GET /api/wallets/:leagueId/withdraw - * - * Check withdrawal eligibility for a league's wallet - */ -export async function GET( - request: NextRequest, - { params }: { params: Promise<{ leagueId: string }> } -) { - try { - const { leagueId } = await params; - const { searchParams } = new URL(request.url); - const seasonId = searchParams.get('seasonId'); - - if (!seasonId) { - return NextResponse.json( - { error: 'Season ID is required' }, - { status: 400 } - ); - } - - const season = MOCK_SEASONS[seasonId]; - if (!season) { - return NextResponse.json( - { error: 'Season not found' }, - { status: 404 } - ); - } - - const wallet = MOCK_WALLETS[leagueId]; - if (!wallet) { - return NextResponse.json( - { error: 'Wallet not found' }, - { status: 404 } - ); - } - - const canWithdraw = season.status === 'completed'; - - return NextResponse.json({ - leagueId, - seasonId, - seasonName: season.name, - seasonStatus: season.status, - canWithdraw, - reason: canWithdraw - ? 'Season is completed, withdrawals are allowed' - : `Withdrawals are only permitted after the season is completed. Season is currently ${season.status}.`, - balance: wallet.balance, - currency: wallet.currency, - }); - - } catch (error) { - console.error('Withdrawal eligibility check error:', error); - return NextResponse.json( - { error: 'Internal server error' }, - { status: 500 } - ); - } -} \ No newline at end of file