From da799ec3ef052ab4a82a0384c0b1b71ba47131fd Mon Sep 17 00:00:00 2001 From: Ediminator <211899731+Ediminator@users.noreply.github.com> Date: Wed, 28 Oct 2026 10:04:50 +0100 Subject: [PATCH 1/2] feat(proposals): add DSAR export, erasure, and commercial hold providers (TASK-0059) Signed-off-by: Ediminator <211899731+Ediminator@users.noreply.github.com> --- docs/privacy/data-inventory.md | 32 +- messages/de.json | 3 + messages/en.json | 3 + src/components/clients/erasure-dialog.tsx | 7 +- src/server/clients/activity.ts | 2 + src/server/privacy/erasure/allowlist.ts | 6 +- src/server/privacy/erasure/index.ts | 2 +- src/server/privacy/erasure/registry.ts | 3 + src/server/privacy/erasure/service.ts | 37 +- src/server/privacy/export/categories.ts | 33 ++ src/server/privacy/export/html.ts | 19 + src/server/privacy/export/registry.ts | 2 + .../proposals/privacy/activity-provider.ts | 64 ++ .../proposals/privacy/erasure-provider.ts | 61 ++ .../proposals/privacy/export-provider.ts | 304 ++++++++++ src/server/proposals/privacy/hold-provider.ts | 71 +++ src/server/proposals/privacy/index.ts | 23 + src/server/retention/policies.ts | 1 + src/server/retention/policy.ts | 50 ++ src/server/retention/runner.ts | 9 + src/server/worker/retention-runner.ts | 15 + tests/integration/proposals-erasure.test.ts | 446 ++++++++++++++ tests/unit/erasure-coverage.test.ts | 12 + .../unit/proposals-activity-provider.test.ts | 62 ++ tests/unit/proposals-erasure-provider.test.ts | 111 ++++ tests/unit/proposals-export-provider.test.ts | 556 ++++++++++++++++++ tests/unit/proposals-hold-provider.test.ts | 160 +++++ 27 files changed, 2055 insertions(+), 39 deletions(-) create mode 100644 src/server/proposals/privacy/activity-provider.ts create mode 100644 src/server/proposals/privacy/erasure-provider.ts create mode 100644 src/server/proposals/privacy/export-provider.ts create mode 100644 src/server/proposals/privacy/hold-provider.ts create mode 100644 src/server/proposals/privacy/index.ts create mode 100644 src/server/retention/policies.ts create mode 100644 src/server/worker/retention-runner.ts create mode 100644 tests/integration/proposals-erasure.test.ts create mode 100644 tests/unit/proposals-activity-provider.test.ts create mode 100644 tests/unit/proposals-erasure-provider.test.ts create mode 100644 tests/unit/proposals-export-provider.test.ts create mode 100644 tests/unit/proposals-hold-provider.test.ts diff --git a/docs/privacy/data-inventory.md b/docs/privacy/data-inventory.md index f606360..828331b 100644 --- a/docs/privacy/data-inventory.md +++ b/docs/privacy/data-inventory.md @@ -164,19 +164,19 @@ Appointments track client and third-party meetings scheduled by the studio owner Proposals track pre-contractual commercial quotations, package/add-on option selections, and formal client contract acceptances or declines. -| Data Field / Entity | Purpose | Lawful Basis (GDPR) | Retention Period | Deletion / Export | -| ------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `proposals.id` | Monotonic UUIDv7 entity identifier | Art. 6(1)(b) Contract / pre-contractual steps | Retained with proposal record | Hard-deleted on project deletion or client erasure (unaccepted proposals) | -| `proposals.project_id` | FK to `projects.id` associating quotation with specific photography engagement | Art. 6(1)(b) Contract / pre-contractual steps | Lifetime of project profile | Cascade deleted on project deletion; unaccepted proposals deleted on client erasure (Art. 17) | -| `proposals.client_id` | FK to `clients.id` linking commercial agreement to client profile | Art. 6(1)(b) Contract / pre-contractual steps | Lifetime of client profile; accepted proposals held under §147 AO / §257 HGB | Unaccepted proposals erased on client erasure (Art. 17); accepted proposals held under statutory commercial hold (TASK-0059) | -| `proposals.title` | Descriptive title of the proposal | Art. 6(1)(b) Pre-contractual steps | Retained with proposal record | Included in DSAR export; erased on client erasure (unaccepted) | -| `proposals.status` | State machine status (`draft`, `sent`, `accepted`, `declined`, `expired`, `superseded`) | Art. 6(1)(b) Pre-contractual steps | Dictates statutory hold: `accepted` triggers 8-year commercial retention (§147 AO / §257 HGB); unaccepted proposals follow client retention | Included in DSAR export; erased on client erasure if not accepted | -| `proposals.token_hash` | SHA-256 digest of secret token for secure customer portal access | Art. 6(1)(b) Pre-contractual steps / Art. 32 Security of processing | Retained while proposal is active / valid | Invalidated on revocation (`token_revoked_at`); erased with proposal record | -| `proposals.accepted_by_name` | Full name of client or authorized representative who accepted proposal | Art. 6(1)(b) Contract / Art. 6(1)(c) Legal obligation (§147 AO) | 8 years from close of calendar year in which proposal was accepted (§147(3) AO, §257(4) HGB) | Included in client DSAR export (Art. 15); exempt from Art. 17 erasure during mandatory statutory commercial retention period (§147 AO / §257 HGB; TASK-0059); never logged in audit metadata | -| `proposals.accepted_by_email` | Email address of person who accepted proposal (used for confirmation) | Art. 6(1)(b) Contract / Art. 6(1)(c) Legal obligation (§147 AO) | 8 years from close of calendar year in which proposal was accepted (§147(3) AO, §257(4) HGB) | Included in client DSAR export (Art. 15); exempt from Art. 17 erasure during mandatory statutory commercial retention period (§147 AO / §257 HGB; TASK-0059); never logged in audit metadata | -| `proposals.acceptance_ip_anonymized` | Truncated IP address (IPv4 `/24`, IPv6 `/48`) proving acceptance without persistent single-device identification | Art. 6(1)(f) Legitimate interest (fraud prevention / proof) | 8 years matching accepted contract record | Included in client DSAR export (Art. 15); exempt from Art. 17 erasure during statutory hold; never logged in audit metadata | -| `proposals.acceptance_user_agent` | Browser / device user-agent string at time of acceptance | Art. 6(1)(f) Legitimate interest (evidentiary integrity) | 8 years matching accepted contract record | Included in client DSAR export (Art. 15); exempt from Art. 17 erasure during statutory hold; never logged in audit metadata | -| `proposals.accepted_options_snapshot` | Immutable JSON snapshot of accepted line items, quantities, and financial totals | Art. 6(1)(b) Contract / Art. 6(1)(c) Statutory commercial record | 8 years matching commercial agreement (§147 AO / §257 HGB) | Included in client DSAR export; permanently locked against modification; held under statutory retention (TASK-0059) | -| `proposals.decline_reason` | Optional free-text reason provided by client when declining proposal | Art. 6(1)(b) Pre-contractual steps | Retained until proposal purge or client erasure | Included in client DSAR export; deleted on lead/client erasure; never logged in audit metadata (only boolean `has_decline_reason` logged in audit metadata) | -| `proposals.internal_notes` | Photographer internal pricing and negotiation notes | Art. 6(1)(f) Legitimate interest (studio business management) | Retained with proposal record | Excluded from client portal and public token view (`internalNotes: null`); included in owner audit export; deleted on client erasure if not subject to statutory hold | -| `proposal_line_items.*` | Quotation line items (package/catalogue snapshots, unit prices, tax rates, quantities) | Art. 6(1)(b) Pre-contractual steps / Contract | Follows parent proposal lifecycle | Included in DSAR export; cascade deleted on parent proposal deletion | +| Data Field / Entity | Purpose | Lawful Basis (GDPR) | Retention Period | Deletion / Export | +| ------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `proposals.id` | Monotonic UUIDv7 entity identifier | Art. 6(1)(b) Contract / pre-contractual steps | Retained with proposal record | Hard-deleted on project deletion or client erasure (unaccepted proposals) | +| `proposals.project_id` | FK to `projects.id` associating quotation with specific photography engagement | Art. 6(1)(b) Contract / pre-contractual steps | Lifetime of project profile | Cascade deleted on project deletion; unaccepted proposals deleted on client erasure (Art. 17) | +| `proposals.client_id` | FK to `clients.id` linking commercial agreement to client profile | Art. 6(1)(b) Contract / pre-contractual steps | Lifetime of client profile; accepted proposals held under §147 AO / §257 HGB | Unaccepted proposals erased on client erasure (Art. 17); accepted proposals held under statutory commercial hold (TASK-0059) | +| `proposals.title` | Descriptive title of the proposal | Art. 6(1)(b) Pre-contractual steps | Retained with proposal record | Included in DSAR export; erased on client erasure (unaccepted) | +| `proposals.status` | State machine status (`draft`, `sent`, `accepted`, `declined`, `expired`, `superseded`) | Art. 6(1)(b) Pre-contractual steps / §147 AO | Dictates statutory hold: `accepted` triggers 10-year commercial retention (§147 AO / §257 HGB); unaccepted proposals follow client retention | Included in DSAR export; erased on client erasure if not accepted | +| `proposals.token_hash` | SHA-256 digest of secret token for secure customer portal access | Art. 6(1)(b) Pre-contractual steps / Art. 32 Security of processing | Retained while proposal is active / valid | Invalidated on revocation (`token_revoked_at`); erased with proposal record | +| `proposals.accepted_by_name` | Full name of client or authorized representative who accepted proposal | Art. 6(1)(b) / §147 AO (Contract / statutory legal obligation) | 10-year statutory retention from close of calendar year in which proposal was accepted (§147(3) AO, §257(4) HGB) | Included in client DSAR export (Art. 15); exempt from Art. 17 erasure during mandatory statutory commercial retention period (§147 AO / §257 HGB; TASK-0059); never logged in audit metadata | +| `proposals.accepted_by_email` | Email address of person who accepted proposal (used for confirmation) | Art. 6(1)(b) / §147 AO (Contract / statutory legal obligation) | 10-year statutory retention from close of calendar year in which proposal was accepted (§147(3) AO, §257(4) HGB) | Included in client DSAR export (Art. 15); exempt from Art. 17 erasure during mandatory statutory commercial retention period (§147 AO / §257 HGB; TASK-0059); never logged in audit metadata | +| `proposals.acceptance_ip_anonymized` | Truncated IP address (IPv4 `/24`, IPv6 `/48`) proving acceptance without persistent single-device identification | Art. 6(1)(b) / §147 AO / Art. 6(1)(f) Legitimate interest | 10-year statutory retention matching accepted contract record (§147 AO / §257 HGB) | Included in client DSAR export (Art. 15); exempt from Art. 17 erasure during statutory hold; never logged in audit metadata | +| `proposals.acceptance_user_agent` | Browser / device user-agent string at time of acceptance | Art. 6(1)(b) / §147 AO / Art. 6(1)(f) Legitimate interest | 10-year statutory retention matching accepted contract record (§147 AO / §257 HGB) | Included in client DSAR export (Art. 15); exempt from Art. 17 erasure during statutory hold; never logged in audit metadata | +| `proposals.accepted_options_snapshot` | Immutable JSON snapshot of accepted line items, quantities, and financial totals | Art. 6(1)(b) / §147 AO (Statutory commercial agreement record) | 10-year statutory retention matching commercial agreement (§147 AO / §257 HGB) | Included in client DSAR export; permanently locked against modification; held under statutory retention (TASK-0059) | +| `proposals.decline_reason` | Optional free-text reason provided by client when declining proposal | Art. 6(1)(b) Pre-contractual steps | Retained until proposal purge or client erasure | Included in client DSAR export; deleted on lead/client erasure; never logged in audit metadata (only boolean `has_decline_reason` logged in audit metadata) | +| `proposals.internal_notes` | Photographer internal pricing and negotiation notes | Art. 6(1)(f) Legitimate interest (studio business management) | Retained with proposal record | Excluded from client portal and public token view (`internalNotes: null`); included in owner audit export; deleted on client erasure if not subject to statutory hold | +| `proposal_line_items.*` | Quotation line items (package/catalogue snapshots, unit prices, tax rates, quantities) | Art. 6(1)(b) Pre-contractual steps / Contract | Follows parent proposal lifecycle | Included in DSAR export; cascade deleted on parent proposal deletion | diff --git a/messages/de.json b/messages/de.json index d153738..e59e3d8 100644 --- a/messages/de.json +++ b/messages/de.json @@ -1381,6 +1381,9 @@ "holds": { "active_project": "Laufendes Projekt / Auftrag", "upcoming_shoot": "Bevorstehendes geplantes Shooting", + "statutory": { + "commercial_document": "Gesetzliche Aufbewahrungspflicht für Handelsdokumente (§ 147 AO, § 257 HGB)" + }, "manual": { "dispute": "Rechtsstreitigkeit", "statutory": "Gesetzliche Anforderung", diff --git a/messages/en.json b/messages/en.json index a960fdf..fb58902 100644 --- a/messages/en.json +++ b/messages/en.json @@ -1381,6 +1381,9 @@ "holds": { "active_project": "Active commission / project", "upcoming_shoot": "Upcoming scheduled shoot", + "statutory": { + "commercial_document": "Statutory commercial agreement (§147 AO, §257 HGB)" + }, "manual": { "dispute": "Dispute resolution", "statutory": "Statutory requirement", diff --git a/src/components/clients/erasure-dialog.tsx b/src/components/clients/erasure-dialog.tsx index 7c558fd..8dfd400 100644 --- a/src/components/clients/erasure-dialog.tsx +++ b/src/components/clients/erasure-dialog.tsx @@ -263,7 +263,12 @@ export function ErasureDialogInner({