/** * DSR API Client * * API client for Data Subject Request management. * Connects to the native compliance backend (Python/FastAPI). */ import { DSRRequest, DSRCreateRequest, DSRStatistics, } from './types' // ============================================================================= // SDK API FUNCTIONS (via Next.js proxy to compliance backend) // ============================================================================= interface BackendDSR { id: string tenant_id: string request_number: string request_type: string status: string priority: string requester_name: string requester_email: string requester_phone?: string requester_address?: string requester_customer_id?: string source: string source_details?: string request_text?: string notes?: string internal_notes?: string received_at: string deadline_at: string extended_deadline_at?: string extension_reason?: string extension_approved_by?: string extension_approved_at?: string identity_verified: boolean verification_method?: string verified_at?: string verified_by?: string verification_notes?: string verification_document_ref?: string assigned_to?: string assigned_at?: string assigned_by?: string completed_at?: string completion_notes?: string rejection_reason?: string rejection_legal_basis?: string erasure_checklist?: any[] data_export?: any rectification_details?: any objection_details?: any affected_systems?: string[] created_at: string updated_at: string created_by?: string updated_by?: string } /** * Transform flat backend DSR to nested SDK DSRRequest format. * New compliance backend already uses the same status names as frontend types. */ export function transformBackendDSR(b: BackendDSR): DSRRequest { return { id: b.id, referenceNumber: b.request_number, type: b.request_type as DSRRequest['type'], status: (b.status as DSRRequest['status']) || 'intake', priority: (b.priority as DSRRequest['priority']) || 'normal', requester: { name: b.requester_name, email: b.requester_email, phone: b.requester_phone, address: b.requester_address, customerId: b.requester_customer_id, }, source: (b.source as DSRRequest['source']) || 'email', sourceDetails: b.source_details, requestText: b.request_text, receivedAt: b.received_at, deadline: { originalDeadline: b.deadline_at, currentDeadline: b.extended_deadline_at || b.deadline_at, extended: !!b.extended_deadline_at, extensionReason: b.extension_reason, extensionApprovedBy: b.extension_approved_by, extensionApprovedAt: b.extension_approved_at, }, completedAt: b.completed_at, identityVerification: { verified: b.identity_verified, method: b.verification_method as any, verifiedAt: b.verified_at, verifiedBy: b.verified_by, notes: b.verification_notes, documentRef: b.verification_document_ref, }, assignment: { assignedTo: b.assigned_to || null, assignedAt: b.assigned_at, assignedBy: b.assigned_by, }, notes: b.notes, internalNotes: b.internal_notes, erasureChecklist: b.erasure_checklist ? { items: b.erasure_checklist, canProceedWithErasure: true } : undefined, dataExport: b.data_export && Object.keys(b.data_export).length > 0 ? b.data_export : undefined, rectificationDetails: b.rectification_details && Object.keys(b.rectification_details).length > 0 ? b.rectification_details : undefined, objectionDetails: b.objection_details && Object.keys(b.objection_details).length > 0 ? b.objection_details : undefined, createdAt: b.created_at, createdBy: b.created_by || 'system', updatedAt: b.updated_at, updatedBy: b.updated_by, tenantId: b.tenant_id, } } function getSdkHeaders(): HeadersInit { if (typeof window === 'undefined') return {} return { 'Content-Type': 'application/json', 'X-Tenant-ID': localStorage.getItem('bp_tenant_id') || '', 'X-User-ID': localStorage.getItem('bp_user_id') || '', } } /** * Fetch DSR list from compliance backend via proxy */ export async function fetchSDKDSRList(): Promise<{ requests: DSRRequest[]; statistics: DSRStatistics }> { // Fetch list and stats in parallel const [listRes, statsRes] = await Promise.all([ fetch('/api/sdk/v1/compliance/dsr?limit=100', { headers: getSdkHeaders() }), fetch('/api/sdk/v1/compliance/dsr/stats', { headers: getSdkHeaders() }), ]) if (!listRes.ok) { throw new Error(`HTTP ${listRes.status}`) } const listData = await listRes.json() const backendDSRs: BackendDSR[] = listData.requests || [] const requests = backendDSRs.map(transformBackendDSR) let statistics: DSRStatistics if (statsRes.ok) { const statsData = await statsRes.json() statistics = { total: statsData.total || 0, byStatus: statsData.by_status || { intake: 0, identity_verification: 0, processing: 0, completed: 0, rejected: 0, cancelled: 0 }, byType: statsData.by_type || { access: 0, rectification: 0, erasure: 0, restriction: 0, portability: 0, objection: 0 }, overdue: statsData.overdue || 0, dueThisWeek: statsData.due_this_week || 0, averageProcessingDays: statsData.average_processing_days || 0, completedThisMonth: statsData.completed_this_month || 0, } } else { // Fallback: calculate locally const now = new Date() statistics = { total: requests.length, byStatus: { intake: requests.filter(r => r.status === 'intake').length, identity_verification: requests.filter(r => r.status === 'identity_verification').length, processing: requests.filter(r => r.status === 'processing').length, completed: requests.filter(r => r.status === 'completed').length, rejected: requests.filter(r => r.status === 'rejected').length, cancelled: requests.filter(r => r.status === 'cancelled').length, }, byType: { access: requests.filter(r => r.type === 'access').length, rectification: requests.filter(r => r.type === 'rectification').length, erasure: requests.filter(r => r.type === 'erasure').length, restriction: requests.filter(r => r.type === 'restriction').length, portability: requests.filter(r => r.type === 'portability').length, objection: requests.filter(r => r.type === 'objection').length, }, overdue: 0, dueThisWeek: 0, averageProcessingDays: 0, completedThisMonth: 0, } } return { requests, statistics } } /** * Create a new DSR via compliance backend */ export async function createSDKDSR(request: DSRCreateRequest): Promise { const body = { request_type: request.type, requester_name: request.requester.name, requester_email: request.requester.email, requester_phone: request.requester.phone || null, requester_address: request.requester.address || null, requester_customer_id: request.requester.customerId || null, source: request.source, source_details: request.sourceDetails || null, request_text: request.requestText || '', priority: request.priority || 'normal', } const res = await fetch('/api/sdk/v1/compliance/dsr', { method: 'POST', headers: getSdkHeaders(), body: JSON.stringify(body), }) if (!res.ok) { throw new Error(`HTTP ${res.status}`) } } /** * Fetch a single DSR by ID from compliance backend */ export async function fetchSDKDSR(id: string): Promise { const res = await fetch(`/api/sdk/v1/compliance/dsr/${id}`, { headers: getSdkHeaders(), }) if (!res.ok) { return null } const data = await res.json() if (!data || !data.id) return null return transformBackendDSR(data) } /** * Update DSR status via compliance backend */ export async function updateSDKDSRStatus(id: string, status: string): Promise { const res = await fetch(`/api/sdk/v1/compliance/dsr/${id}/status`, { method: 'POST', headers: getSdkHeaders(), body: JSON.stringify({ status }), }) if (!res.ok) { throw new Error(`HTTP ${res.status}`) } } // ============================================================================= // WORKFLOW ACTIONS // ============================================================================= /** * Verify identity of DSR requester */ export async function verifyDSRIdentity(id: string, data: { method: string; notes?: string; document_ref?: string }): Promise { const res = await fetch(`/api/sdk/v1/compliance/dsr/${id}/verify-identity`, { method: 'POST', headers: getSdkHeaders(), body: JSON.stringify(data), }) if (!res.ok) throw new Error(`HTTP ${res.status}`) return transformBackendDSR(await res.json()) } /** * Assign DSR to a user */ export async function assignDSR(id: string, assigneeId: string): Promise { const res = await fetch(`/api/sdk/v1/compliance/dsr/${id}/assign`, { method: 'POST', headers: getSdkHeaders(), body: JSON.stringify({ assignee_id: assigneeId }), }) if (!res.ok) throw new Error(`HTTP ${res.status}`) return transformBackendDSR(await res.json()) } /** * Extend DSR deadline (Art. 12 Abs. 3 DSGVO) */ export async function extendDSRDeadline(id: string, reason: string, days: number = 60): Promise { const res = await fetch(`/api/sdk/v1/compliance/dsr/${id}/extend`, { method: 'POST', headers: getSdkHeaders(), body: JSON.stringify({ reason, days }), }) if (!res.ok) throw new Error(`HTTP ${res.status}`) return transformBackendDSR(await res.json()) } /** * Complete a DSR */ export async function completeDSR(id: string, summary?: string): Promise { const res = await fetch(`/api/sdk/v1/compliance/dsr/${id}/complete`, { method: 'POST', headers: getSdkHeaders(), body: JSON.stringify({ summary }), }) if (!res.ok) throw new Error(`HTTP ${res.status}`) return transformBackendDSR(await res.json()) } /** * Reject a DSR with legal basis */ export async function rejectDSR(id: string, reason: string, legalBasis?: string): Promise { const res = await fetch(`/api/sdk/v1/compliance/dsr/${id}/reject`, { method: 'POST', headers: getSdkHeaders(), body: JSON.stringify({ reason, legal_basis: legalBasis }), }) if (!res.ok) throw new Error(`HTTP ${res.status}`) return transformBackendDSR(await res.json()) } // ============================================================================= // COMMUNICATIONS // ============================================================================= /** * Fetch communications for a DSR */ export async function fetchDSRCommunications(id: string): Promise { const res = await fetch(`/api/sdk/v1/compliance/dsr/${id}/communications`, { headers: getSdkHeaders(), }) if (!res.ok) throw new Error(`HTTP ${res.status}`) return res.json() } /** * Send a communication for a DSR */ export async function sendDSRCommunication(id: string, data: { communication_type?: string; channel?: string; subject?: string; content: string }): Promise { const res = await fetch(`/api/sdk/v1/compliance/dsr/${id}/communicate`, { method: 'POST', headers: getSdkHeaders(), body: JSON.stringify({ communication_type: 'outgoing', channel: 'email', ...data }), }) if (!res.ok) throw new Error(`HTTP ${res.status}`) return res.json() } // ============================================================================= // EXCEPTION CHECKS (Art. 17) // ============================================================================= /** * Fetch exception checks for an erasure DSR */ export async function fetchDSRExceptionChecks(id: string): Promise { const res = await fetch(`/api/sdk/v1/compliance/dsr/${id}/exception-checks`, { headers: getSdkHeaders(), }) if (!res.ok) throw new Error(`HTTP ${res.status}`) return res.json() } /** * Initialize Art. 17(3) exception checks for an erasure DSR */ export async function initDSRExceptionChecks(id: string): Promise { const res = await fetch(`/api/sdk/v1/compliance/dsr/${id}/exception-checks/init`, { method: 'POST', headers: getSdkHeaders(), }) if (!res.ok) throw new Error(`HTTP ${res.status}`) return res.json() } /** * Update a single exception check */ export async function updateDSRExceptionCheck(dsrId: string, checkId: string, data: { applies: boolean; notes?: string }): Promise { const res = await fetch(`/api/sdk/v1/compliance/dsr/${dsrId}/exception-checks/${checkId}`, { method: 'PUT', headers: getSdkHeaders(), body: JSON.stringify(data), }) if (!res.ok) throw new Error(`HTTP ${res.status}`) return res.json() } // ============================================================================= // HISTORY // ============================================================================= /** * Fetch status change history for a DSR */ export async function fetchDSRHistory(id: string): Promise { const res = await fetch(`/api/sdk/v1/compliance/dsr/${id}/history`, { headers: getSdkHeaders(), }) if (!res.ok) throw new Error(`HTTP ${res.status}`) return res.json() } /** * Update DSR fields (priority, notes, etc.) */ export async function updateDSR(id: string, data: Record): Promise { const res = await fetch(`/api/sdk/v1/compliance/dsr/${id}`, { method: 'PUT', headers: getSdkHeaders(), body: JSON.stringify(data), }) if (!res.ok) throw new Error(`HTTP ${res.status}`) return transformBackendDSR(await res.json()) } // ============================================================================= // MOCK DATA FUNCTIONS (kept as fallback) // ============================================================================= export function createMockDSRList(): DSRRequest[] { const now = new Date() return [ { id: 'dsr-001', referenceNumber: 'DSR-2025-000001', type: 'access', status: 'intake', priority: 'high', requester: { name: 'Max Mustermann', email: 'max.mustermann@example.de' }, source: 'web_form', sourceDetails: 'Kontaktformular auf breakpilot.de', receivedAt: new Date(now.getTime() - 2 * 24 * 60 * 60 * 1000).toISOString(), deadline: { originalDeadline: new Date(now.getTime() + 28 * 24 * 60 * 60 * 1000).toISOString(), currentDeadline: new Date(now.getTime() + 28 * 24 * 60 * 60 * 1000).toISOString(), extended: false }, identityVerification: { verified: false }, assignment: { assignedTo: null }, createdAt: new Date(now.getTime() - 2 * 24 * 60 * 60 * 1000).toISOString(), createdBy: 'system', updatedAt: new Date(now.getTime() - 2 * 24 * 60 * 60 * 1000).toISOString(), tenantId: 'default-tenant' }, { id: 'dsr-002', referenceNumber: 'DSR-2025-000002', type: 'erasure', status: 'identity_verification', priority: 'high', requester: { name: 'Anna Schmidt', email: 'anna.schmidt@example.de', phone: '+49 170 1234567' }, source: 'email', requestText: 'Ich moechte, dass alle meine Daten geloescht werden.', receivedAt: new Date(now.getTime() - 5 * 24 * 60 * 60 * 1000).toISOString(), deadline: { originalDeadline: new Date(now.getTime() + 9 * 24 * 60 * 60 * 1000).toISOString(), currentDeadline: new Date(now.getTime() + 9 * 24 * 60 * 60 * 1000).toISOString(), extended: false }, identityVerification: { verified: false }, assignment: { assignedTo: 'DSB Mueller', assignedAt: new Date(now.getTime() - 4 * 24 * 60 * 60 * 1000).toISOString() }, createdAt: new Date(now.getTime() - 5 * 24 * 60 * 60 * 1000).toISOString(), createdBy: 'system', updatedAt: new Date(now.getTime() - 4 * 24 * 60 * 60 * 1000).toISOString(), tenantId: 'default-tenant' }, { id: 'dsr-003', referenceNumber: 'DSR-2025-000003', type: 'rectification', status: 'processing', priority: 'normal', requester: { name: 'Peter Meier', email: 'peter.meier@example.de' }, source: 'email', requestText: 'Meine Adresse ist falsch gespeichert.', receivedAt: new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString(), deadline: { originalDeadline: new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000).toISOString(), currentDeadline: new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000).toISOString(), extended: false }, identityVerification: { verified: true, method: 'existing_account', verifiedAt: new Date(now.getTime() - 6 * 24 * 60 * 60 * 1000).toISOString(), verifiedBy: 'DSB Mueller' }, assignment: { assignedTo: 'DSB Mueller', assignedAt: new Date(now.getTime() - 6 * 24 * 60 * 60 * 1000).toISOString() }, rectificationDetails: { fieldsToCorrect: [ { field: 'Adresse', currentValue: 'Musterstr. 1, 12345 Berlin', requestedValue: 'Musterstr. 10, 12345 Berlin', corrected: false } ] }, createdAt: new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString(), createdBy: 'system', updatedAt: new Date(now.getTime() - 1 * 24 * 60 * 60 * 1000).toISOString(), tenantId: 'default-tenant' }, { id: 'dsr-004', referenceNumber: 'DSR-2025-000004', type: 'portability', status: 'processing', priority: 'normal', requester: { name: 'Lisa Weber', email: 'lisa.weber@example.de' }, source: 'web_form', receivedAt: new Date(now.getTime() - 10 * 24 * 60 * 60 * 1000).toISOString(), deadline: { originalDeadline: new Date(now.getTime() + 20 * 24 * 60 * 60 * 1000).toISOString(), currentDeadline: new Date(now.getTime() + 20 * 24 * 60 * 60 * 1000).toISOString(), extended: false }, identityVerification: { verified: true, method: 'id_document', verifiedAt: new Date(now.getTime() - 8 * 24 * 60 * 60 * 1000).toISOString(), verifiedBy: 'DSB Mueller' }, assignment: { assignedTo: 'IT Team', assignedAt: new Date(now.getTime() - 8 * 24 * 60 * 60 * 1000).toISOString() }, notes: 'JSON-Export wird vorbereitet', createdAt: new Date(now.getTime() - 10 * 24 * 60 * 60 * 1000).toISOString(), createdBy: 'system', updatedAt: new Date(now.getTime() - 2 * 24 * 60 * 60 * 1000).toISOString(), tenantId: 'default-tenant' }, { id: 'dsr-005', referenceNumber: 'DSR-2025-000005', type: 'objection', status: 'rejected', priority: 'low', requester: { name: 'Thomas Klein', email: 'thomas.klein@example.de' }, source: 'letter', requestText: 'Ich widerspreche der Verarbeitung meiner Daten fuer Marketingzwecke.', receivedAt: new Date(now.getTime() - 35 * 24 * 60 * 60 * 1000).toISOString(), deadline: { originalDeadline: new Date(now.getTime() - 5 * 24 * 60 * 60 * 1000).toISOString(), currentDeadline: new Date(now.getTime() - 5 * 24 * 60 * 60 * 1000).toISOString(), extended: false }, completedAt: new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString(), identityVerification: { verified: true, method: 'postal', verifiedAt: new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000).toISOString(), verifiedBy: 'DSB Mueller' }, assignment: { assignedTo: 'Rechtsabteilung', assignedAt: new Date(now.getTime() - 28 * 24 * 60 * 60 * 1000).toISOString() }, objectionDetails: { processingPurpose: 'Marketing', legalBasis: 'Berechtigtes Interesse (Art. 6(1)(f))', objectionGrounds: 'Keine konkreten Gruende genannt', decision: 'rejected', decisionReason: 'Zwingende schutzwuerdige Gruende fuer die Verarbeitung ueberwiegen', decisionBy: 'Rechtsabteilung', decisionAt: new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString() }, notes: 'Widerspruch unberechtigt - zwingende schutzwuerdige Gruende', createdAt: new Date(now.getTime() - 35 * 24 * 60 * 60 * 1000).toISOString(), createdBy: 'system', updatedAt: new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString(), tenantId: 'default-tenant' }, { id: 'dsr-006', referenceNumber: 'DSR-2025-000006', type: 'access', status: 'completed', priority: 'normal', requester: { name: 'Sarah Braun', email: 'sarah.braun@example.de' }, source: 'email', receivedAt: new Date(now.getTime() - 45 * 24 * 60 * 60 * 1000).toISOString(), deadline: { originalDeadline: new Date(now.getTime() - 15 * 24 * 60 * 60 * 1000).toISOString(), currentDeadline: new Date(now.getTime() - 15 * 24 * 60 * 60 * 1000).toISOString(), extended: false }, completedAt: new Date(now.getTime() - 20 * 24 * 60 * 60 * 1000).toISOString(), identityVerification: { verified: true, method: 'id_document', verifiedAt: new Date(now.getTime() - 42 * 24 * 60 * 60 * 1000).toISOString(), verifiedBy: 'DSB Mueller' }, assignment: { assignedTo: 'DSB Mueller', assignedAt: new Date(now.getTime() - 42 * 24 * 60 * 60 * 1000).toISOString() }, dataExport: { format: 'pdf', generatedAt: new Date(now.getTime() - 20 * 24 * 60 * 60 * 1000).toISOString(), generatedBy: 'DSB Mueller', fileName: 'datenauskunft_sarah_braun.pdf', fileSize: 245000, includesThirdPartyData: false }, createdAt: new Date(now.getTime() - 45 * 24 * 60 * 60 * 1000).toISOString(), createdBy: 'system', updatedAt: new Date(now.getTime() - 20 * 24 * 60 * 60 * 1000).toISOString(), tenantId: 'default-tenant' } ] } export function createMockStatistics(): DSRStatistics { return { total: 6, byStatus: { intake: 1, identity_verification: 1, processing: 2, completed: 1, rejected: 1, cancelled: 0 }, byType: { access: 2, rectification: 1, erasure: 1, restriction: 0, portability: 1, objection: 1 }, overdue: 0, dueThisWeek: 2, averageProcessingDays: 18, completedThisMonth: 1 } }