module creation
This commit is contained in:
@@ -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 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
@@ -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,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -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 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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 },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user