module creation

This commit is contained in:
2025-12-15 22:11:55 +01:00
parent 7c7267da72
commit 7a11daa878
17 changed files with 0 additions and 1462 deletions

View File

@@ -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 }
);
}
}

View File

@@ -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 }
);
}
}

View File

@@ -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);
}

View File

@@ -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,
});
}

View File

@@ -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 }
);
}
}

View File

@@ -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 }
);
}
}

View File

@@ -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 }
);
}
}

View File

@@ -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 },
);
}
}

View File

@@ -1,171 +0,0 @@
import { NextRequest, NextResponse } from 'next/server';
// Alpha: In-memory membership fee storage
const membershipFees: Map<string, {
id: string;
leagueId: string;
seasonId?: string;
type: 'season' | 'monthly' | 'per_race';
amount: number;
enabled: boolean;
createdAt: Date;
updatedAt: Date;
}> = new Map();
const memberPayments: Map<string, {
id: string;
feeId: string;
driverId: string;
amount: number;
platformFee: number;
netAmount: number;
status: 'pending' | 'paid' | 'overdue';
dueDate: Date;
paidAt?: Date;
}> = 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<string, infer V> ? 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 }
);
}
}

View File

@@ -1,180 +0,0 @@
import { NextRequest, NextResponse } from 'next/server';
// Alpha: In-memory prize storage
const prizes: Map<string, {
id: string;
leagueId: string;
seasonId: string;
position: number;
name: string;
amount: number;
type: 'cash' | 'merchandise' | 'other';
description?: string;
awarded: boolean;
awardedTo?: string;
awardedAt?: Date;
createdAt: Date;
}> = 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 }
);
}
}

View File

@@ -1,141 +0,0 @@
import { NextRequest, NextResponse } from 'next/server';
// Alpha: In-memory payment storage (mock payment gateway)
const payments: Map<string, {
id: string;
type: 'sponsorship' | 'membership_fee';
amount: number;
platformFee: number;
netAmount: number;
payerId: string;
payerType: 'sponsor' | 'driver';
leagueId: string;
seasonId?: string;
status: 'pending' | 'completed' | 'failed' | 'refunded';
createdAt: Date;
completedAt?: Date;
}> = 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 }
);
}
}

View File

@@ -1,142 +0,0 @@
import { NextRequest, NextResponse } from 'next/server';
// Alpha: In-memory wallet storage
const wallets: Map<string, {
id: string;
leagueId: string;
balance: number;
totalRevenue: number;
totalPlatformFees: number;
totalWithdrawn: number;
createdAt: Date;
}> = new Map();
const transactions: Map<string, {
id: string;
walletId: string;
type: 'deposit' | 'withdrawal' | 'platform_fee';
amount: number;
description: string;
referenceId?: string;
referenceType?: 'sponsorship' | 'membership_fee' | 'prize';
createdAt: Date;
}> = 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 }
);
}
}

View File

@@ -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<string, { email: string; createdAt: number; ip: string }>();
const SIGNUP_KV_HASH_KEY = 'signups:emails';
const isDev = !process.env.KV_REST_API_URL;
function jsonError(status: number, message: string, extra: Record<string, unknown> = {}) {
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,
},
);
}

View File

@@ -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 });
}
}

View File

@@ -1,50 +0,0 @@
import { NextRequest, NextResponse } from 'next/server';
// Alpha: In-memory sponsor storage
const sponsors: Map<string, {
id: string;
name: string;
contactEmail: string;
websiteUrl?: string;
logoUrl?: string;
createdAt: Date;
}> = 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 }
);
}
}

View File

@@ -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 });
}
}

View File

@@ -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<string, { status: 'planned' | 'active' | 'completed'; name: string }> = {
'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<string, { balance: number; currency: string }> = {
'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 }
);
}
}