diff --git a/admin-compliance/app/sdk/dsr/[requestId]/page.tsx b/admin-compliance/app/sdk/dsr/[requestId]/page.tsx
index 7698564..9d6721d 100644
--- a/admin-compliance/app/sdk/dsr/[requestId]/page.tsx
+++ b/admin-compliance/app/sdk/dsr/[requestId]/page.tsx
@@ -13,7 +13,21 @@ import {
DSRCommunication,
DSRVerifyIdentityRequest
} from '@/lib/sdk/dsr/types'
-import { fetchSDKDSR, updateSDKDSRStatus } from '@/lib/sdk/dsr/api'
+import {
+ fetchSDKDSR,
+ updateSDKDSRStatus,
+ verifyDSRIdentity,
+ assignDSR,
+ extendDSRDeadline,
+ completeDSR,
+ rejectDSR,
+ fetchDSRCommunications,
+ sendDSRCommunication,
+ fetchDSRExceptionChecks,
+ initDSRExceptionChecks,
+ updateDSRExceptionCheck,
+ fetchDSRHistory,
+} from '@/lib/sdk/dsr/api'
import {
DSRWorkflowStepper,
DSRIdentityModal,
@@ -22,12 +36,6 @@ import {
DSRDataExportComponent
} from '@/components/sdk/dsr'
-// =============================================================================
-// MOCK COMMUNICATIONS
-// =============================================================================
-
-// TODO: Backend fehlt — Communications API noch nicht implementiert
-
// =============================================================================
// COMPONENTS
// =============================================================================
@@ -155,56 +163,38 @@ function ActionButtons({
)
}
-function AuditLog({ request }: { request: DSRRequest }) {
- type AuditEvent = { action: string; timestamp: string; user: string }
-
- const events: AuditEvent[] = [
- { action: 'Erstellt', timestamp: request.createdAt, user: request.createdBy }
- ]
-
- if (request.assignment.assignedAt) {
- events.push({
- action: `Zugewiesen an ${request.assignment.assignedTo}`,
- timestamp: request.assignment.assignedAt,
- user: request.assignment.assignedBy || 'System'
- })
- }
-
- if (request.identityVerification.verifiedAt) {
- events.push({
- action: 'Identitaet verifiziert',
- timestamp: request.identityVerification.verifiedAt,
- user: request.identityVerification.verifiedBy || 'System'
- })
- }
-
- if (request.completedAt) {
- events.push({
- action: request.status === 'rejected' ? 'Abgelehnt' : 'Abgeschlossen',
- timestamp: request.completedAt,
- user: request.updatedBy || 'System'
- })
- }
-
+function AuditLog({ history }: { history: any[] }) {
return (
Aktivitaeten
- {events.map((event, idx) => (
-
+ {history.length === 0 && (
+
Keine Eintraege
+ )}
+ {history.map((entry, idx) => (
+
-
{event.action}
+
+ {entry.previous_status
+ ? `${entry.previous_status} → ${entry.new_status}`
+ : entry.new_status
+ }
+ {entry.comment && `: ${entry.comment}`}
+
- {new Date(event.timestamp).toLocaleDateString('de-DE', {
- day: '2-digit',
- month: '2-digit',
- year: 'numeric',
- hour: '2-digit',
- minute: '2-digit'
- })}
+ {entry.created_at
+ ? new Date(entry.created_at).toLocaleDateString('de-DE', {
+ day: '2-digit',
+ month: '2-digit',
+ year: 'numeric',
+ hour: '2-digit',
+ minute: '2-digit'
+ })
+ : ''
+ }
{' - '}
- {event.user}
+ {entry.changed_by || 'System'}
@@ -225,10 +215,17 @@ export default function DSRDetailPage() {
const [request, setRequest] = useState
(null)
const [communications, setCommunications] = useState([])
+ const [history, setHistory] = useState([])
+ const [exceptionChecks, setExceptionChecks] = useState([])
const [isLoading, setIsLoading] = useState(true)
const [showIdentityModal, setShowIdentityModal] = useState(false)
const [activeContentTab, setActiveContentTab] = useState<'details' | 'communication' | 'type-specific'>('details')
+ const reloadRequest = async () => {
+ const found = await fetchSDKDSR(requestId)
+ if (found) setRequest(found)
+ }
+
// Load data from SDK backend
useEffect(() => {
const loadData = async () => {
@@ -237,8 +234,40 @@ export default function DSRDetailPage() {
const found = await fetchSDKDSR(requestId)
if (found) {
setRequest(found)
- // TODO: Backend fehlt — Communications API noch nicht implementiert
- setCommunications([])
+ // Load communications, history, and exception checks in parallel
+ const [comms, hist] = await Promise.all([
+ fetchDSRCommunications(requestId).catch(() => []),
+ fetchDSRHistory(requestId).catch(() => []),
+ ])
+ setCommunications(comms.map((c: any) => ({
+ id: c.id,
+ dsrId: c.dsr_id,
+ type: c.communication_type,
+ channel: c.channel,
+ subject: c.subject,
+ content: c.content,
+ createdAt: c.created_at,
+ createdBy: c.created_by,
+ sentAt: c.sent_at,
+ sentBy: c.sent_by,
+ })))
+ setHistory(hist)
+
+ // Load exception checks for erasure requests
+ if (found.type === 'erasure') {
+ try {
+ const checks = await fetchDSRExceptionChecks(requestId)
+ if (checks.length === 0) {
+ // Auto-initialize if none exist
+ const initialized = await initDSRExceptionChecks(requestId)
+ setExceptionChecks(initialized)
+ } else {
+ setExceptionChecks(checks)
+ }
+ } catch {
+ setExceptionChecks([])
+ }
+ }
}
} catch (error) {
console.error('Failed to load DSR:', error)
@@ -252,46 +281,109 @@ export default function DSRDetailPage() {
const handleVerifyIdentity = async (verification: DSRVerifyIdentityRequest) => {
if (!request) return
try {
- await updateSDKDSRStatus(request.id, 'verified')
- setRequest({
- ...request,
- identityVerification: {
- verified: true,
- method: verification.method,
- verifiedAt: new Date().toISOString(),
- verifiedBy: 'Current User',
- notes: verification.notes
- },
- status: request.status === 'identity_verification' ? 'processing' : request.status
+ const updated = await verifyDSRIdentity(request.id, {
+ method: verification.method,
+ notes: verification.notes,
+ document_ref: verification.documentRef,
})
+ setRequest(updated)
+ // Reload history
+ fetchDSRHistory(requestId).then(setHistory).catch(() => {})
} catch (err) {
console.error('Failed to verify identity:', err)
- // Still update locally as fallback
- setRequest({
- ...request,
- identityVerification: {
- verified: true,
- method: verification.method,
- verifiedAt: new Date().toISOString(),
- verifiedBy: 'Current User',
- notes: verification.notes
- },
- status: request.status === 'identity_verification' ? 'processing' : request.status
- })
+ }
+ }
+
+ const handleAssign = async () => {
+ if (!request) return
+ const assignee = prompt('Zuweisen an (Name/ID):')
+ if (!assignee) return
+ try {
+ const updated = await assignDSR(request.id, assignee)
+ setRequest(updated)
+ fetchDSRHistory(requestId).then(setHistory).catch(() => {})
+ } catch (err) {
+ console.error('Failed to assign:', err)
+ }
+ }
+
+ const handleExtendDeadline = async () => {
+ if (!request) return
+ const reason = prompt('Grund fuer die Fristverlaengerung:')
+ if (!reason) return
+ const daysStr = prompt('Um wie viele Tage verlaengern? (Standard: 60)', '60')
+ const days = parseInt(daysStr || '60', 10) || 60
+ try {
+ const updated = await extendDSRDeadline(request.id, reason, days)
+ setRequest(updated)
+ fetchDSRHistory(requestId).then(setHistory).catch(() => {})
+ } catch (err) {
+ console.error('Failed to extend deadline:', err)
+ }
+ }
+
+ const handleComplete = async () => {
+ if (!request) return
+ const summary = prompt('Zusammenfassung der Bearbeitung:')
+ if (summary === null) return
+ try {
+ const updated = await completeDSR(request.id, summary || undefined)
+ setRequest(updated)
+ fetchDSRHistory(requestId).then(setHistory).catch(() => {})
+ } catch (err) {
+ console.error('Failed to complete:', err)
+ }
+ }
+
+ const handleReject = async () => {
+ if (!request) return
+ const reason = prompt('Ablehnungsgrund:')
+ if (!reason) return
+ const legalBasis = prompt('Rechtsgrundlage (optional):')
+ try {
+ const updated = await rejectDSR(request.id, reason, legalBasis || undefined)
+ setRequest(updated)
+ fetchDSRHistory(requestId).then(setHistory).catch(() => {})
+ } catch (err) {
+ console.error('Failed to reject:', err)
}
}
const handleSendCommunication = async (message: any) => {
- const newComm: DSRCommunication = {
- id: `comm-${Date.now()}`,
- dsrId: requestId,
- ...message,
- createdAt: new Date().toISOString(),
- createdBy: 'Current User',
- sentAt: message.type === 'outgoing' ? new Date().toISOString() : undefined,
- sentBy: message.type === 'outgoing' ? 'Current User' : undefined
+ if (!request) return
+ try {
+ const result = await sendDSRCommunication(requestId, {
+ communication_type: message.type || 'outgoing',
+ channel: message.channel || 'email',
+ subject: message.subject,
+ content: message.content,
+ })
+ // Map backend response to frontend format
+ const newComm: DSRCommunication = {
+ id: result.id,
+ dsrId: result.dsr_id,
+ type: result.communication_type,
+ channel: result.channel,
+ subject: result.subject,
+ content: result.content,
+ createdAt: result.created_at,
+ createdBy: result.created_by || 'Current User',
+ sentAt: result.sent_at,
+ sentBy: result.sent_by,
+ }
+ setCommunications(prev => [newComm, ...prev])
+ } catch (err) {
+ console.error('Failed to send communication:', err)
+ }
+ }
+
+ const handleExceptionCheckChange = async (checkId: string, applies: boolean, notes?: string) => {
+ try {
+ const updated = await updateDSRExceptionCheck(requestId, checkId, { applies, notes })
+ setExceptionChecks(prev => prev.map(c => c.id === checkId ? updated : c))
+ } catch (err) {
+ console.error('Failed to update exception check:', err)
}
- setCommunications(prev => [newComm, ...prev])
}
if (isLoading) {
@@ -528,10 +620,48 @@ export default function DSRDetailPage() {
{/* Art. 17 - Erasure */}
{request.type === 'erasure' && (
-
setRequest({ ...request, erasureChecklist: checklist })}
- />
+
+
setRequest({ ...request, erasureChecklist: checklist })}
+ />
+
+ {/* Art. 17(3) Exception Checks from Backend */}
+ {exceptionChecks.length > 0 && (
+
+
Art. 17(3) Ausnahmepruefung
+
+ Pruefen Sie, ob eine der gesetzlichen Ausnahmen zur Loeschpflicht greift.
+
+
+ {exceptionChecks.map((check) => (
+
+
+
handleExceptionCheckChange(check.id, e.target.checked, check.notes)}
+ className="mt-1 h-4 w-4 rounded border-gray-300 text-purple-600 focus:ring-purple-500"
+ />
+
+
+ {check.article}: {check.label}
+
+
{check.description}
+ {check.checked_by && (
+
+ Geprueft von {check.checked_by} am{' '}
+ {check.checked_at ? new Date(check.checked_at).toLocaleDateString('de-DE') : '-'}
+
+ )}
+
+
+
+ ))}
+
+
+ )}
+
)}
{/* Art. 15/20 - Data Export */}
@@ -697,16 +827,16 @@ export default function DSRDetailPage() {
setShowIdentityModal(true)}
- onExtendDeadline={() => alert('Fristverlaengerung - Coming soon')}
- onComplete={() => alert('Abschliessen - Coming soon')}
- onReject={() => alert('Ablehnen - Coming soon')}
- onAssign={() => alert('Zuweisen - Coming soon')}
+ onExtendDeadline={handleExtendDeadline}
+ onComplete={handleComplete}
+ onReject={handleReject}
+ onAssign={handleAssign}
/>
{/* Audit Log Card */}
diff --git a/admin-compliance/app/sdk/dsr/page.tsx b/admin-compliance/app/sdk/dsr/page.tsx
index 7d7ee6d..7d880b2 100644
--- a/admin-compliance/app/sdk/dsr/page.tsx
+++ b/admin-compliance/app/sdk/dsr/page.tsx
@@ -808,15 +808,31 @@ export default function DSRPage() {
explanation={stepInfo.explanation}
tips={stepInfo.tips}
>
-
+
+
+
+
{/* Tab Navigation */}
diff --git a/admin-compliance/lib/sdk/dsr/api.ts b/admin-compliance/lib/sdk/dsr/api.ts
index a351311..cc58d46 100644
--- a/admin-compliance/lib/sdk/dsr/api.ts
+++ b/admin-compliance/lib/sdk/dsr/api.ts
@@ -245,6 +245,171 @@ export async function updateSDKDSRStatus(id: string, status: 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)
// =============================================================================
diff --git a/ai-compliance-sdk/cmd/server/main.go b/ai-compliance-sdk/cmd/server/main.go
index a1e55d4..14efdb3 100644
--- a/ai-compliance-sdk/cmd/server/main.go
+++ b/ai-compliance-sdk/cmd/server/main.go
@@ -273,6 +273,8 @@ func main() {
}
// DSR - Data Subject Requests / Betroffenenrechte (Art. 15-22)
+ // DEPRECATED: DSR is now managed by backend-compliance (Python).
+ // Use: GET/POST/PUT /api/compliance/dsr/* on backend-compliance:8002
dsr := dsgvoRoutes.Group("/dsr")
{
dsr.GET("", dsgvoHandlers.ListDSRs)
@@ -304,7 +306,7 @@ func main() {
{
exports.GET("/vvt", dsgvoHandlers.ExportVVT) // DEPRECATED: use backend-compliance /vvt/export?format=csv
exports.GET("/tom", dsgvoHandlers.ExportTOM) // DEPRECATED: use backend-compliance /tom/export?format=csv
- exports.GET("/dsr", dsgvoHandlers.ExportDSR)
+ exports.GET("/dsr", dsgvoHandlers.ExportDSR) // DEPRECATED: use backend-compliance /dsr/export?format=csv
exports.GET("/retention", dsgvoHandlers.ExportRetentionPolicies)
}
}
diff --git a/ai-compliance-sdk/internal/api/handlers/dsgvo_handlers.go b/ai-compliance-sdk/internal/api/handlers/dsgvo_handlers.go
index f44c069..180cba3 100644
--- a/ai-compliance-sdk/internal/api/handlers/dsgvo_handlers.go
+++ b/ai-compliance-sdk/internal/api/handlers/dsgvo_handlers.go
@@ -220,9 +220,12 @@ func (h *DSGVOHandlers) CreateTOM(c *gin.Context) {
// ============================================================================
// DSR - Data Subject Requests
+// DEPRECATED: DSR is now managed by backend-compliance (Python/FastAPI).
+// Use: /api/compliance/dsr/* endpoints on backend-compliance:8002
// ============================================================================
// ListDSRs returns all DSRs for a tenant
+// DEPRECATED: Use backend-compliance GET /api/compliance/dsr
func (h *DSGVOHandlers) ListDSRs(c *gin.Context) {
tenantID := rbac.GetTenantID(c)
if tenantID == uuid.Nil {
@@ -243,6 +246,7 @@ func (h *DSGVOHandlers) ListDSRs(c *gin.Context) {
}
// GetDSR returns a DSR by ID
+// DEPRECATED: Use backend-compliance GET /api/compliance/dsr/{id}
func (h *DSGVOHandlers) GetDSR(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
if err != nil {
@@ -264,6 +268,7 @@ func (h *DSGVOHandlers) GetDSR(c *gin.Context) {
}
// CreateDSR creates a new DSR
+// DEPRECATED: Use backend-compliance POST /api/compliance/dsr
func (h *DSGVOHandlers) CreateDSR(c *gin.Context) {
tenantID := rbac.GetTenantID(c)
userID := rbac.GetUserID(c)
@@ -293,6 +298,7 @@ func (h *DSGVOHandlers) CreateDSR(c *gin.Context) {
}
// UpdateDSR updates a DSR
+// DEPRECATED: Use backend-compliance PUT /api/compliance/dsr/{id}
func (h *DSGVOHandlers) UpdateDSR(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
if err != nil {
@@ -612,6 +618,7 @@ func (h *DSGVOHandlers) ExportTOM(c *gin.Context) {
}
// ExportDSR exports DSR overview as CSV/JSON
+// DEPRECATED: Use backend-compliance GET /api/compliance/dsr/export?format=csv|json
func (h *DSGVOHandlers) ExportDSR(c *gin.Context) {
tenantID := rbac.GetTenantID(c)
if tenantID == uuid.Nil {
diff --git a/backend-compliance/compliance/api/dsr_routes.py b/backend-compliance/compliance/api/dsr_routes.py
index 8b05d13..0a4db34 100644
--- a/backend-compliance/compliance/api/dsr_routes.py
+++ b/backend-compliance/compliance/api/dsr_routes.py
@@ -4,11 +4,14 @@ DSR (Data Subject Request) Routes — Betroffenenanfragen nach DSGVO Art. 15-21.
Native Python/FastAPI Implementierung, ersetzt Go consent-service Proxy.
"""
+import io
+import csv
import uuid
from datetime import datetime, timedelta
from typing import Optional, List, Dict, Any
from fastapi import APIRouter, Depends, HTTPException, Query, Header
+from fastapi.responses import StreamingResponse
from pydantic import BaseModel
from sqlalchemy.orm import Session
from sqlalchemy import text, func, and_, or_, cast, String
@@ -438,6 +441,61 @@ async def get_dsr_stats(
}
+# =============================================================================
+# Export
+# =============================================================================
+
+@router.get("/export")
+async def export_dsrs(
+ format: str = Query("csv", pattern="^(csv|json)$"),
+ tenant_id: str = Depends(_get_tenant),
+ db: Session = Depends(get_db),
+):
+ """Exportiert alle DSRs als CSV oder JSON."""
+ tid = uuid.UUID(tenant_id)
+ dsrs = db.query(DSRRequestDB).filter(
+ DSRRequestDB.tenant_id == tid,
+ ).order_by(DSRRequestDB.created_at.desc()).all()
+
+ if format == "json":
+ return {
+ "exported_at": datetime.utcnow().isoformat(),
+ "total": len(dsrs),
+ "requests": [_dsr_to_dict(d) for d in dsrs],
+ }
+
+ # CSV export (semicolon-separated, matching Go format + extended fields)
+ output = io.StringIO()
+ writer = csv.writer(output, delimiter=';', quoting=csv.QUOTE_MINIMAL)
+ writer.writerow([
+ "ID", "Referenznummer", "Typ", "Name", "E-Mail", "Status",
+ "Prioritaet", "Eingegangen", "Frist", "Abgeschlossen", "Quelle", "Zugewiesen",
+ ])
+
+ for dsr in dsrs:
+ writer.writerow([
+ str(dsr.id),
+ dsr.request_number or "",
+ dsr.request_type or "",
+ dsr.requester_name or "",
+ dsr.requester_email or "",
+ dsr.status or "",
+ dsr.priority or "",
+ dsr.received_at.strftime("%Y-%m-%d") if dsr.received_at else "",
+ dsr.deadline_at.strftime("%Y-%m-%d") if dsr.deadline_at else "",
+ dsr.completed_at.strftime("%Y-%m-%d") if dsr.completed_at else "",
+ dsr.source or "",
+ dsr.assigned_to or "",
+ ])
+
+ output.seek(0)
+ return StreamingResponse(
+ output,
+ media_type="text/csv; charset=utf-8",
+ headers={"Content-Disposition": "attachment; filename=dsr_export.csv"},
+ )
+
+
# =============================================================================
# Deadline Processing (MUST be before /{dsr_id} to avoid path conflicts)
# =============================================================================
diff --git a/backend-compliance/tests/test_dsr_routes.py b/backend-compliance/tests/test_dsr_routes.py
index 11540fc..ff91e9d 100644
--- a/backend-compliance/tests/test_dsr_routes.py
+++ b/backend-compliance/tests/test_dsr_routes.py
@@ -697,3 +697,49 @@ class TestDSRTemplates:
fake_id = str(uuid.uuid4())
resp = client.get(f"/api/compliance/dsr/templates/{fake_id}/versions", headers=HEADERS)
assert resp.status_code == 404
+
+
+class TestDSRExport:
+ """Tests for DSR export endpoint."""
+
+ def test_export_csv_empty(self):
+ resp = client.get("/api/compliance/dsr/export?format=csv", headers=HEADERS)
+ assert resp.status_code == 200
+ assert "text/csv" in resp.headers.get("content-type", "")
+ lines = resp.text.strip().split("\n")
+ assert len(lines) == 1 # Header only
+ assert "Referenznummer" in lines[0]
+ assert "Zugewiesen" in lines[0]
+
+ def test_export_csv_with_data(self):
+ # Create a DSR first
+ body = {
+ "request_type": "access",
+ "requester_name": "Export Test",
+ "requester_email": "export@example.de",
+ "source": "email",
+ }
+ create_resp = client.post("/api/compliance/dsr", json=body, headers=HEADERS)
+ assert create_resp.status_code == 200
+
+ resp = client.get("/api/compliance/dsr/export?format=csv", headers=HEADERS)
+ assert resp.status_code == 200
+ lines = resp.text.strip().split("\n")
+ assert len(lines) >= 2 # Header + at least 1 data row
+ # Check data row contains our test data
+ assert "Export Test" in lines[1]
+ assert "export@example.de" in lines[1]
+ assert "access" in lines[1]
+
+ def test_export_json(self):
+ resp = client.get("/api/compliance/dsr/export?format=json", headers=HEADERS)
+ assert resp.status_code == 200
+ data = resp.json()
+ assert "exported_at" in data
+ assert "total" in data
+ assert "requests" in data
+ assert isinstance(data["requests"], list)
+
+ def test_export_invalid_format(self):
+ resp = client.get("/api/compliance/dsr/export?format=xml", headers=HEADERS)
+ assert resp.status_code == 422 # Validation error