172 lines
5.2 KiB
TypeScript
172 lines
5.2 KiB
TypeScript
import axios from "axios";
|
|
import { useAuthStore } from "@/store/authStore";
|
|
|
|
const apiClient = axios.create({
|
|
baseURL: process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:8081/api",
|
|
headers: { "Content-Type": "application/json" },
|
|
});
|
|
|
|
apiClient.interceptors.request.use((config) => {
|
|
const token = useAuthStore.getState().token;
|
|
if (token) {
|
|
config.headers.Authorization = `Bearer ${token}`;
|
|
}
|
|
return config;
|
|
});
|
|
|
|
// ─── Auth ─────────────────────────────────────────────────────────────────────
|
|
|
|
interface LoginResponse {
|
|
access_token: string;
|
|
token_type: string;
|
|
}
|
|
|
|
/**
|
|
* POST /auth/login
|
|
* Returns a Bearer access token on success.
|
|
* Throws AxiosError 401 for invalid credentials, 422 for validation errors.
|
|
*/
|
|
export async function loginApi(
|
|
email: string,
|
|
password: string
|
|
): Promise<{ access_token: string }> {
|
|
const response = await apiClient.post<LoginResponse>("/auth/login", {
|
|
email,
|
|
password,
|
|
});
|
|
return { access_token: response.data.access_token };
|
|
}
|
|
|
|
// ─── Domain types (mirror backend schemas) ────────────────────────────────────
|
|
|
|
export interface TournamentInfo {
|
|
id: number;
|
|
title: string;
|
|
event_date: string; // ISO-8601
|
|
}
|
|
|
|
export interface SeatInfo {
|
|
id: number;
|
|
sector: string;
|
|
row: number;
|
|
number: number;
|
|
price: number;
|
|
tournament: TournamentInfo;
|
|
}
|
|
|
|
export type TicketStatus = "AVAILABLE" | "LOCKED" | "PAID" | "SCANNED" | "REFUNDED";
|
|
|
|
export interface TicketResponse {
|
|
id: number;
|
|
status: TicketStatus;
|
|
pdf_url: string | null;
|
|
created_at: string; // ISO-8601
|
|
seat: SeatInfo;
|
|
}
|
|
|
|
// ─── Tickets API ──────────────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* GET /api/tickets/me
|
|
* Returns all PAID tickets for the authenticated user.
|
|
* Throws AxiosError 401 if the token is missing or expired.
|
|
*/
|
|
export async function getMyTicketsApi(): Promise<TicketResponse[]> {
|
|
const response = await apiClient.get<TicketResponse[]>("/tickets/me");
|
|
return response.data;
|
|
}
|
|
|
|
// ─── Seat locking ─────────────────────────────────────────────────────────────
|
|
|
|
interface LockSeatResponse {
|
|
message: string;
|
|
seat_id: number;
|
|
status: string;
|
|
/** Returned by backend when the Ticket row is created. Falls back to seat_id. */
|
|
ticket_id?: number;
|
|
}
|
|
|
|
/**
|
|
* POST /api/seats/{seat_id}/lock?user_id={user_id}
|
|
* Returns the ticketId for the locked seat (falls back to seatId if backend
|
|
* does not yet expose ticket_id in the response body).
|
|
* Throws AxiosError with status 409 if the seat is already locked.
|
|
*/
|
|
export async function lockSeatApi(
|
|
seatId: number,
|
|
userId: number
|
|
): Promise<{ ticketId: number }> {
|
|
const response = await apiClient.post<LockSeatResponse>(
|
|
`/seats/${seatId}/lock`,
|
|
null,
|
|
{ params: { user_id: userId } }
|
|
);
|
|
return { ticketId: response.data.ticket_id ?? seatId };
|
|
}
|
|
|
|
// ─── Payment webhook ──────────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* POST /api/webhooks/payment
|
|
* Simulates a payment gateway callback for the given ticket.
|
|
* Uses a random idempotency key to satisfy the backend's idempotency check.
|
|
*/
|
|
export async function processPaymentWebhook(ticketId: number): Promise<void> {
|
|
const idempotencyKey = "req-" + Math.random().toString(36).substring(2, 9);
|
|
await apiClient.post("/webhooks/payment", {
|
|
ticket_id: ticketId,
|
|
idempotency_key: idempotencyKey,
|
|
status: "success",
|
|
});
|
|
}
|
|
|
|
// ─── Admin: Tournaments ───────────────────────────────────────────────────────
|
|
|
|
export interface TournamentCreate {
|
|
title: string;
|
|
description?: string;
|
|
event_date: string; // ISO-8601
|
|
}
|
|
|
|
export interface TournamentAdminResponse {
|
|
id: number;
|
|
title: string;
|
|
description: string | null;
|
|
event_date: string;
|
|
}
|
|
|
|
export interface SectorConfigInput {
|
|
sector_name: string;
|
|
rows: number;
|
|
seats_per_row: number;
|
|
price: number;
|
|
}
|
|
|
|
/**
|
|
* POST /api/tournaments
|
|
* Creates a new tournament. Requires a superuser token (403 otherwise).
|
|
*/
|
|
export async function createTournamentApi(
|
|
data: TournamentCreate
|
|
): Promise<TournamentAdminResponse> {
|
|
const response = await apiClient.post<TournamentAdminResponse>("/tournaments", data);
|
|
return response.data;
|
|
}
|
|
|
|
/**
|
|
* POST /api/tournaments/{tournament_id}/generate-seats
|
|
* Bulk-generates seats for a tournament. Requires a superuser token.
|
|
*/
|
|
export async function generateSeatsApi(
|
|
tournamentId: number,
|
|
sectors: SectorConfigInput[]
|
|
): Promise<{ message: string }> {
|
|
const response = await apiClient.post<{ message: string }>(
|
|
`/tournaments/${tournamentId}/generate-seats`,
|
|
{ sectors }
|
|
);
|
|
return response.data;
|
|
}
|
|
|
|
export default apiClient;
|