This commit is contained in:
2025-12-10 12:38:55 +01:00
parent 0f7fe67d3c
commit fbbcf414a4
87 changed files with 11972 additions and 390 deletions

View File

@@ -0,0 +1,171 @@
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

@@ -0,0 +1,180 @@
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

@@ -0,0 +1,141 @@
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

@@ -0,0 +1,142 @@
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 }
);
}
}