Files
breakpilot-compliance/admin-compliance/lib/sdk/dsr/api.ts
Benjamin Admin b7c1a5da1a
All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Successful in 36s
CI / test-python-backend-compliance (push) Successful in 31s
CI / test-python-document-crawler (push) Successful in 23s
CI / test-python-dsms-gateway (push) Successful in 18s
feat: Consent-Service Module nach Compliance migriert (DSR, E-Mail-Templates, Legal Docs, Banner)
5-Phasen-Migration: Go consent-service Proxies durch native Python/FastAPI ersetzt.

Phase 1 — DSR (Betroffenenrechte): 6 Tabellen, 30 Endpoints, Frontend-API umgestellt
Phase 2 — E-Mail-Templates: 5 Tabellen, 20 Endpoints, neues Frontend, SDK_STEPS erweitert
Phase 3 — Legal Documents Extension: User Consents, Audit Log, Cookie-Kategorien
Phase 4 — Banner Consent: Device-Consents, Site-Configs, Kategorien, Vendors
Phase 5 — Cleanup: DSR-Proxy aus main.py entfernt, Frontend-URLs aktualisiert

148 neue Tests (50 + 47 + 26 + 25), alle bestanden.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 00:36:24 +01:00

505 lines
17 KiB
TypeScript

/**
* 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<void> {
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<DSRRequest | null> {
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<void> {
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}`)
}
}
// =============================================================================
// 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
}
}