implemented event package addons with filament resource, event-admin purchase path and notifications, showing up in purchase history
This commit is contained in:
@@ -82,6 +82,7 @@ export type TenantEvent = {
|
||||
expires_at: string | null;
|
||||
} | null;
|
||||
limits?: EventLimitSummary | null;
|
||||
addons?: EventAddonSummary[];
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
@@ -156,6 +157,32 @@ export type PhotoboothStatus = {
|
||||
};
|
||||
};
|
||||
|
||||
export type EventAddonCheckout = {
|
||||
addon_key: string;
|
||||
quantity?: number;
|
||||
checkout_url: string | null;
|
||||
checkout_id: string | null;
|
||||
expires_at: string | null;
|
||||
};
|
||||
|
||||
export type EventAddonCatalogItem = {
|
||||
key: string;
|
||||
label: string;
|
||||
price_id: string | null;
|
||||
increments?: Record<string, number>;
|
||||
};
|
||||
|
||||
export type EventAddonSummary = {
|
||||
id: number;
|
||||
key: string;
|
||||
label?: string | null;
|
||||
status: 'pending' | 'completed' | 'failed';
|
||||
extra_photos: number;
|
||||
extra_guests: number;
|
||||
extra_gallery_days: number;
|
||||
purchased_at: string | null;
|
||||
};
|
||||
|
||||
export type HelpCenterArticleSummary = {
|
||||
slug: string;
|
||||
title: string;
|
||||
@@ -338,6 +365,28 @@ export type PaddleTransactionSummary = {
|
||||
tax?: number | null;
|
||||
};
|
||||
|
||||
export type TenantAddonEventSummary = {
|
||||
id: number;
|
||||
slug: string;
|
||||
name: string | Record<string, string> | null;
|
||||
};
|
||||
|
||||
export type TenantAddonHistoryEntry = {
|
||||
id: number;
|
||||
addon_key: string;
|
||||
label?: string | null;
|
||||
event: TenantAddonEventSummary | null;
|
||||
amount: number | null;
|
||||
currency: string | null;
|
||||
status: 'pending' | 'completed' | 'failed';
|
||||
purchased_at: string | null;
|
||||
extra_photos: number;
|
||||
extra_guests: number;
|
||||
extra_gallery_days: number;
|
||||
quantity: number;
|
||||
receipt_url?: string | null;
|
||||
};
|
||||
|
||||
export type CreditLedgerEntry = {
|
||||
id: number;
|
||||
delta: number;
|
||||
@@ -829,6 +878,48 @@ function normalizePaddleTransaction(entry: JsonValue): PaddleTransactionSummary
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeTenantAddonHistoryEntry(entry: JsonValue): TenantAddonHistoryEntry {
|
||||
let event: TenantAddonEventSummary | null = null;
|
||||
|
||||
if (entry.event && typeof entry.event === 'object') {
|
||||
const rawEvent = entry.event as JsonValue;
|
||||
const id = Number((rawEvent as { id?: unknown }).id ?? 0);
|
||||
const slugValue = (rawEvent as { slug?: unknown }).slug;
|
||||
const rawName = (rawEvent as { name?: unknown }).name ?? null;
|
||||
let name: TenantAddonEventSummary['name'] = null;
|
||||
|
||||
if (typeof rawName === 'string') {
|
||||
name = rawName;
|
||||
} else if (rawName && typeof rawName === 'object') {
|
||||
name = normalizeTranslationMap(rawName, undefined, true);
|
||||
}
|
||||
|
||||
event = {
|
||||
id,
|
||||
slug: typeof slugValue === 'string' ? slugValue : '',
|
||||
name,
|
||||
};
|
||||
}
|
||||
|
||||
const amountValue = entry.amount;
|
||||
|
||||
return {
|
||||
id: Number(entry.id ?? 0),
|
||||
addon_key: String(entry.addon_key ?? ''),
|
||||
label: typeof entry.label === 'string' ? entry.label : null,
|
||||
event,
|
||||
amount: amountValue !== undefined && amountValue !== null ? Number(amountValue) : null,
|
||||
currency: typeof entry.currency === 'string' ? entry.currency : null,
|
||||
status: (entry.status as TenantAddonHistoryEntry['status']) ?? 'pending',
|
||||
purchased_at: typeof entry.purchased_at === 'string' ? entry.purchased_at : null,
|
||||
extra_photos: Number(entry.extra_photos ?? 0),
|
||||
extra_guests: Number(entry.extra_guests ?? 0),
|
||||
extra_gallery_days: Number(entry.extra_gallery_days ?? 0),
|
||||
quantity: Number(entry.quantity ?? 1),
|
||||
receipt_url: typeof entry.receipt_url === 'string' ? entry.receipt_url : null,
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeTask(task: JsonValue): TenantTask {
|
||||
const titleTranslations = normalizeTranslationMap(task.title_translations ?? task.title ?? {});
|
||||
const descriptionTranslations = normalizeTranslationMap(task.description_translations ?? task.description ?? {});
|
||||
@@ -1122,6 +1213,28 @@ export async function getEvent(slug: string): Promise<TenantEvent> {
|
||||
return normalizeEvent(data.data);
|
||||
}
|
||||
|
||||
export async function createEventAddonCheckout(
|
||||
eventSlug: string,
|
||||
params: { addon_key: string; quantity?: number; success_url?: string; cancel_url?: string }
|
||||
): Promise<{ checkout_url: string | null; checkout_id: string | null; expires_at: string | null }> {
|
||||
const response = await authorizedFetch(`${eventEndpoint(eventSlug)}/addons/checkout`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(params),
|
||||
});
|
||||
|
||||
return await jsonOrThrow<{ checkout_url: string | null; checkout_id: string | null; expires_at: string | null }>(
|
||||
response,
|
||||
'Failed to create addon checkout'
|
||||
);
|
||||
}
|
||||
|
||||
export async function getAddonCatalog(): Promise<EventAddonCatalogItem[]> {
|
||||
const response = await authorizedFetch('/api/v1/tenant/addons/catalog');
|
||||
const data = await jsonOrThrow<{ data?: EventAddonCatalogItem[] }>(response, 'Failed to load add-ons');
|
||||
return data.data ?? [];
|
||||
}
|
||||
|
||||
export async function getEventTypes(): Promise<TenantEventType[]> {
|
||||
const response = await authorizedFetch('/api/v1/tenant/event-types');
|
||||
const data = await jsonOrThrow<{ data?: JsonValue[] }>(response, 'Failed to load event types');
|
||||
@@ -1675,6 +1788,42 @@ export async function getTenantPaddleTransactions(cursor?: string): Promise<{
|
||||
};
|
||||
}
|
||||
|
||||
export async function getTenantAddonHistory(page = 1, perPage = 25): Promise<{
|
||||
data: TenantAddonHistoryEntry[];
|
||||
meta: PaginationMeta;
|
||||
}> {
|
||||
const params = new URLSearchParams({
|
||||
page: String(Math.max(1, page)),
|
||||
per_page: String(Math.max(1, Math.min(perPage, 100))),
|
||||
});
|
||||
|
||||
const response = await authorizedFetch(`/api/v1/tenant/billing/addons?${params.toString()}`);
|
||||
|
||||
if (response.status === 404) {
|
||||
return {
|
||||
data: [],
|
||||
meta: { current_page: 1, last_page: 1, per_page: perPage, total: 0 },
|
||||
};
|
||||
}
|
||||
|
||||
const payload = await jsonOrThrow<{ data?: JsonValue[]; meta?: Partial<PaginationMeta>; current_page?: number; last_page?: number; per_page?: number; total?: number }>(
|
||||
response,
|
||||
'Failed to load add-on history'
|
||||
);
|
||||
|
||||
const rows = Array.isArray(payload.data) ? payload.data.map((row) => normalizeTenantAddonHistoryEntry(row)) : [];
|
||||
const metaSource = payload.meta ?? payload;
|
||||
|
||||
const meta: PaginationMeta = {
|
||||
current_page: Number(metaSource.current_page ?? 1),
|
||||
last_page: Number(metaSource.last_page ?? 1),
|
||||
per_page: Number(metaSource.per_page ?? perPage),
|
||||
total: Number(metaSource.total ?? rows.length),
|
||||
};
|
||||
|
||||
return { data: rows, meta };
|
||||
}
|
||||
|
||||
export async function getCreditBalance(): Promise<CreditBalance> {
|
||||
const response = await authorizedFetch('/api/v1/tenant/credits/balance');
|
||||
if (response.status === 404) {
|
||||
|
||||
Reference in New Issue
Block a user