Fix PayPal billing flow and mobile admin UX
Some checks failed
linter / quality (push) Has been cancelled
tests / ci (push) Has been cancelled
tests / ui (push) Has been cancelled

This commit is contained in:
Codex Agent
2026-02-05 10:19:29 +01:00
parent c43327af74
commit 0d7a861875
39 changed files with 1630 additions and 253 deletions

View File

@@ -571,6 +571,19 @@ export type LemonSqueezyOrderSummary = {
tax?: number | null;
};
export type TenantBillingTransactionSummary = {
id: string | number | null;
status: string | null;
amount: number | null;
currency: string | null;
provider: string | null;
provider_id: string | null;
package_name: string | null;
purchased_at: string | null;
receipt_url?: string | null;
tax?: number | null;
};
export type TenantAddonEventSummary = {
id: number;
slug: string;
@@ -1125,21 +1138,21 @@ export function normalizeTenantPackage(pkg: JsonValue): TenantPackageSummary {
};
}
function normalizeLemonSqueezyOrder(entry: JsonValue): LemonSqueezyOrderSummary {
const amountValue = entry.amount ?? entry.grand_total ?? (entry.totals && entry.totals.grand_total);
const taxValue = entry.tax ?? (entry.totals && entry.totals.tax_total);
function normalizeTenantBillingTransaction(entry: JsonValue): TenantBillingTransactionSummary {
const idValue = (entry as { id?: unknown }).id;
const amountValue = (entry as { amount?: unknown }).amount;
const taxValue = (entry as { tax?: unknown }).tax;
return {
id: typeof entry.id === 'string' ? entry.id : entry.id ? String(entry.id) : null,
status: entry.status ?? null,
id: typeof idValue === 'string' || typeof idValue === 'number' ? idValue : idValue ? String(idValue) : null,
status: typeof (entry as { status?: unknown }).status === 'string' ? (entry as { status?: unknown }).status : null,
amount: amountValue !== undefined && amountValue !== null ? Number(amountValue) : null,
currency: entry.currency ?? entry.currency_code ?? 'EUR',
origin: entry.origin ?? null,
checkout_id: entry.checkout_id ?? (entry.details?.checkout_id ?? null),
created_at: entry.created_at ?? null,
updated_at: entry.updated_at ?? null,
receipt_url: entry.receipt_url ?? entry.invoice_url ?? null,
grand_total: entry.grand_total !== undefined && entry.grand_total !== null ? Number(entry.grand_total) : null,
currency: typeof (entry as { currency?: unknown }).currency === 'string' ? (entry as { currency?: unknown }).currency : 'EUR',
provider: typeof (entry as { provider?: unknown }).provider === 'string' ? (entry as { provider?: unknown }).provider : null,
provider_id: typeof (entry as { provider_id?: unknown }).provider_id === 'string' ? (entry as { provider_id?: unknown }).provider_id : null,
package_name: typeof (entry as { package_name?: unknown }).package_name === 'string' ? (entry as { package_name?: unknown }).package_name : null,
purchased_at: typeof (entry as { purchased_at?: unknown }).purchased_at === 'string' ? (entry as { purchased_at?: unknown }).purchased_at : null,
receipt_url: typeof (entry as { receipt_url?: unknown }).receipt_url === 'string' ? (entry as { receipt_url?: unknown }).receipt_url : null,
tax: taxValue !== undefined && taxValue !== null ? Number(taxValue) : null,
};
}
@@ -2731,32 +2744,43 @@ export async function downloadTenantDataExport(downloadUrl: string): Promise<Blo
return response.blob();
}
export async function getTenantLemonSqueezyTransactions(cursor?: string): Promise<{
data: LemonSqueezyOrderSummary[];
nextCursor: string | null;
hasMore: boolean;
export async function downloadTenantBillingReceipt(receiptUrl: string): Promise<Blob> {
const response = await authorizedFetch(receiptUrl, {
headers: { 'Accept': 'application/pdf' },
});
if (!response.ok) {
const payload = await safeJson(response);
console.error('[API] Failed to download billing receipt', response.status, payload);
throw new Error('Failed to download billing receipt');
}
return response.blob();
}
export async function getTenantBillingTransactions(page = 1): Promise<{
data: TenantBillingTransactionSummary[];
}> {
const query = cursor ? `?cursor=${encodeURIComponent(cursor)}` : '';
const response = await authorizedFetch(`/api/v1/tenant/billing/transactions${query}`);
const params = new URLSearchParams({
page: String(Math.max(1, page)),
});
const response = await authorizedFetch(`/api/v1/tenant/billing/transactions?${params.toString()}`);
if (response.status === 404) {
return { data: [], nextCursor: null, hasMore: false };
return { data: [] };
}
if (!response.ok) {
const payload = await safeJson(response);
console.error('[API] Failed to load Lemon Squeezy transactions', response.status, payload);
throw new Error('Failed to load Lemon Squeezy transactions');
console.error('[API] Failed to load billing transactions', response.status, payload);
throw new Error('Failed to load billing transactions');
}
const payload = await safeJson(response) ?? {};
const entries = Array.isArray(payload.data) ? payload.data : [];
const meta = payload.meta ?? {};
return {
data: entries.map(normalizeLemonSqueezyOrder),
nextCursor: typeof meta.next === 'string' ? meta.next : null,
hasMore: Boolean(meta.has_more),
data: entries.map(normalizeTenantBillingTransaction),
};
}