Files
ticket-system/frontend-client/src/api/client.ts
2026-03-06 19:53:08 +00:00

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;