fixed notification system and added a new tenant notifications receipt table to track read status and filter messages by scope.

This commit is contained in:
Codex Agent
2025-12-17 10:57:19 +01:00
parent 0aae494945
commit d64839ba2f
31 changed files with 1089 additions and 127 deletions

View File

@@ -386,6 +386,19 @@ export type NotificationPreferences = Record<string, boolean>;
export type NotificationPreferencesMeta = Record<string, never>;
export type NotificationLogEntry = {
id: number;
type: string;
channel: string;
recipient: string | null;
status: string;
context: Record<string, unknown> | null;
sent_at: string | null;
failed_at: string | null;
failure_reason: string | null;
is_read?: boolean;
};
export type PaddleTransactionSummary = {
id: string | null;
status: string | null;
@@ -2016,6 +2029,71 @@ export async function updateNotificationPreferences(
};
}
function normalizeNotificationLog(entry: JsonValue): NotificationLogEntry | null {
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
return null;
}
const row = entry as Record<string, JsonValue>;
return {
id: Number(row.id ?? 0),
type: typeof row.type === 'string' ? row.type : '',
channel: typeof row.channel === 'string' ? row.channel : '',
recipient: typeof row.recipient === 'string' ? row.recipient : null,
status: typeof row.status === 'string' ? row.status : '',
context: (row.context && typeof row.context === 'object' && !Array.isArray(row.context)) ? (row.context as Record<string, unknown>) : null,
sent_at: typeof row.sent_at === 'string' ? row.sent_at : null,
failed_at: typeof row.failed_at === 'string' ? row.failed_at : null,
failure_reason: typeof row.failure_reason === 'string' ? row.failure_reason : null,
};
}
export async function listNotificationLogs(options?: {
page?: number;
perPage?: number;
type?: string;
status?: string;
scope?: string;
eventId?: number;
}): Promise<{
data: NotificationLogEntry[];
meta: PaginationMeta & { unread_count?: number };
}> {
const params = new URLSearchParams();
if (options?.page) params.set('page', String(options.page));
if (options?.perPage) params.set('per_page', String(options.perPage));
if (options?.type) params.set('type', options.type);
if (options?.status) params.set('status', options.status);
if (options?.scope) params.set('scope', options.scope);
if (options?.eventId) params.set('event_id', String(options.eventId));
const response = await authorizedFetch(`/api/v1/tenant/notifications/logs${params.toString() ? `?${params.toString()}` : ''}`);
const payload = await jsonOrThrow<{ data?: JsonValue[]; meta?: Partial<PaginationMeta> }>(
response,
'Failed to load notification logs'
);
const rows = Array.isArray(payload.data) ? payload.data : [];
const meta = buildPagination((payload.meta ?? {}) as JsonValue, 0) as PaginationMeta & { unread_count?: number };
if (payload.meta && typeof (payload.meta as any).unread_count === 'number') {
meta.unread_count = (payload.meta as any).unread_count as number;
}
return {
data: rows.map((row) => normalizeNotificationLog(row)).filter((row): row is NotificationLogEntry => Boolean(row)),
meta,
};
}
export async function markNotificationLogs(ids: number[], status: 'read' | 'dismissed'): Promise<void> {
await authorizedFetch('/api/v1/tenant/notifications/logs/mark', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ids, status }),
});
}
export async function getTenantPaddleTransactions(cursor?: string): Promise<{
data: PaddleTransactionSummary[];
nextCursor: string | null;