Files
fotospiel-app/resources/js/admin/api.ts

774 lines
26 KiB
TypeScript

import { authorizedFetch } from './auth/tokens';
type JsonValue = Record<string, any>;
export type TenantEvent = {
id: number;
name: string | Record<string, string>;
slug: string;
event_date: string | null;
status: 'draft' | 'published' | 'archived';
is_active?: boolean;
description?: string | null;
photo_count?: number;
like_count?: number;
};
export type TenantPhoto = {
id: number;
filename: string;
original_name: string | null;
mime_type: string | null;
size: number;
url: string;
thumbnail_url: string;
status: string;
is_featured: boolean;
likes_count: number;
uploaded_at: string;
uploader_name: string | null;
};
export type EventStats = {
total: number;
featured: number;
likes: number;
recent_uploads: number;
status: string;
is_active: boolean;
};
export type PaginationMeta = {
current_page: number;
last_page: number;
per_page: number;
total: number;
};
export type PaginatedResult<T> = {
data: T[];
meta: PaginationMeta;
};
export type DashboardSummary = {
active_events: number;
new_photos: number;
task_progress: number;
credit_balance?: number | null;
upcoming_events?: number | null;
active_package?: {
name: string;
expires_at?: string | null;
remaining_events?: number | null;
} | null;
};
export type TenantPackageSummary = {
id: number;
package_id: number;
package_name: string;
active: boolean;
used_events: number;
remaining_events: number | null;
price: number | null;
currency: string | null;
purchased_at: string | null;
expires_at: string | null;
package_limits: Record<string, unknown> | null;
};
export type CreditBalance = {
balance: number;
free_event_granted_at?: string | null;
};
export type CreditLedgerEntry = {
id: number;
delta: number;
reason: string;
note: string | null;
related_purchase_id: number | null;
created_at: string;
};
export type TenantTask = {
id: number;
title: string;
description: string | null;
priority: 'low' | 'medium' | 'high' | 'urgent' | null;
due_date: string | null;
is_completed: boolean;
collection_id: number | null;
assigned_events_count: number;
assigned_events?: TenantEvent[];
created_at: string | null;
updated_at: string | null;
};
export type TaskPayload = Partial<{
title: string;
description: string | null;
collection_id: number | null;
priority: 'low' | 'medium' | 'high' | 'urgent';
due_date: string | null;
is_completed: boolean;
}>;
export type EventMember = {
id: number;
name: string;
email: string | null;
role: 'tenant_admin' | 'member' | 'guest' | string;
status?: 'pending' | 'active' | 'invited' | string;
joined_at?: string | null;
avatar_url?: string | null;
};
type EventListResponse = { data?: TenantEvent[] };
type EventResponse = { data: TenantEvent };
export type EventJoinToken = {
id: number;
token: string;
url: string;
label: string | null;
usage_limit: number | null;
usage_count: number;
expires_at: string | null;
revoked_at: string | null;
is_active: boolean;
created_at: string | null;
metadata: Record<string, unknown>;
};
type CreatedEventResponse = { message: string; data: TenantEvent; balance: number };
type PhotoResponse = { message: string; data: TenantPhoto };
type EventSavePayload = {
name: string;
slug: string;
date?: string;
status?: 'draft' | 'published' | 'archived';
is_active?: boolean;
package_id?: number;
};
async function jsonOrThrow<T>(response: Response, message: string): Promise<T> {
if (!response.ok) {
const body = await safeJson(response);
console.error('[API]', message, response.status, body);
throw new Error(message);
}
return (await response.json()) as T;
}
async function safeJson(response: Response): Promise<JsonValue | null> {
try {
return (await response.clone().json()) as JsonValue;
} catch {
return null;
}
}
function buildPagination(payload: JsonValue | null, defaultCount: number): PaginationMeta {
const meta = (payload?.meta as Partial<PaginationMeta>) ?? {};
return {
current_page: Number(meta.current_page ?? payload?.current_page ?? 1),
last_page: Number(meta.last_page ?? payload?.last_page ?? 1),
per_page: Number(meta.per_page ?? payload?.per_page ?? defaultCount ?? 0) || defaultCount || 0,
total: Number(meta.total ?? payload?.total ?? defaultCount ?? 0) || defaultCount || 0,
};
}
function normalizeEvent(event: TenantEvent): TenantEvent {
return {
...event,
is_active: typeof event.is_active === 'boolean' ? event.is_active : undefined,
};
}
function normalizePhoto(photo: TenantPhoto): TenantPhoto {
return {
id: photo.id,
filename: photo.filename,
original_name: photo.original_name ?? null,
mime_type: photo.mime_type ?? null,
size: Number(photo.size ?? 0),
url: photo.url,
thumbnail_url: photo.thumbnail_url ?? photo.url,
status: photo.status ?? 'approved',
is_featured: Boolean(photo.is_featured),
likes_count: Number(photo.likes_count ?? 0),
uploaded_at: photo.uploaded_at,
uploader_name: photo.uploader_name ?? null,
};
}
function normalizeDashboard(payload: JsonValue | null): DashboardSummary | null {
if (!payload) {
return null;
}
return {
active_events: Number(payload.active_events ?? payload.activeEvents ?? 0),
new_photos: Number(payload.new_photos ?? payload.newPhotos ?? 0),
task_progress: Number(payload.task_progress ?? payload.taskProgress ?? 0),
credit_balance: payload.credit_balance ?? payload.creditBalance ?? null,
upcoming_events: payload.upcoming_events ?? payload.upcomingEvents ?? null,
active_package: payload.active_package
? {
name: String(payload.active_package.name ?? 'Aktives Package'),
expires_at: payload.active_package.expires_at ?? null,
remaining_events: payload.active_package.remaining_events ?? payload.active_package.remainingEvents ?? null,
}
: null,
};
}
function normalizeTenantPackage(pkg: JsonValue): TenantPackageSummary {
const packageData = pkg.package ?? {};
return {
id: Number(pkg.id ?? 0),
package_id: Number(pkg.package_id ?? packageData.id ?? 0),
package_name: String(packageData.name ?? pkg.package_name ?? 'Unbekanntes Package'),
active: Boolean(pkg.active ?? false),
used_events: Number(pkg.used_events ?? 0),
remaining_events: pkg.remaining_events !== undefined ? Number(pkg.remaining_events) : null,
price: packageData.price !== undefined ? Number(packageData.price) : pkg.price ?? null,
currency: packageData.currency ?? pkg.currency ?? 'EUR',
purchased_at: pkg.purchased_at ?? pkg.created_at ?? null,
expires_at: pkg.expires_at ?? pkg.valid_until ?? null,
package_limits: pkg.package_limits ?? packageData.limits ?? null,
};
}
function normalizeTask(task: JsonValue): TenantTask {
return {
id: Number(task.id ?? 0),
title: String(task.title ?? 'Ohne Titel'),
description: task.description ?? null,
priority: (task.priority ?? null) as TenantTask['priority'],
due_date: task.due_date ?? null,
is_completed: Boolean(task.is_completed ?? false),
collection_id: task.collection_id ?? null,
assigned_events_count: Number(task.assigned_events_count ?? 0),
assigned_events: Array.isArray(task.assigned_events) ? task.assigned_events.map(normalizeEvent) : undefined,
created_at: task.created_at ?? null,
updated_at: task.updated_at ?? null,
};
}
function normalizeMember(member: JsonValue): EventMember {
return {
id: Number(member.id ?? 0),
name: String(member.name ?? member.email ?? 'Unbekannt'),
email: member.email ?? null,
role: (member.role ?? 'member') as EventMember['role'],
status: member.status ?? 'active',
joined_at: member.joined_at ?? member.created_at ?? null,
avatar_url: member.avatar_url ?? member.avatar ?? null,
};
}
function normalizeJoinToken(raw: JsonValue): EventJoinToken {
return {
id: Number(raw.id ?? 0),
token: String(raw.token ?? ''),
url: String(raw.url ?? ''),
label: raw.label ?? null,
usage_limit: raw.usage_limit ?? null,
usage_count: Number(raw.usage_count ?? 0),
expires_at: raw.expires_at ?? null,
revoked_at: raw.revoked_at ?? null,
is_active: Boolean(raw.is_active),
created_at: raw.created_at ?? null,
metadata: (raw.metadata ?? {}) as Record<string, unknown>,
};
}
function eventEndpoint(slug: string): string {
return `/api/v1/tenant/events/${encodeURIComponent(slug)}`;
}
export async function getEvents(): Promise<TenantEvent[]> {
const response = await authorizedFetch('/api/v1/tenant/events');
const data = await jsonOrThrow<EventListResponse>(response, 'Failed to load events');
return (data.data ?? []).map(normalizeEvent);
}
export async function createEvent(payload: EventSavePayload): Promise<{ event: TenantEvent; balance: number }> {
const response = await authorizedFetch('/api/v1/tenant/events', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
const data = await jsonOrThrow<CreatedEventResponse>(response, 'Failed to create event');
return { event: normalizeEvent(data.data), balance: data.balance };
}
export async function updateEvent(slug: string, payload: Partial<EventSavePayload>): Promise<TenantEvent> {
const response = await authorizedFetch(eventEndpoint(slug), {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
const data = await jsonOrThrow<EventResponse>(response, 'Failed to update event');
return normalizeEvent(data.data);
}
export async function getEvent(slug: string): Promise<TenantEvent> {
const response = await authorizedFetch(eventEndpoint(slug));
const data = await jsonOrThrow<EventResponse>(response, 'Failed to load event');
return normalizeEvent(data.data);
}
export async function getEventPhotos(slug: string): Promise<TenantPhoto[]> {
const response = await authorizedFetch(`${eventEndpoint(slug)}/photos`);
const data = await jsonOrThrow<{ data?: TenantPhoto[] }>(response, 'Failed to load photos');
return (data.data ?? []).map(normalizePhoto);
}
export async function featurePhoto(slug: string, id: number): Promise<TenantPhoto> {
const response = await authorizedFetch(`${eventEndpoint(slug)}/photos/${id}/feature`, { method: 'POST' });
const data = await jsonOrThrow<PhotoResponse>(response, 'Failed to feature photo');
return normalizePhoto(data.data);
}
export async function unfeaturePhoto(slug: string, id: number): Promise<TenantPhoto> {
const response = await authorizedFetch(`${eventEndpoint(slug)}/photos/${id}/unfeature`, { method: 'POST' });
const data = await jsonOrThrow<PhotoResponse>(response, 'Failed to unfeature photo');
return normalizePhoto(data.data);
}
export async function deletePhoto(slug: string, id: number): Promise<void> {
const response = await authorizedFetch(`${eventEndpoint(slug)}/photos/${id}`, { method: 'DELETE' });
if (!response.ok) {
await safeJson(response);
throw new Error('Failed to delete photo');
}
}
export async function toggleEvent(slug: string): Promise<TenantEvent> {
const response = await authorizedFetch(`${eventEndpoint(slug)}/toggle`, { method: 'POST' });
const data = await jsonOrThrow<{ message: string; data: TenantEvent }>(response, 'Failed to toggle event');
return normalizeEvent(data.data);
}
export async function getEventStats(slug: string): Promise<EventStats> {
const response = await authorizedFetch(`${eventEndpoint(slug)}/stats`);
const data = await jsonOrThrow<EventStats>(response, 'Failed to load stats');
return {
total: Number(data.total ?? 0),
featured: Number(data.featured ?? 0),
likes: Number(data.likes ?? 0),
recent_uploads: Number(data.recent_uploads ?? 0),
status: data.status ?? 'draft',
is_active: Boolean(data.is_active),
};
}
export async function getEventJoinTokens(slug: string): Promise<EventJoinToken[]> {
const response = await authorizedFetch(`${eventEndpoint(slug)}/join-tokens`);
const payload = await jsonOrThrow<{ data: JsonValue[] }>(response, 'Failed to load join tokens');
const list = Array.isArray(payload.data) ? payload.data : [];
return list.map(normalizeJoinToken);
}
export async function createInviteLink(
slug: string,
payload?: { label?: string; usage_limit?: number; expires_at?: string }
): Promise<EventJoinToken> {
const body = JSON.stringify(payload ?? {});
const response = await authorizedFetch(`${eventEndpoint(slug)}/join-tokens`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body,
});
const data = await jsonOrThrow<{ data: JsonValue }>(response, 'Failed to create invite');
return normalizeJoinToken(data.data ?? {});
}
export async function revokeEventJoinToken(
slug: string,
tokenId: number,
reason?: string
): Promise<EventJoinToken> {
const options: RequestInit = { method: 'DELETE' };
if (reason) {
options.headers = { 'Content-Type': 'application/json' };
options.body = JSON.stringify({ reason });
}
const response = await authorizedFetch(`${eventEndpoint(slug)}/join-tokens/${tokenId}`, options);
const data = await jsonOrThrow<{ data: JsonValue }>(response, 'Failed to revoke join token');
return normalizeJoinToken(data.data ?? {});
}
export type Package = {
id: number;
name: string;
price: number;
max_photos: number | null;
max_guests: number | null;
gallery_days: number | null;
features: Record<string, boolean>;
};
export async function getPackages(type: 'endcustomer' | 'reseller' = 'endcustomer'): Promise<Package[]> {
const response = await authorizedFetch(`/api/v1/packages?type=${type}`);
const data = await jsonOrThrow<{ data: Package[] }>(response, 'Failed to load packages');
return data.data ?? [];
}
type TenantPackagesResponse = {
data?: JsonValue[];
active_package?: JsonValue | null;
message?: string;
};
type LedgerResponse = {
data?: JsonValue[];
meta?: Partial<PaginationMeta>;
current_page?: number;
last_page?: number;
per_page?: number;
total?: number;
};
type TaskCollectionResponse = {
data?: JsonValue[];
meta?: Partial<PaginationMeta>;
current_page?: number;
last_page?: number;
per_page?: number;
total?: number;
};
type MemberResponse = {
data?: JsonValue[];
meta?: Partial<PaginationMeta>;
current_page?: number;
last_page?: number;
per_page?: number;
total?: number;
};
async function fetchTenantPackagesEndpoint(): Promise<Response> {
const first = await authorizedFetch('/api/v1/tenant/tenant/packages');
if (first.status === 404) {
return authorizedFetch('/api/v1/tenant/packages');
}
return first;
}
export async function getDashboardSummary(): Promise<DashboardSummary | null> {
const response = await authorizedFetch('/api/v1/tenant/dashboard');
if (response.status === 404) {
return null;
}
if (!response.ok) {
const payload = await safeJson(response);
console.error('[API] Failed to load dashboard', response.status, payload);
throw new Error('Failed to load dashboard');
}
const json = (await response.json()) as JsonValue;
return normalizeDashboard(json);
}
export async function getTenantPackagesOverview(): Promise<{
packages: TenantPackageSummary[];
activePackage: TenantPackageSummary | null;
}> {
const response = await fetchTenantPackagesEndpoint();
if (!response.ok) {
const payload = await safeJson(response);
console.error('[API] Failed to load tenant packages', response.status, payload);
throw new Error('Failed to load tenant packages');
}
const data = (await response.json()) as TenantPackagesResponse;
const packages = Array.isArray(data.data) ? data.data.map(normalizeTenantPackage) : [];
const activePackage = data.active_package ? normalizeTenantPackage(data.active_package) : null;
return { packages, activePackage };
}
export async function getCreditBalance(): Promise<CreditBalance> {
const response = await authorizedFetch('/api/v1/tenant/credits/balance');
if (response.status === 404) {
return { balance: 0 };
}
const data = await jsonOrThrow<CreditBalance>(response, 'Failed to load credit balance');
return { balance: Number(data.balance ?? 0), free_event_granted_at: data.free_event_granted_at ?? null };
}
export async function getCreditLedger(page = 1): Promise<PaginatedResult<CreditLedgerEntry>> {
const response = await authorizedFetch(`/api/v1/tenant/credits/ledger?page=${page}`);
if (!response.ok) {
const payload = await safeJson(response);
console.error('[API] Failed to load credit ledger', response.status, payload);
throw new Error('Failed to load credit ledger');
}
const json = (await response.json()) as LedgerResponse;
const entries = Array.isArray(json.data) ? json.data.map((entry) => ({
id: Number(entry.id ?? 0),
delta: Number(entry.delta ?? 0),
reason: String(entry.reason ?? 'unknown'),
note: entry.note ?? null,
related_purchase_id: entry.related_purchase_id ?? null,
created_at: entry.created_at ?? '',
})) : [];
return {
data: entries,
meta: buildPagination(json as JsonValue, entries.length),
};
}
export async function createTenantPackagePaymentIntent(packageId: number): Promise<string> {
const response = await authorizedFetch('/api/v1/tenant/packages/payment-intent', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ package_id: packageId }),
});
const data = await jsonOrThrow<{ client_secret: string }>(
response,
'Failed to create package payment intent'
);
if (!data.client_secret) {
throw new Error('Missing client secret in response');
}
return data.client_secret;
}
export async function completeTenantPackagePurchase(params: {
packageId: number;
paymentMethodId?: string;
paypalOrderId?: string;
}): Promise<void> {
const { packageId, paymentMethodId, paypalOrderId } = params;
const payload: Record<string, unknown> = { package_id: packageId };
if (paymentMethodId) {
payload.payment_method_id = paymentMethodId;
}
if (paypalOrderId) {
payload.paypal_order_id = paypalOrderId;
}
const response = await authorizedFetch('/api/v1/tenant/packages/complete', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(payload),
});
await jsonOrThrow(response, 'Failed to complete package purchase');
}
export async function assignFreeTenantPackage(packageId: number): Promise<void> {
const response = await authorizedFetch('/api/v1/tenant/packages/free', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ package_id: packageId }),
});
await jsonOrThrow(response, 'Failed to assign free package');
}
export async function createTenantPayPalOrder(packageId: number): Promise<string> {
const response = await authorizedFetch('/api/v1/tenant/packages/paypal-create', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ package_id: packageId }),
});
const data = await jsonOrThrow<{ orderID: string }>(response, 'Failed to create PayPal order');
if (!data.orderID) {
throw new Error('Missing PayPal order ID');
}
return data.orderID;
}
export async function captureTenantPayPalOrder(orderId: string): Promise<void> {
const response = await authorizedFetch('/api/v1/tenant/packages/paypal-capture', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ order_id: orderId }),
});
await jsonOrThrow(response, 'Failed to capture PayPal order');
}
export async function recordCreditPurchase(payload: {
package_id: string;
credits_added: number;
platform?: string;
transaction_id?: string;
subscription_active?: boolean;
}): Promise<CreditBalance> {
const response = await authorizedFetch('/api/v1/tenant/credits/purchase', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
const data = await jsonOrThrow<{ message: string; balance: number; subscription_active?: boolean }>(
response,
'Failed to record credit purchase'
);
return { balance: Number(data.balance ?? 0) };
}
export async function syncCreditBalance(payload: {
balance: number;
subscription_active?: boolean;
last_sync?: string;
}): Promise<{ balance: number; subscription_active: boolean; server_time: string }> {
const response = await authorizedFetch('/api/v1/tenant/credits/sync', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
return jsonOrThrow(response, 'Failed to sync credit balance');
}
export async function getTasks(params: { page?: number; per_page?: number; search?: string } = {}): Promise<PaginatedResult<TenantTask>> {
const searchParams = new URLSearchParams();
if (params.page) searchParams.set('page', String(params.page));
if (params.per_page) searchParams.set('per_page', String(params.per_page));
if (params.search) searchParams.set('search', params.search);
const response = await authorizedFetch(`/api/v1/tenant/tasks?${searchParams.toString()}`);
if (!response.ok) {
const payload = await safeJson(response);
console.error('[API] Failed to load tasks', response.status, payload);
throw new Error('Failed to load tasks');
}
const json = (await response.json()) as TaskCollectionResponse;
const tasks = Array.isArray(json.data) ? json.data.map(normalizeTask) : [];
return {
data: tasks,
meta: buildPagination(json as JsonValue, tasks.length),
};
}
export async function createTask(payload: TaskPayload): Promise<TenantTask> {
const response = await authorizedFetch(`/api/v1/tenant/tasks`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
const data = await jsonOrThrow<{ data: JsonValue }>(response, 'Failed to create task');
return normalizeTask(data.data);
}
export async function updateTask(taskId: number, payload: TaskPayload): Promise<TenantTask> {
const response = await authorizedFetch(`/api/v1/tenant/tasks/${taskId}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
const data = await jsonOrThrow<{ data: JsonValue }>(response, 'Failed to update task');
return normalizeTask(data.data);
}
export async function deleteTask(taskId: number): Promise<void> {
const response = await authorizedFetch(`/api/v1/tenant/tasks/${taskId}`, {
method: 'DELETE',
});
if (!response.ok) {
const payload = await safeJson(response);
console.error('[API] Failed to delete task', response.status, payload);
throw new Error('Failed to delete task');
}
}
export async function assignTasksToEvent(eventId: number, taskIds: number[]): Promise<void> {
const response = await authorizedFetch(`/api/v1/tenant/tasks/bulk-assign-event/${eventId}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ task_ids: taskIds }),
});
if (!response.ok) {
const payload = await safeJson(response);
console.error('[API] Failed to assign tasks', response.status, payload);
throw new Error('Failed to assign tasks');
}
}
export async function getEventTasks(eventId: number, page = 1): Promise<PaginatedResult<TenantTask>> {
const response = await authorizedFetch(`/api/v1/tenant/tasks/event/${eventId}?page=${page}`);
if (!response.ok) {
const payload = await safeJson(response);
console.error('[API] Failed to load event tasks', response.status, payload);
throw new Error('Failed to load event tasks');
}
const json = (await response.json()) as TaskCollectionResponse;
const tasks = Array.isArray(json.data) ? json.data.map(normalizeTask) : [];
return {
data: tasks,
meta: buildPagination(json as JsonValue, tasks.length),
};
}
export async function getEventMembers(eventIdentifier: number | string, page = 1): Promise<PaginatedResult<EventMember>> {
const response = await authorizedFetch(
`/api/v1/tenant/events/${encodeURIComponent(String(eventIdentifier))}/members?page=${page}`
);
if (response.status === 404) {
return { data: [], meta: { current_page: 1, last_page: 1, per_page: 0, total: 0 } };
}
if (!response.ok) {
const payload = await safeJson(response);
console.error('[API] Failed to load event members', response.status, payload);
throw new Error('Failed to load event members');
}
const json = (await response.json()) as MemberResponse;
const members = Array.isArray(json.data) ? json.data.map(normalizeMember) : [];
return {
data: members,
meta: buildPagination(json as JsonValue, members.length),
};
}
export async function inviteEventMember(eventIdentifier: number | string, payload: { email: string; role?: string; name?: string }): Promise<EventMember> {
const response = await authorizedFetch(
`/api/v1/tenant/events/${encodeURIComponent(String(eventIdentifier))}/members`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
}
);
if (response.status === 404) {
throw new Error('Mitgliederverwaltung ist fuer dieses Event noch nicht verfuegbar.');
}
const data = await jsonOrThrow<{ data: JsonValue }>(response, 'Failed to invite member');
return normalizeMember(data.data);
}
export async function removeEventMember(eventIdentifier: number | string, memberId: number): Promise<void> {
const response = await authorizedFetch(
`/api/v1/tenant/events/${encodeURIComponent(String(eventIdentifier))}/members/${memberId}`,
{ method: 'DELETE' }
);
if (response.status === 404) {
throw new Error('Mitglied konnte nicht gefunden werden.');
}
if (!response.ok) {
const payload = await safeJson(response);
console.error('[API] Failed to remove member', response.status, payload);
throw new Error('Failed to remove member');
}
}