# 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.