Files
Cibello-app/docs/29-gdpr-radering-audit.md
Sven (AAMOS AI) c32a7e33c7
CI / Typecheck, test & build (push) Failing after 2s
ci: trigga på master + formatfix inför Gitea Actions
2026-08-13 17:25:14 +07:00

402 lines
40 KiB
Markdown

# GDPR-radering — kompletthetsaudit (cibello, HEAD `c2cc387` → `28908d5`)
> **Syfte:** identifiera varje tabell som bär `user_id` eller annan persondata, klassa den enligt GDPR, hitta luckor mot nuvarande `DELETE /v1/me`, och designa ett permanent residual-test.
>
> **Status:** ✅ Implementerad i efterföljande commits. Residual-testet `apps/api/test/me.residual.test.ts` ingår permanent i `pnpm test` och failar automatiskt om nya `user_id`-liknande kolumner läggs till utan klassning.
>
> **Audit gjord mot:**
>
> - `packages/database/src/schema/*.ts`
> - `infrastructure/migrations/*.sql`
> - `apps/api/src/routes/me.ts` (nuvarande `DELETE /v1/me`)
> - `packages/database/src/analytics-gdpr.ts`
---
## 1. Metod
1. Enumrerade alla tabeller exporterade från `packages/database/src/schema/index.ts`.
2. Grep:ade `user_id`, `references(() => users.id)` och fria text/PGD-fält som kan bära PII (foton, hälsa, kvitton, betyg, kommentarer).
3. Klassade varje tabell i tre kategorier:
- **RADERA** — direkt personlig, ingen laglig grund att behålla.
- **ANONYMISERA** — behövs för referensintegritet/aggregat, men `user_id` och PII-fält ska skrubbas eller sättas `NULL`.
- **BEHÅLL MED LAGLIG GRUND** — t.ex. bokföring, säkerhet, rättslig förpliktelse. Får inte raderas, men PII ska minimeras.
4. Jämförde klassningen mot vad `DELETE /v1/me` faktiskt gör idag.
---
## 2. Tabell-för-tabell-klassning
### 2.1 Användarkonto & autentisering — RADERA
| Tabell | Motivering | Nuvarande `DELETE /v1/me` |
| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------- |
| `users` | Personliga grunduppgifter (e-post, namn). Soft-delete/anonymisering accepteras för att bevara referensintegritet, men övriga personfält måste skrubbas. | ✅ anonymiserar `email`, `displayName`, sätter `deletedAt`. |
| `user_credentials` | Lösenordshash. Ingen laglig grund efter radering. | ❌ **saknas** — cascade på `users.id` fyrar inte vid soft-delete. |
| `admin_totp` | TOTP-secret. Säkerhetsuppgift. | ❌ **saknas** — cascade fyrar inte. |
| `email_verification_tokens` | Engångstokens kopplade till användaren. | ❌ **saknas** — cascade fyrar inte. |
| `password_reset_tokens` | Engångstokens kopplade till användaren. | ❌ **saknas** — cascade fyrar inte. |
| `refresh_tokens` | Sessions-token-hash, IP, user-agent. | ✅ raderas explicit. |
**Bedömning:** Fyra auth-tabeller är ohanterade eftersom `users` soft-deletas. Måste explicit raderas.
---
### 2.2 Hälsa, preferenser, samtycken & locale — RADERA
| Tabell | Motivering | Nuvarande `DELETE /v1/me` |
| ------------------------- | ----------------------------------------------------------------------- | ----------------------------------------------------------------- |
| `user_health_profiles` | Hälsodata (längd, vikt, födelseår, kön, aktivitet). Känslig persondata. | ✅ raderas explicit. |
| `user_preferences` | Kost/mål/allergier/intoleranser. Personlig profilering. | ✅ raderas explicit. |
| `user_consents` | Samtyckeshistorik kopplad till person. | ❌ **saknas** — cascade på `users.id` fyrar inte vid soft-delete. |
| `user_locale_preferences` | Språk, region, tidszon, valuta. Personlig preferens. | ❌ **saknas** — cascade fyrar inte. |
**Bedömning:** `user_consents` och `user_locale_preferences` är nya luckor.
---
### 2.3 Hushåll & medlemskap — BESLUT KRÄVS
| Tabell | Motivering | Nuvarande `DELETE /v1/me` |
| ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------- |
| `household_members` | Länkar användare till hushåll. Ingen laglig grund att behålla kopplingen efter radering. | ❌ **saknas helt**. |
| `households` | Om användaren är **enda medlemmen/ägaren** innehåller hushållet personliga spår (namn, budget, aktiveringsdatum, storlek). Om flera medlemmar finns är hushållet en delad resurs. | ❌ **saknas helt**. |
| `storage_locations` | Hushålls-specifika förvaringsplatser. Delad resurs om hushållet har flera medlemmar. | ❌ **saknas helt**. |
**Bedömning:** Detta är den största strukturella luckan. Förslag (se öppna frågor nedan):
- Ta bort användarens `household_members`-rad(er).
- Om användaren är ensam ägare/medlem i ett hushåll: **radera hela hushållet och dess cascading data** (`storage_locations`, `inventory_items`, `inventory_transactions`, `shopping_lists`, `week_plans`, `meal_boxes`, `receipts`, `cooking_sessions`, etc.).
- Om andra medlemmar finns: låt hushållet leva, anonymisera användarreferenser i delade data.
**Öppen fråga till Johan:** Vill vi automatiskt radera ett enpersonshushåll vid kontoradering, eller ska användaren först tvingas överföra ägarskap? (GDPR-mässigt är automatisk radering av ett enpersonshushåll det säkraste.)
---
### 2.4 Lager, inköp, matplanering — hushållsdata (ANONYMISERA eller RADERA beroende på hushåll)
| Tabell | Klassning | Motivering |
| ------------------------------ | -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `inventory_items` | ANONYMISERA / RADERA | Hushållsdata. Ingen personlig info i sig, men `source`, `verifiedByUser` etc. kan kopplas till användaren. Om hushållet överlever: sätt `verifiedByUser = false` där aktuell användare verifierat. Om hushållet raderas: cascade bort. |
| `inventory_transactions` | ANONYMISERA | `actor_user_id` är en personlig referens. Sätt `NULL`. Data behövs för hushållets saldo/historik. |
| `inventory_item_decay_profile` | BEHÅLL / RADERA | Teknisk koppling till `inventory_items`. Följer hushållets öde. |
| `inventory_conflicts` | ANONYMISERA | `resolved_by_user_id` ska vara `NULL`. Övriga fält är hushållsdata. |
| `shopping_lists` | BEHÅLL | Hushållsdata, ingen PII. |
| `shopping_list_items` | ANONYMISERA | `added_by_user_id` ska vara `NULL`. |
| `receipts` | RADERA / ANONYMISERA | Kvittobild (`image_url`) kan innehålla personuppgifter (butik, kortnummer, artiklar). Om hushållet överlever och kvittot är delat: radera bild-URL:en i objektlagring och anonymisera. Om hushållet raderas: cascade bort. |
| `receipt_lines` | RADERA / ANONYMISERA | `raw_text` är OCR-text från kvitto (potentiellt personligt). Samma logik som `receipts`. |
| `price_observations` | BEHÅLL | Aggregatdata, anonymiserad prisinformation. `store_name` är inte personligt i GDPR-mening. |
| `meal_boxes` | ANONYMISERA | `reserved_for_user_id` ska vara `NULL`. |
| `meals` | RADERA | Direkt personlig måltidslogg (`user_id`, `photo_url`). Raderas explicit idag. |
| `week_plans` | BEHÅLL | Hushållsdata, ingen PII. |
| `week_plan_entries` | BEHÅLL | Hushållsdata. |
| `cooking_sessions` | ANONYMISERA | `started_by_user_id` ska vara `NULL`. Sessionen är hushållshistorik. |
| `cooking_assumption_profiles` | BEHÅLL / RADERA | Hushållsaggregat. Följer hushållets öde. |
| `waste_summaries` | BEHÅLL | Hushållsaggregat, ingen PII. |
**Bedömning:** Dessa tabeller är i princip ohanterade idag. De flesta är hushållsdata och ska anonymiseras snarare än raderas, förutsatt att hushållet överlever.
---
### 2.5 Skanningar, AI-korrigeringar & träningsbank — RADERA
| Tabell | Motivering | Nuvarande `DELETE /v1/me` |
| ------------------ | ---------------------------------------------------------------------------- | ---------------------------------------- |
| `scan_jobs` | Innehåller uppladdade bilder (`s3_keys`) och användarspecifika resultat. | ✅ raderas explicit. |
| `ai_corrections` | Innehåller personliga korrigeringar och bildnyckel. | ✅ raderas explicit. |
| `ai_training_bank` | Innehåller träningsbild och korrigeringsdata. Cascadar via `ai_corrections`. | ✅ raderas via `ai_corrections` cascade. |
**Bedömning:** Godkänd i föregående fix. Ingen ytterligare åtgärd.
---
### 2.6 Recept & matminnen — RADERA eller ANONYMISERA
| Tabell | Klassning | Motivering |
| ------------------ | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `recipes` | ANONYMISERA | Publik/verifierad receptdata är inte personlig, men `creator_user_id` och `creator_display_name` är personliga. **Draft/private**-recept däremot är personliga och bör raderas. |
| `recipe_ratings` | RADERA | Användarbetyg och kommentar är personligt. | ✅ raderas ej idag (lucka). |
| `recipe_favorites` | RADERA | Användarens favoriter är personligt. | ❌ **saknas** (lucka). |
| `recipe_cooks` | RADERA / ANONYMISERA | Logg över vilka recept användaren lagat. Personligt. | ❌ **saknas** (lucka). |
| `food_memories` | RADERA | Matminne med eventuell bild (`photo_url`). Personligt. | ❌ **saknas** (lucka). |
| `memory_items` | RADERA | AAMOS-minne — personligt. | ✅ raderas explicit. |
| `taste_signals` | RADERA | Smakpreferenser kopplade till användaren. | ✅ raderas explicit. |
| `creator_stats` | RADERA | Creator-profil, följare, statistik. Personligt. | ❌ **saknas** (lucka). |
| `creator_follows` | RADERA | Följer/följare-relationer. Personliga. | ❌ **saknas** (lucka). |
**Bedömning:** Receptinteraktioner är i stort sett ohanterade.
---
### 2.7 Notiser & push — RADERA
| Tabell | Motivering | Nuvarande `DELETE /v1/me` |
| --------------- | ------------------------------------------------------------------- | ----------------------------------- |
| `notifications` | Push-/inapp-notiser till användaren. | ❌ **saknas** (cascade fyrar inte). |
| `push_tokens` | Push-token för Expo. Teknisk identifierare kopplad till användaren. | ❌ **saknas** (cascade fyrar inte). |
**Bedömning:** Båda är luckor.
---
### 2.8 Betalningar & prenumerationer — BEHÅLL MED LAGLIG GRUND, MEN ANONYMISERA
| Tabell | Klassning | Motivering |
| --------------------- | --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `subscriptions` | BEHÅLL | Bokförings- och rättslig grund för att bevisa köp/åtkomst. `user_id` bör pseudonymiseras eller sättas `NULL` efter en viss period, men transaktionens existens måste kunna verifieras. Retention: minst 7 år (Bokföringslagen) eller tills rättslig tvist är avslutad. |
| `subscription_events` | BEHÅLL | Händelselogg för prenumeration. Samma grund. `user_id` kan anonymiseras. |
| `store_transactions` | BEHÅLL | Råa store-transaktioner för revision. `raw_payload` kan innehålla PII (store-kvitto) — **ska skrubbas** så att endast transaktions-ID, produkt-ID och belopp återstår. `user_id` anonymiseras. Retention: 7 år. |
| `store_notifications` | BEHÅLL | App/Play-servernotiser. Ingen direkt PII i sig, men kan innehålla transaktions-ID. Behålls för spårbarhet. |
| `trials` | RADERA / BEHÅLL | Trial-spårning. För rättvis kvot/fair-use kan vi behöva spara att denna person redan provat. Men efter kontoradering bör raden raderas eller pseudonymiseras (t.ex. hash av ursprungligt user_id för anti-fraud). **Förslag:** radera vid GDPR-radering; anti-fraud hanteras via store-transaktioner istället. | ❌ **saknas** idag. |
| `ai_usage_counters` | RADERA | Månatlig AI-användning. Ingen laglig grund efter kontoradering. | ❌ **saknas** idag. |
**Bedömning:** Finansiella poster får inte raderas, men PII måste skalas bort. `subscriptions`, `subscription_events`, `store_transactions` behöver en anonymiseringsfunktion, inte deletion.
---
### 2.9 Analytics & audit — BEHÅLL MED LAGLIG GRUND, MEN ANONYMISERA
| Tabell | Klassning | Motivering |
| --------------------------- | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `product_analytics_events` | RADERA | Pseudonyma events, men `anonymous_id` kan länka till användaren. Hard-delete görs redan via `deleteUserAnalyticsEvents`. | ✅ raderas explicit. |
| `analytics_daily_snapshots` | BEHÅLL | Hushållsaggregat, ingen PII. |
| `audit_logs` | BEHÅLL | Säkerhets-/rättslig grund. `actor_user_id`, `ip` och ev. PII i `metadata` ska anonymiseras (`actor_user_id``NULL`, `ip``NULL`, metadata skrubbas). Retention: minst 2 år, alternativt tills rättslig tvist är avslutad. | ❌ **saknas** idag. |
| `domain_events` | BEHÅLL / ANONYMISERA | Event-sourcing outbox. `user_id` och PII i `payload` ska anonymiseras för att inte bryta downstream-konsumenter. | ❌ **saknas** idag. |
| `idempotency_keys` | RADERA | Caching av API-svar kopplade till användaren. Ingen laglig grund. | ❌ **saknas** idag. |
---
### 2.10 Globala system- och översättningstabeller — BEHÅLL (ingen PII)
| Tabell | Klassning | Motivering |
| ---------------------------- | --------- | --------------------------------------- |
| `canonical_ingredients` | BEHÅLL | Global produktdata. |
| `ingredient_translations` | BEHÅLL | Översättningar av globala ingredienser. |
| `unit_translations` | BEHÅLL | Översättningar av enheter. |
| `recipe_translations` | BEHÅLL | Översättningar av recept. |
| `recipe_step_translations` | BEHÅLL | Översättningar av receptsteg. |
| `recipe_source_registry` | BEHÅLL | Juridisk källregister. |
| `recipe_ingredients` | BEHÅLL | Receptdata. |
| `recipe_steps` | BEHÅLL | Receptdata. |
| `recipe_similarities` | BEHÅLL | Systemdata. |
| `substitutions` | BEHÅLL | Systemdata. |
| `nutrition_display_profiles` | BEHÅLL | Systemdata. |
| `allergen_market_rules` | BEHÅLL | Systemdata. |
| `season_events` | BEHÅLL | Systemdata. |
| `feature_flags` | BEHÅLL | Systemdata. |
| `release_gates` | BEHÅLL | Systemdata. |
| `inventory_decay_profiles` | BEHÄLL | Systemdata. |
| `ai_eval_cases` | BEHÅLL | Testdata. |
| `ai_eval_runs` | BEHÅLL | Testkörningsresultat. |
---
## 3. Gap-tabell mot nuvarande `DELETE /v1/me`
Nedan är tabeller som **idag varken raderas, anonymiseras eller motiveras att behållas** vid kontoradering.
| # | Tabell | Kategori | Risk | Föreslagen åtgärd |
| --- | ------------------------------- | ----------- | ------------------------------------------------ | --------------------------------------------------------------------------------------------------------------- |
| 1 | `user_credentials` | Auth | Lösenordshash överlever radering. | `DELETE FROM user_credentials WHERE user_id = $1` |
| 2 | `admin_totp` | Auth | TOTP-secret överlever. | `DELETE` |
| 3 | `email_verification_tokens` | Auth | Tokens överlever. | `DELETE` |
| 4 | `password_reset_tokens` | Auth | Tokens överlever. | `DELETE` |
| 5 | `user_consents` | Samtycken | Samtyckeshistorik överlever. | `DELETE` |
| 6 | `user_locale_preferences` | Preferenser | Locale-preferenser överlever. | `DELETE` |
| 7 | `household_members` | Hushåll | Användaren kvarstår som medlem. | `DELETE` |
| 8 | `households` (+ cascading data) | Hushåll | Enpersonshushåll läcker persondata. | **Beslut krävs** (se §2.3) |
| 9 | `inventory_transactions` | Hushåll | `actor_user_id` kvarstår. | `SET actor_user_id = NULL` |
| 10 | `inventory_conflicts` | Hushåll | `resolved_by_user_id` kvarstår. | `SET resolved_by_user_id = NULL` |
| 11 | `shopping_list_items` | Hushåll | `added_by_user_id` kvarstår. | `SET added_by_user_id = NULL` |
| 12 | `meal_boxes` | Hushåll | `reserved_for_user_id` kvarstår. | `SET reserved_for_user_id = NULL` |
| 13 | `cooking_sessions` | Hushåll | `started_by_user_id` kvarstår. | `SET started_by_user_id = NULL` |
| 14 | `receipts` | Hushåll | Kvittobild (`image_url`) kan innehålla PII. | Radera bild i lagring, sätt `image_url = NULL` |
| 15 | `receipt_lines` | Hushåll | OCR-text kan innehålla PII. | Skrubba `raw_text` eller radera rader om hushåll raderas. |
| 16 | `recipe_ratings` | Recept | Betyg/kommentar överlever. | `DELETE` |
| 17 | `recipe_favorites` | Recept | Favoriter överlever. | `DELETE` |
| 18 | `recipe_cooks` | Recept | Lagningslogg överlever. | `DELETE` |
| 19 | `food_memories` | Matminnen | Matminnen + bild överlever. | `DELETE` (+ bild i lagring) |
| 20 | `creator_stats` | Creator | Creator-profil överlever. | `DELETE` |
| 21 | `creator_follows` | Creator | Följer/följare överlever. | `DELETE` |
| 22 | `notifications` | Notiser | Notiser överlever. | `DELETE` |
| 23 | `push_tokens` | Push | Push-token överlever. | `DELETE` |
| 24 | `trials` | Trial | Trial-info överlever. | `DELETE` |
| 25 | `ai_usage_counters` | Usage | AI-användning överlever. | `DELETE` |
| 26 | `audit_logs` | Audit | `actor_user_id`, `ip`, metadata överlever. | Anonymisera (`NULL` + skrubba metadata) |
| 27 | `domain_events` | Events | `user_id` + PII i payload överlever. | Anonymisera (`NULL` + skrubba payload) |
| 28 | `idempotency_keys` | API | Cachade svar kopplade till användaren överlever. | `DELETE` |
| 29 | `subscriptions` | Betalning | `user_id` kvarstår för evigt. | Pseudonymisera efter retention (eller direkt vid radering om ekonomiskt spår finns kvar i `store_transactions`) |
| 30 | `subscription_events` | Betalning | `user_id` + payload kvarstår. | Anonymisera (`user_id = NULL`, skrubba payload) |
| 31 | `store_transactions` | Betalning | `raw_payload` kan innehålla PII. | Skrubba `raw_payload`, anonymisera `user_id` |
**Totalt:** 31 tabell-förekomster som behöver åtgärdas (många är `SET NULL`, inte radering).
---
## 4. Bilder / objektlagring
Förutom redan hanterade `scan_jobs.s3_keys`, `ai_corrections.image_s3_key` och `ai_training_bank.image_s3_key` finns följande användarbilder:
| Källa | Kolumn | Risk | Åtgärd vid GDPR-radering |
| ------------------------- | ------------------ | ----------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ |
| `meals.photo_url` | Foto på måltid | Personlig bild. | Radera objektet i lagring; sätt `photo_url = NULL`. |
| `food_memories.photo_url` | Foto från matminne | Personlig bild. | Radera objektet i lagring; sätt `photo_url = NULL`. |
| `receipts.image_url` | Kvittobild | Potentiellt personlig (kortnummer, butik, köp). | Radera objektet i lagring; sätt `image_url = NULL`. |
| `recipes.image_urls` | Receptbild | Om receptet är **draft/private** och skapats av användaren: personlig. Om publikt/verifierat: delad resurs. | För draft/private: radera bilderna. För publika: behåll (om användaren samtyckt) eller anonymisera skaparen. |
**Notering:** Det finns ingen dedikerad profilbildskolumn i `users`. Om sådan läggs till i framtiden måste den inkluderas i residual-testet.
---
## 5. Design — RESIDUAL-TEST
Mål: en permanent test som skapar en användare med **en rad i varje personlig tabell**, kör `DELETE /v1/me`, och verifierar att inga personspår kvarstår (utom motiverat-behåll som måste vara PII-fritt/anonymiserat).
### 5.1 Teststruktur (förslag)
```ts
describe("DELETE /v1/me — GDPR residual test", () => {
it("lämnar inga personspår efter kontoradering", async () => {
const { token, userId } = await registerUser("residual@example.invalid");
// 1. Skapa en rad i varje personlig tabell
await seedAllPersonalTables(userId, token);
// 2. Kör DELETE /v1/me
const res = await app.inject({
method: "DELETE",
url: "/v1/me",
headers: { authorization: `Bearer ${token}` },
});
expect(res.statusCode).toBe(200);
// 3. Verifiera: varje tabell är antingen tom eller anonym/PII-fri
await assertNoPersonalTraces(app.db, userId);
});
});
```
### 5.2 Hjälpfunktioner
`seedAllPersonalTables(userId, token)` ska skapa exempeldata i:
- `user_credentials` (skapas vid registrering)
- `user_health_profiles`, `user_preferences`, `user_locale_preferences`, `user_consents`
- `households` + `household_members` (+ `storage_locations`)
- `scan_jobs` + `ai_corrections` + `ai_training_bank`
- `meals`, `memory_items`, `taste_signals`, `food_memories`
- `recipe_ratings`, `recipe_favorites`, `recipe_cooks`, `creator_stats`, `creator_follows`
- `notifications`, `push_tokens`
- `trials`, `ai_usage_counters`
- `audit_logs`, `domain_events`, `idempotency_keys`
- `subscriptions`, `subscription_events`, `store_transactions`
- `inventory_transactions`, `inventory_conflicts`, `shopping_list_items`, `meal_boxes`, `cooking_sessions`
- `receipts` + `receipt_lines`
`assertNoPersonalTraces(db, userId)` ska:
1. För **RADERA**-tabeller: `SELECT count(*) = 0 WHERE user_id = $userId`.
2. För **ANONYMISERA**-tabeller: `SELECT count(*) = 0 WHERE user_id = $userId` (eftersom `user_id` ska vara `NULL`).
3. För **BEHÅLL**-tabeller: tillåta rader med `user_id = $userId` **men** kontrollera att känsliga fält (`raw_payload`, `metadata`, `ip`) är skrubbade.
4. För bilder: mocka `storage.deleteObject` och verifiera att alla personliga bildnycklar raderats.
5. För `users`: kontrollera att `email`, `displayName` är anonymiserade och `deletedAt` är satt.
### 5.3 Mekanism för att upptäcka nya tabeller
För att residual-testet automatiskt ska upptäcka framtida tabeller som lägger till `user_id`:
```ts
// I testet: reflektera schemat och jämför med en allow-list
const userIdColumns = await db.execute(sql`
SELECT table_name, column_name
FROM information_schema.columns
WHERE table_schema = 'public'
AND column_name IN ('user_id', 'actor_user_id', 'creator_user_id',
'started_by_user_id', 'resolved_by_user_id',
'added_by_user_id', 'reserved_for_user_id')
`);
// Jämför med allow-list över tabeller som testet täcker.
// Om en ny tabell dyker upp: testet failar tills den klassas.
```
Denna "schema-diff"-assertion gör testet till en permanent vakt.
### 5.4 Förväntat testresultat idag (före implementation)
Testet kommer att **faila** på de ~31 luckor listade i §3. Det är avsiktligt — det blir den röda tråden som implementationen ska göra grön.
---
## 6. Öppna beslut innan implementation
Följande punkter behöver ditt (Johans) godkännande eller vägledning:
1. **Enpersonshushåll vid radering:** Ska vi automatiskt radera hela hushållet (inkl. lager, listor, kvitton, matplaner) när enda medlemmen begär GDPR-radering? Alternativet är att tvinga användaren överföra ägarskap först, vilket är mer komplext.
- **Rekommendation:** Automatisk radering av enpersonshushåll.
2. **Publika recept:** Ska ett publikt/verifierat recept som användaren skapat anonymiseras (`creator_user_id = NULL`, `creator_display_name = NULL`) eller raderas helt?
- **Rekommendation:** Anonymisera publika recept, radera draft/private.
3. **Finansiella poster:** Ska `subscriptions.user_id` pseudonymiseras direkt vid GDPR-radering, eller först efter 7 år? Bokföringslagen kräver att vi kan bevisa köpet, men inte nödvändigtvis vem som gjorde det.
- **Rekommendation:** Sätt `subscriptions.user_id = NULL` direkt vid radering; ekonomisk spårbarhet finns kvar i `store_transactions` (som skrubbas men behåller transaktions-ID).
4. **Audit logs:** Ska `audit_logs` behållas med `actor_user_id = NULL` i 2+ år, eller raderas efter kortare tid?
- **Rekommendation:** Behåll 2 år med anonymiserad aktör, för säkerhetsincident-utredning.
5. **Domain events:** Ska `domain_events` behållas för event-sourcing, men med `user_id = NULL` och skrubbad payload?
- **Rekommendation:** Ja, behåll för downstream replay/konsumenter.
---
## 7. Sammanfattning & rekommenderad ordningsföljd
### 7.1 Snabbfix (högsta riskerna)
1. Radera auth-tabeller: `user_credentials`, `admin_totp`, `email_verification_tokens`, `password_reset_tokens`.
2. Radera preferenser/samtycken: `user_consents`, `user_locale_preferences`.
3. Radera notiser/push: `notifications`, `push_tokens`.
4. Radera trials/usage: `trials`, `ai_usage_counters`.
5. Radera idempotency: `idempotency_keys`.
6. Radera receptinteraktioner: `recipe_ratings`, `recipe_favorites`, `recipe_cooks`, `creator_stats`, `creator_follows`, `food_memories`.
7. Anonymisera hushållsreferenser: `inventory_transactions.actor_user_id`, `inventory_conflicts.resolved_by_user_id`, `shopping_list_items.added_by_user_id`, `meal_boxes.reserved_for_user_id`, `cooking_sessions.started_by_user_id`.
8. Hantera kvitton: radera `receipts.image_url` i lagring, skrubba `receipt_lines.raw_text`.
### 7.2 Större beslut
9. Hushållsstrategi: radera enpersonshushåll, anonymisera delade hushåll.
10. Finansiella poster: anonymisera `subscriptions`/`subscription_events`/`store_transactions` enligt retention.
11. Audit/domain events: anonymisera men behåll enligt retention.
### 7.3 Permanent vakt
12. Implementera residual-testet i `apps/api/test/me.residual.test.ts` med schema-diff-assertion.
---
## 8. Bilaga — fullständig tabellista från schema
(Används för att säkerställa att inga tabeller missats.)
```
adminTotp, aiCorrections, aiEvalCases, aiEvalRuns, aiTrainingBank,
aiUsageCounters, allergenMarketRules, analyticsDailySnapshots, auditLogs,
canonicalIngredients, cookingAssumptionProfiles, cookingSessions,
creatorFollows, creatorStats, domainEvents, emailVerificationTokens,
featureFlags, foodMemories, householdMembers, households, idempotencyKeys,
ingredientTranslations, inventoryConflicts, inventoryDecayProfiles,
inventoryItemDecayProfile, inventoryItems, inventoryTransactions,
mealBoxes, meals, memoryItems, notifications, nutritionDisplayProfiles,
passwordResetTokens, priceObservations, productAnalyticsEvents, products,
pushTokens, receiptLines, receipts, recipeCooks, recipeFavorites,
recipeIngredients, recipeRatings, recipeSimilarities, recipeSourceRegistry,
recipeStepTranslations, recipeSteps, recipeTranslations, recipes,
refreshTokens, releaseGates, scanJobs, seasonEvents, shoppingListItems,
shoppingLists, storageLocations, storeNotifications, storeTransactions,
subscriptionEvents, subscriptions, substitutions, tasteSignals, trials,
unitTranslations, userConsents, userCredentials, userHealthProfiles,
userLocalePreferences, userPreferences, users, wasteSummaries,
weekPlanEntries, weekPlans
```
Alla tabeller är granskade och klassade ovan.