Implement compliance exports and retention overrides
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-01-02 20:13:45 +01:00
parent 5fd546c428
commit eed7699549
45 changed files with 2319 additions and 40 deletions

View File

@@ -430,6 +430,23 @@ export type NotificationLogEntry = {
is_read?: boolean;
};
export type DataExportSummary = {
id: number;
scope: 'tenant' | 'event';
status: 'pending' | 'processing' | 'ready' | 'failed';
include_media: boolean;
size_bytes: number | null;
created_at: string | null;
expires_at: string | null;
download_url: string | null;
error_message?: string | null;
event?: {
id: number;
slug: string;
name: string | Record<string, string>;
} | null;
};
export type PaddleTransactionSummary = {
id: string | null;
status: string | null;
@@ -2133,6 +2150,39 @@ function normalizeNotificationLog(entry: JsonValue): NotificationLogEntry | null
};
}
function normalizeDataExport(entry: JsonValue): DataExportSummary | null {
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
return null;
}
const row = entry as Record<string, JsonValue>;
const event = row.event;
const eventRecord = event && typeof event === 'object' && !Array.isArray(event)
? (event as Record<string, JsonValue>)
: null;
return {
id: Number(row.id ?? 0),
scope: row.scope === 'event' ? 'event' : 'tenant',
status: typeof row.status === 'string' ? (row.status as DataExportSummary['status']) : 'pending',
include_media: Boolean(row.include_media),
size_bytes: typeof row.size_bytes === 'number' ? row.size_bytes : null,
created_at: typeof row.created_at === 'string' ? row.created_at : null,
expires_at: typeof row.expires_at === 'string' ? row.expires_at : null,
download_url: typeof row.download_url === 'string' ? row.download_url : null,
error_message: typeof row.error_message === 'string' ? row.error_message : null,
event: eventRecord
? {
id: Number(eventRecord.id ?? 0),
slug: typeof eventRecord.slug === 'string' ? eventRecord.slug : '',
name: typeof eventRecord.name === 'string' || typeof eventRecord.name === 'object'
? (eventRecord.name as DataExportSummary['event']['name'])
: '',
}
: null,
};
}
export async function listNotificationLogs(options?: {
page?: number;
perPage?: number;
@@ -2178,6 +2228,51 @@ export async function markNotificationLogs(ids: number[], status: 'read' | 'dism
});
}
export async function listTenantDataExports(): Promise<DataExportSummary[]> {
const response = await authorizedFetch('/api/v1/tenant/exports');
const payload = await jsonOrThrow<{ data?: JsonValue[] }>(response, 'Failed to load data exports');
const rows = Array.isArray(payload.data) ? payload.data : [];
return rows
.map((row) => normalizeDataExport(row))
.filter((row): row is DataExportSummary => Boolean(row));
}
export async function requestTenantDataExport(payload: {
scope: 'tenant' | 'event';
eventId?: number;
includeMedia?: boolean;
}): Promise<DataExportSummary | null> {
const response = await authorizedFetch('/api/v1/tenant/exports', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
scope: payload.scope,
event_id: payload.eventId,
include_media: payload.includeMedia,
}),
});
const body = await jsonOrThrow<{ data?: JsonValue }>(response, 'Failed to request export');
const record = body.data ? normalizeDataExport(body.data) : null;
return record ?? null;
}
export async function downloadTenantDataExport(downloadUrl: string): Promise<Blob> {
const response = await authorizedFetch(downloadUrl, {
headers: { 'Accept': 'application/octet-stream' },
});
if (!response.ok) {
const payload = await safeJson(response);
console.error('[API] Failed to download data export', response.status, payload);
throw new Error('Failed to download data export');
}
return response.blob();
}
export async function getTenantPaddleTransactions(cursor?: string): Promise<{
data: PaddleTransactionSummary[];
nextCursor: string | null;