Replace manualProviders.ts with a paymentProviders.ts registry (automatic vs manual settlement) and move all seat counting into capacity.ts as the single source of truth: only paid/checked-in tickets and pending_approval payments hold a seat, so abandoned checkouts never block sales. Admins can now knowingly approve a payment over capacity (allowOverCapacity), with the booking and admin UIs updated to match. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
72 lines
2.1 KiB
TypeScript
72 lines
2.1 KiB
TypeScript
export const API_BASE = process.env.NEXT_PUBLIC_API_URL || '';
|
|
|
|
export interface ApiError {
|
|
error: string;
|
|
}
|
|
|
|
/** Read the stored auth token (browser only). */
|
|
export function getToken(): string | null {
|
|
return typeof window !== 'undefined' ? localStorage.getItem('spanglish-token') : null;
|
|
}
|
|
|
|
export async function fetchApi<T>(
|
|
endpoint: string,
|
|
options: RequestInit = {}
|
|
): Promise<T> {
|
|
const token = getToken();
|
|
|
|
const headers: HeadersInit = {
|
|
'Content-Type': 'application/json',
|
|
...options.headers,
|
|
};
|
|
|
|
if (token) {
|
|
(headers as Record<string, string>)['Authorization'] = `Bearer ${token}`;
|
|
}
|
|
|
|
const res = await fetch(`${API_BASE}${endpoint}`, {
|
|
...options,
|
|
headers,
|
|
});
|
|
|
|
if (!res.ok) {
|
|
const errorData = await res.json().catch(() => ({ error: 'Request failed' }));
|
|
const errorMessage = typeof errorData.error === 'string'
|
|
? errorData.error
|
|
: (errorData.message || JSON.stringify(errorData) || 'Request failed');
|
|
const error = new Error(errorMessage);
|
|
// Preserve structured error info (e.g. code: 'EVENT_OVER_CAPACITY') so
|
|
// callers can react beyond the message text.
|
|
(error as any).code = errorData.code;
|
|
(error as any).data = errorData;
|
|
throw error;
|
|
}
|
|
|
|
return res.json();
|
|
}
|
|
|
|
/**
|
|
* Fetch a file download (CSV/blob) with auth, returning the blob and the
|
|
* filename parsed from the Content-Disposition header.
|
|
*/
|
|
export async function fetchBlob(
|
|
endpoint: string,
|
|
fallbackFilename: string
|
|
): Promise<{ blob: Blob; filename: string }> {
|
|
const token = getToken();
|
|
const headers: Record<string, string> = {};
|
|
if (token) headers['Authorization'] = `Bearer ${token}`;
|
|
|
|
const res = await fetch(`${API_BASE}${endpoint}`, { headers });
|
|
if (!res.ok) {
|
|
const errorData = await res.json().catch(() => ({ error: 'Export failed' }));
|
|
throw new Error(errorData.error || 'Export failed');
|
|
}
|
|
|
|
const disposition = res.headers.get('Content-Disposition') || '';
|
|
const filenameMatch = disposition.match(/filename="?([^"]+)"?/);
|
|
const filename = filenameMatch ? filenameMatch[1] : fallbackFilename;
|
|
const blob = await res.blob();
|
|
return { blob, filename };
|
|
}
|