ci: trigga på master + formatfix inför Gitea Actions
CI / Typecheck, test & build (push) Failing after 2s
CI / Typecheck, test & build (push) Failing after 2s
This commit is contained in:
@@ -122,8 +122,8 @@ separat `image_training`-samtycke. Personligt minne är aldrig träningsdata (§
|
||||
|
||||
## Lägen
|
||||
|
||||
| `AAMOS_MODE` | Användning |
|
||||
|---|---|
|
||||
| `mock` | dev/test utan nätverk. Produkten vägrar starta i mock. |
|
||||
| `http` | Riktig AAMOS Core när matmodell finns. |
|
||||
| `gemini` | Gemini 2.5 Flash som lärar-tier för kylskåpsskanning (Skiva 1). |
|
||||
| `AAMOS_MODE` | Användning |
|
||||
| ------------ | --------------------------------------------------------------- |
|
||||
| `mock` | dev/test utan nätverk. Produkten vägrar starta i mock. |
|
||||
| `http` | Riktig AAMOS Core när matmodell finns. |
|
||||
| `gemini` | Gemini 2.5 Flash som lärar-tier för kylskåpsskanning (Skiva 1). |
|
||||
|
||||
+25
-25
@@ -28,18 +28,18 @@ eller råa bilder (se §56/§58).
|
||||
|
||||
Varje rad i `ai_corrections` representerar **ett item** från en skanning:
|
||||
|
||||
| Fält | Innehåll |
|
||||
|---|---|
|
||||
| `scanJobId` | Källskanningen (`scan_jobs.id`). |
|
||||
| `taskType` | T.ex. `ANALYZE_FRIDGE_IMAGE`, `READ_RECEIPT`. |
|
||||
| `aiOutput` | Hela AI-raw-resultatet från `scan_jobs.result`. |
|
||||
| `proposal` | Det specifika AI-förslag item:et kom från (`detectedName`, `canonicalIngredientId`, `brand`, `estimatedQuantity`, `unit`, `bestBeforeDate`, `confidence`, `requiresConfirmation`). |
|
||||
| `userCorrection` | `{ action: "accept" \| "edit" \| "reject" \| "add", corrected?: {...} }` |
|
||||
| `corrected` (inbäddad) | Användarens slutgiltiga värden vid accept/edit/add: `displayName`, `canonicalIngredientId`, `brand`, `quantity`, `unit`, `bestBeforeDate`, `useByDate`. |
|
||||
| `imageS3Key` | Första lagrade bildnyckeln från skanningen, **endast om** `image_training`-samtycke fanns vid bekräftelsen. Annars `null`. |
|
||||
| `modelVersion` / `promptVersion` | Vilken modell och prompt som producerade förslaget. |
|
||||
| `consentSnapshot` | `{ anonymized_improvement: "granted"\|"denied", image_training: "granted"\|"denied", ... }` som JSON vid bekräftelsetillfället. |
|
||||
| `createdAt` | Tidsstämpel för bekräftelsen. |
|
||||
| Fält | Innehåll |
|
||||
| -------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `scanJobId` | Källskanningen (`scan_jobs.id`). |
|
||||
| `taskType` | T.ex. `ANALYZE_FRIDGE_IMAGE`, `READ_RECEIPT`. |
|
||||
| `aiOutput` | Hela AI-raw-resultatet från `scan_jobs.result`. |
|
||||
| `proposal` | Det specifika AI-förslag item:et kom från (`detectedName`, `canonicalIngredientId`, `brand`, `estimatedQuantity`, `unit`, `bestBeforeDate`, `confidence`, `requiresConfirmation`). |
|
||||
| `userCorrection` | `{ action: "accept" \| "edit" \| "reject" \| "add", corrected?: {...} }` |
|
||||
| `corrected` (inbäddad) | Användarens slutgiltiga värden vid accept/edit/add: `displayName`, `canonicalIngredientId`, `brand`, `quantity`, `unit`, `bestBeforeDate`, `useByDate`. |
|
||||
| `imageS3Key` | Första lagrade bildnyckeln från skanningen, **endast om** `image_training`-samtycke fanns vid bekräftelsen. Annars `null`. |
|
||||
| `modelVersion` / `promptVersion` | Vilken modell och prompt som producerade förslaget. |
|
||||
| `consentSnapshot` | `{ anonymized_improvement: "granted"\|"denied", image_training: "granted"\|"denied", ... }` som JSON vid bekräftelsetillfället. |
|
||||
| `createdAt` | Tidsstämpel för bekräftelsen. |
|
||||
|
||||
### Åtgärder som sparas
|
||||
|
||||
@@ -56,19 +56,19 @@ Alla fyra åtgärder sparas. Bara accept/edit/add leder till att ett
|
||||
När `BUILD_TRAINING_SAMPLE` kör (veckoschema i worker) bankas rader med
|
||||
`anonymized_improvement = granted` till `ai_training_bank`:
|
||||
|
||||
| Fält | Innehåll |
|
||||
|---|---|
|
||||
| `correctionId` | Referens till `ai_corrections.id` (cascade delete). |
|
||||
| `scanJobId` | Källskanningen. |
|
||||
| `version` | Dataset-version, t.ex. `v1`. Bumpar när formatet ändras. |
|
||||
| `taskType` | Samma som källan. |
|
||||
| `imageS3Key` | Kopierad från `ai_corrections.image_s3_key` (kan vara `null`). |
|
||||
| `proposal` | AI-förslaget för just det item:et. |
|
||||
| `action` | Användarens åtgärd. |
|
||||
| `corrected` | Slutgiltiga värden, eller `null` vid reject. |
|
||||
| `modelVersion` / `promptVersion` | Spårbarhet till modell/prompt. |
|
||||
| `consentSnapshot` | Kopia av samtyckesläget. |
|
||||
| `exportedAt` | När raden bankades. |
|
||||
| Fält | Innehåll |
|
||||
| -------------------------------- | -------------------------------------------------------------- |
|
||||
| `correctionId` | Referens till `ai_corrections.id` (cascade delete). |
|
||||
| `scanJobId` | Källskanningen. |
|
||||
| `version` | Dataset-version, t.ex. `v1`. Bumpar när formatet ändras. |
|
||||
| `taskType` | Samma som källan. |
|
||||
| `imageS3Key` | Kopierad från `ai_corrections.image_s3_key` (kan vara `null`). |
|
||||
| `proposal` | AI-förslaget för just det item:et. |
|
||||
| `action` | Användarens åtgärd. |
|
||||
| `corrected` | Slutgiltiga värden, eller `null` vid reject. |
|
||||
| `modelVersion` / `promptVersion` | Spårbarhet till modell/prompt. |
|
||||
| `consentSnapshot` | Kopia av samtyckesläget. |
|
||||
| `exportedAt` | När raden bankades. |
|
||||
|
||||
Banken ägs av cibello och är versionerad. Ingen extern leverantör
|
||||
anropas under exporten — jobbet får aldrig kasta på grund av att
|
||||
|
||||
+137
-130
@@ -5,6 +5,7 @@
|
||||
> **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`)
|
||||
@@ -28,14 +29,14 @@
|
||||
|
||||
### 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. |
|
||||
| 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.
|
||||
|
||||
@@ -43,12 +44,12 @@
|
||||
|
||||
### 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. |
|
||||
| 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.
|
||||
|
||||
@@ -56,13 +57,14 @@
|
||||
|
||||
### 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**. |
|
||||
| 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.
|
||||
@@ -73,24 +75,24 @@
|
||||
|
||||
### 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. |
|
||||
| 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.
|
||||
|
||||
@@ -98,10 +100,10 @@
|
||||
|
||||
### 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. |
|
||||
| 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.
|
||||
@@ -110,17 +112,17 @@
|
||||
|
||||
### 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). |
|
||||
| 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.
|
||||
|
||||
@@ -128,10 +130,10 @@
|
||||
|
||||
### 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). |
|
||||
| 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.
|
||||
|
||||
@@ -139,14 +141,14 @@
|
||||
|
||||
### 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. |
|
||||
| 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.
|
||||
|
||||
@@ -154,38 +156,38 @@
|
||||
|
||||
### 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. |
|
||||
| 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. |
|
||||
| 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. |
|
||||
|
||||
---
|
||||
|
||||
@@ -193,39 +195,39 @@
|
||||
|
||||
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` |
|
||||
| # | 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).
|
||||
|
||||
@@ -235,12 +237,12 @@ Nedan är tabeller som **idag varken raderas, anonymiseras eller motiveras att b
|
||||
|
||||
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. |
|
||||
| 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.
|
||||
|
||||
@@ -277,6 +279,7 @@ describe("DELETE /v1/me — GDPR residual test", () => {
|
||||
### 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`)
|
||||
@@ -291,6 +294,7 @@ describe("DELETE /v1/me — GDPR residual test", () => {
|
||||
- `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.
|
||||
@@ -347,6 +351,7 @@ Följande punkter behöver ditt (Johans) godkännande eller vägledning:
|
||||
## 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`.
|
||||
@@ -357,11 +362,13 @@ Följande punkter behöver ditt (Johans) godkännande eller vägledning:
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
@@ -5,22 +5,26 @@
|
||||
## Befintliga motorer
|
||||
|
||||
### Sökmotor: `/v1/inventory`
|
||||
|
||||
- Enkel `ILIKE` mot `inventory_items.display_name` och `brand`.
|
||||
- Inga synonymer, ingen fonetisk matchning, ingen felstavningstolerans.
|
||||
- Filtrering på `storage_location_id`, `expiry_status`, paginering.
|
||||
|
||||
### Förbättrad sökmotor: `/v1/inventory/natural-search`
|
||||
|
||||
- Kombinerar `unaccent(...) ILIKE unaccent(...)` med PostgreSQL `pg_trgm`-similarity.
|
||||
- Söker även mot kanoniska ingrediensnamn (`name_sv`, `name_en`).
|
||||
- Sorterar på `GREATEST(similarity(...))`.
|
||||
- Returnerar både strukturerade träffar och ett naturligt-språkligt svar.
|
||||
|
||||
### Trust-motor (`@app/inventory-engine`)
|
||||
|
||||
- `computeTrust(...)` ger `trustState` (`trusted`/`decaying`/`stale`/`unverified`) och `trustScore` 0–100.
|
||||
- Baserat på `confidence`, `verifiedByUser`, ålder och förruttnelseprofil.
|
||||
- Används redan i `/v1/inventory` och vid skanning/konsumtion.
|
||||
|
||||
### FoodTwin-data
|
||||
|
||||
- `storage_locations`: namn, typ, sublocations.
|
||||
- `inventory_items`: `storage_location_id`, `sublocation`, kvantitet, enhet, `trust_state`.
|
||||
- `canonical_ingredients`: kanoniskt namn, synonymer, hållbarhetsriktlinjer.
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
> Detta dokument definierar **en gång** hur Cibello ska kännas personlig, så att
|
||||
> alla AI-nära funktioner (sök, "vad ska vi äta", rekommendationer, proaktiva
|
||||
> puffar, receptpresentation) byggs mot samma själ i stället för att uppfinnas
|
||||
> per funktion. Det är en kravspec för *känslan* – inte en teknisk migrering.
|
||||
> per funktion. Det är en kravspec för _känslan_ – inte en teknisk migrering.
|
||||
> Allt nedan förankras i motorer som redan finns i plattformen.
|
||||
|
||||
---
|
||||
@@ -13,7 +13,7 @@
|
||||
Cibello är inte "ännu en receptsök". Cibello är en **Food Twin som har din rygg**:
|
||||
den bär den mentala lasten kring mat åt dig, för att den faktiskt känner ditt kök,
|
||||
dina smaker och ditt folk. Målet är att en vanlig, trött människa ska känna att
|
||||
appen *ser* dem och gör vardagen enklare – aldrig att de blir övervakade eller
|
||||
appen _ser_ dem och gör vardagen enklare – aldrig att de blir övervakade eller
|
||||
bedömda.
|
||||
|
||||
Ledstjärna i en mening: **"Den vet var räkosten står, förstår att du tycker den
|
||||
@@ -24,10 +24,10 @@ Ledstjärna i en mening: **"Den vet var räkosten står, förstår att du tycker
|
||||
## 2. Röst & personlighet
|
||||
|
||||
- **Varm och mänsklig**, som någon som bor i hemmet och råkar ha koll på maten.
|
||||
Erkänner suget ("jag fattar, den är ju god"), dömer aldrig.
|
||||
Erkänner suget ("jag fattar, den är ju god"), dömer aldrig.
|
||||
- **Kompetent men ödmjuk**. Säker när den vet, öppen när den gissar. Skryter inte.
|
||||
- **Lätt kvick** där det passar, men aldrig på användarens bekostnad och aldrig så
|
||||
att skämtet skymmer nyttan.
|
||||
att skämtet skymmer nyttan.
|
||||
- **Kort och konkret**. En trött människa vill ha svar, inte en essä.
|
||||
- **Aldrig en robot, aldrig en tjatig coach.** Ingen pekpinne, ingen skam.
|
||||
|
||||
@@ -39,19 +39,19 @@ hårdkodas per skärm.
|
||||
## 3. Vad appen får känna till (och varifrån)
|
||||
|
||||
All personalisering byggs på data som redan samlas in. "Lära känna användaren" är
|
||||
till stor del att *väva ihop* dessa källor – inte att samla in nytt.
|
||||
till stor del att _väva ihop_ dessa källor – inte att samla in nytt.
|
||||
|
||||
| Vad appen kan veta | Källa som redan finns |
|
||||
| --- | --- |
|
||||
| Vad du har hemma och var | `inventory_items` + `storage_locations` (+ `sublocation`) |
|
||||
| Hur säker den är på att det stämmer | Trust-motorn (`trusted`/`decaying`/`stale`) |
|
||||
| Vad du gillar/ogillar | `taste_signals`, betyg, laghistorik |
|
||||
| Hur mycket ni brukar äta | `cooking_assumption_profiles` |
|
||||
| Vilka ni är i hushållet | `household_members`, personer/portioner |
|
||||
| Allergener & kostbehov | `user_health_profiles` (**alltid användarsatt**) |
|
||||
| Uttalade mål & preferenser | onboarding-mål, `user_preferences` |
|
||||
| Uttryckliga minnen | `memory_items` / `food_memories` ("barnen ogillar broccoli") |
|
||||
| Språk | locale-preferens (×12) |
|
||||
| Vad appen kan veta | Källa som redan finns |
|
||||
| ----------------------------------- | ------------------------------------------------------------ |
|
||||
| Vad du har hemma och var | `inventory_items` + `storage_locations` (+ `sublocation`) |
|
||||
| Hur säker den är på att det stämmer | Trust-motorn (`trusted`/`decaying`/`stale`) |
|
||||
| Vad du gillar/ogillar | `taste_signals`, betyg, laghistorik |
|
||||
| Hur mycket ni brukar äta | `cooking_assumption_profiles` |
|
||||
| Vilka ni är i hushållet | `household_members`, personer/portioner |
|
||||
| Allergener & kostbehov | `user_health_profiles` (**alltid användarsatt**) |
|
||||
| Uttalade mål & preferenser | onboarding-mål, `user_preferences` |
|
||||
| Uttryckliga minnen | `memory_items` / `food_memories` ("barnen ogillar broccoli") |
|
||||
| Språk | locale-preferens (×12) |
|
||||
|
||||
---
|
||||
|
||||
@@ -61,14 +61,14 @@ Detta är regeln som gör det personliga **magiskt i stället för trasigt eller
|
||||
creepy**, och den är icke-förhandlingsbar:
|
||||
|
||||
- Appen påstår **bara** fakta som finns i verklig data. "Står i kyldörren" endast
|
||||
om lager + plats säger det. Ingen påhittad hylla, ingen påhittad vara.
|
||||
om lager + plats säger det. Ingen påhittad hylla, ingen påhittad vara.
|
||||
- Osäkerhet kommer från **trust-motorn**, aldrig från ett påklistrat skämt. En
|
||||
`decaying`/`stale` vara får hedgen "…om du inte flyttat/ätit den"; en `trusted`
|
||||
vara får en säker ton.
|
||||
`decaying`/`stale` vara får hedgen "…om du inte flyttat/ätit den"; en `trusted`
|
||||
vara får en säker ton.
|
||||
- Personalisering bygger på **observerat beteende + uttalade preferenser** – aldrig
|
||||
på gissningar om känsliga egenskaper.
|
||||
på gissningar om känsliga egenskaper.
|
||||
- Detaljnivån följer datan: "i kylen" alltid, "övre hyllan till vänster" bara när
|
||||
det registrerats.
|
||||
det registrerats.
|
||||
|
||||
Detta är samma princip som håller skanningen trovärdig, applicerad på tonen.
|
||||
|
||||
@@ -80,10 +80,10 @@ Detta är samma princip som håller skanningen trovärdig, applicerad på tonen.
|
||||
osynlig. Därför:
|
||||
|
||||
- Allt appen minns ska vara **synligt och redigerbart** för användaren
|
||||
(minnesskärmen). Inget hemligt profilbygge.
|
||||
(minnesskärmen). Inget hemligt profilbygge.
|
||||
- Personalisering är **samtyckesgatad** och kan stängas av per kategori.
|
||||
- Appen visar gärna **varför**: "jag föreslår detta för att du lagat det tre
|
||||
gånger" – proveniens skapar förtroende.
|
||||
gånger" – proveniens skapar förtroende.
|
||||
- Inget känsligt härleds tyst. Allergener och kostbehov **sätter användaren**.
|
||||
- Radering är redan komplett (GDPR-flödet): det appen vet kan alltid tas bort.
|
||||
|
||||
@@ -95,9 +95,9 @@ En app som känner din mat måste vara snäll. Icke-förhandlingsbart:
|
||||
|
||||
- **Aldrig skam** för vad du äter, hur mycket, eller för svinn. Hjälp, polisa inte.
|
||||
- **Ingen skadlig kaloripolisiär.** Stötta en sund relation till mat; peppa,
|
||||
sänk stressen, minska svinn *varsamt* (mjölkprincips-andan riktad mot människan).
|
||||
sänk stressen, minska svinn _varsamt_ (mjölkprincips-andan riktad mot människan).
|
||||
- **Säkerhet går före personlighet.** Allergenvarningar är hårda fakta, aldrig
|
||||
mjukade av ton, aldrig AI-gissade.
|
||||
mjukade av ton, aldrig AI-gissade.
|
||||
- **Familjesäkert.** Hushåll har barn; håll språk och innehåll åldersanpassat.
|
||||
|
||||
---
|
||||
@@ -107,14 +107,14 @@ En app som känner din mat måste vara snäll. Icke-förhandlingsbart:
|
||||
Samma själ, konsekvent applicerad:
|
||||
|
||||
- **"Vad ska vi äta?" / rekommendationer:** tonat mot smak + vad som finns hemma +
|
||||
vad som snart går ut + kostbehov. Förklarar valet kort.
|
||||
vad som snart går ut + kostbehov. Förklarar valet kort.
|
||||
- **Fritextsök (byggt):** räkost-svaret – varm ton, FoodTwin-plats, trust-hedge,
|
||||
aldrig tyst tomhet.
|
||||
aldrig tyst tomhet.
|
||||
- **Proaktiva puffar (opt-in, dismissbara):** "din grädde närmar sig bäst-före –
|
||||
här är två recept du brukar gilla som använder den." Aldrig tjat.
|
||||
här är två recept du brukar gilla som använder den." Aldrig tjat.
|
||||
- **Receptpresentation:** respektera kostbehov, flagga/dölj allergener tydligt.
|
||||
- **Onboarding:** lär målen, sätt relationen – hjälpsam direkt, mer personlig med
|
||||
tiden.
|
||||
tiden.
|
||||
|
||||
---
|
||||
|
||||
@@ -148,15 +148,15 @@ i18n.
|
||||
|
||||
## 11. Koppling till befintliga motorer (så det blir ihopkoppling, inte nybygge)
|
||||
|
||||
| Persona-behov | Befintlig motor att väva in |
|
||||
| --- | --- |
|
||||
| "Vet var grejen står" | inventory + storage_locations + sublocation |
|
||||
| "Ärlig om osäkerhet" | trust-motorn (states + score) |
|
||||
| "Kommer ihåg mig" | memory_items / food_memories |
|
||||
| "Vet vad jag gillar" | taste_signals + laghistorik |
|
||||
| "Vet hur mycket vi äter" | cooking_assumption_profiles |
|
||||
| "Respekterar mina behov" | user_health_profiles (användarsatt) |
|
||||
| "Jag har kontroll" | user_consents + GDPR-radering (klart) |
|
||||
| "Pratar mitt språk" | i18n ×12 |
|
||||
| Persona-behov | Befintlig motor att väva in |
|
||||
| ------------------------ | ------------------------------------------- |
|
||||
| "Vet var grejen står" | inventory + storage_locations + sublocation |
|
||||
| "Ärlig om osäkerhet" | trust-motorn (states + score) |
|
||||
| "Kommer ihåg mig" | memory_items / food_memories |
|
||||
| "Vet vad jag gillar" | taste_signals + laghistorik |
|
||||
| "Vet hur mycket vi äter" | cooking_assumption_profiles |
|
||||
| "Respekterar mina behov" | user_health_profiles (användarsatt) |
|
||||
| "Jag har kontroll" | user_consents + GDPR-radering (klart) |
|
||||
| "Pratar mitt språk" | i18n ×12 |
|
||||
|
||||
Nästan varje personlig känsla har redan en motor. Uppgiften är att ge den röst.
|
||||
|
||||
@@ -15,65 +15,73 @@
|
||||
|
||||
### 1. `meatball_pork_beef` – Köttbullar (färdiga)
|
||||
|
||||
| Fält | Nuvarande | Kommentar |
|
||||
|---|---|---|
|
||||
| `allergens` | `[]` | Färdiga köttbullar innehåller nästan alltid ströbröd (vetemjöl) och ofta ägg och/eller mjölk som bindemedel. |
|
||||
| `isPork` | `true` | ✅ |
|
||||
| `isBeef` | `true` | ✅ |
|
||||
| Fält | Nuvarande | Kommentar |
|
||||
| ----------- | --------- | ------------------------------------------------------------------------------------------------------------ |
|
||||
| `allergens` | `[]` | Färdiga köttbullar innehåller nästan alltid ströbröd (vetemjöl) och ofta ägg och/eller mjölk som bindemedel. |
|
||||
| `isPork` | `true` | ✅ |
|
||||
| `isBeef` | `true` | ✅ |
|
||||
|
||||
**Föreslagen rättning:**
|
||||
|
||||
```ts
|
||||
allergens: ["gluten", "eggs", "milk"]
|
||||
containsGluten: true
|
||||
containsLactose: true
|
||||
allergens: ["gluten", "eggs", "milk"];
|
||||
containsGluten: true;
|
||||
containsLactose: true;
|
||||
```
|
||||
|
||||
**Motivering:** Standardrecept för svenska färdiga köttbullar innehåller ströbröd/vetemjöl, ägg och mjölk. Detta är den konservativa taggningen för produkttypen.
|
||||
|
||||
---
|
||||
|
||||
### 2. `falukorv` – Falukorv
|
||||
|
||||
| Fält | Nuvarande | Kommentar |
|
||||
|---|---|---|
|
||||
| `allergens` | `[]` | Falukorv innehåller ofta potatismjöl eller vetemjöl samt mjölkpulver. |
|
||||
| `isPork` | `true` | ✅ |
|
||||
| `isBeef` | `true` | ✅ |
|
||||
| Fält | Nuvarande | Kommentar |
|
||||
| ----------- | --------- | --------------------------------------------------------------------- |
|
||||
| `allergens` | `[]` | Falukorv innehåller ofta potatismjöl eller vetemjöl samt mjölkpulver. |
|
||||
| `isPork` | `true` | ✅ |
|
||||
| `isBeef` | `true` | ✅ |
|
||||
|
||||
**Föreslagen rättning:**
|
||||
|
||||
```ts
|
||||
allergens: ["gluten", "milk"]
|
||||
containsGluten: true
|
||||
containsLactose: true
|
||||
allergens: ["gluten", "milk"];
|
||||
containsGluten: true;
|
||||
containsLactose: true;
|
||||
```
|
||||
|
||||
**Motivering:** Falukorv är en köttkorv som traditionellt innehåller späck, potatismjöl/vetemjöl och ofta mjölkpulver. Konservativ taggning för produkttypen.
|
||||
|
||||
---
|
||||
|
||||
### 3. `red_curry_paste` – Röd currypasta
|
||||
|
||||
| Fält | Nuvarande | Kommentar |
|
||||
|---|---|---|
|
||||
| `allergens` | `[]` | Thailändsk röd currypasta (khrueang kaeng phet) innehåller traditionellt khrung (räkor eller fisk) som en av basingredienserna. |
|
||||
| Fält | Nuvarande | Kommentar |
|
||||
| ----------- | --------- | ------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `allergens` | `[]` | Thailändsk röd currypasta (khrueang kaeng phet) innehåller traditionellt khrung (räkor eller fisk) som en av basingredienserna. |
|
||||
|
||||
**Föreslagen rättning:**
|
||||
|
||||
```ts
|
||||
allergens: ["crustaceans", "fish"]
|
||||
allergens: ["crustaceans", "fish"];
|
||||
```
|
||||
|
||||
**Motivering:** Röd currypasta tillverkas ofta med shrimp paste (kapi) eller fisk. Det finns veganska varianter, men produkttypen som sådan är högrisk för både fisk och skaldjur.
|
||||
|
||||
---
|
||||
|
||||
### 4. `taco_spice` – Tacokrydda
|
||||
|
||||
| Fält | Nuvarande | Kommentar |
|
||||
|---|---|---|
|
||||
| `allergens` | `[]` | Kommersiell tacokrydda varierar kraftigt mellan märken; vissa innehåller selleri, gluten, mjölk eller soja. |
|
||||
| Fält | Nuvarande | Kommentar |
|
||||
| ----------- | --------- | ----------------------------------------------------------------------------------------------------------- |
|
||||
| `allergens` | `[]` | Kommersiell tacokrydda varierar kraftigt mellan märken; vissa innehåller selleri, gluten, mjölk eller soja. |
|
||||
|
||||
**Föreslagen rättning:**
|
||||
|
||||
```ts
|
||||
allergens: []
|
||||
allergens: [];
|
||||
// men lägg till mayContainAllergens: ["celery", "gluten", "milk", "soy"]
|
||||
```
|
||||
|
||||
**Motivering:** Det finns inte en enhetlig sammansättning för tacokrydda. I stället för att hårdtagga bör vi markera "kan innehålla" för de vanligaste dolda allergenerna, så att användare med allvarliga allergier varnas utan att vi blockerar alla recept.
|
||||
|
||||
> **Notering:** `mayContainAllergens` finns redan i `IngredientSafetyInfo` men används inte i seed-modellen idag. Detta kräver ett mindre schema-API-tillägg.
|
||||
@@ -82,8 +90,8 @@ allergens: []
|
||||
|
||||
### 5. `chicken_stock_cube` – Kycklingbuljongtärning
|
||||
|
||||
| Fält | Nuvarande | Kommentar |
|
||||
|---|---|---|
|
||||
| Fält | Nuvarande | Kommentar |
|
||||
| ----------- | ------------ | ----------------------------------------------------- |
|
||||
| `allergens` | `["celery"]` | ✅ Korrekt. Buljongtärningar innehåller ofta selleri. |
|
||||
|
||||
**Föreslagen rättning:** Ingen ändring av huvudallergener. Överväg `mayContainAllergens: ["gluten", "milk", "soy", "egg"]` eftersom kommersiella tärningar ofta innehåller maltodextrin, jästextrakt eller mjölkprotein beroende på märke.
|
||||
@@ -92,8 +100,8 @@ allergens: []
|
||||
|
||||
### 6. `vegetable_stock_cube` – Grönsaksbuljongtärning
|
||||
|
||||
| Fält | Nuvarande | Kommentar |
|
||||
|---|---|---|
|
||||
| Fält | Nuvarande | Kommentar |
|
||||
| ----------- | ------------ | ----------- |
|
||||
| `allergens` | `["celery"]` | ✅ Korrekt. |
|
||||
|
||||
**Föreslagen rättning:** Ingen ändring av huvudallergener. Överväg `mayContainAllergens: ["gluten", "milk", "soy", "egg"]` av samma skäl som ovan.
|
||||
@@ -104,21 +112,21 @@ allergens: []
|
||||
|
||||
Följande sammansatta ingredienser har redan korrekta allergen-taggar och behöver ingen åtgärd:
|
||||
|
||||
| Ingrediens | Nuvarande `allergens` | Bedömning |
|
||||
|---|---|---|
|
||||
| `noodles_egg` | `["gluten", "eggs"]` | ✅ |
|
||||
| `bread_sourdough` | `["gluten"]` | ✅ |
|
||||
| `tortilla` | `["gluten"]` | ✅ |
|
||||
| `hamburger_bun` | `["gluten", "sesame"]` | ✅ |
|
||||
| `breadcrumbs` | `["gluten"]` | ✅ |
|
||||
| `soy_sauce` | `["soy", "gluten"]` | ✅ |
|
||||
| `fish_sauce` | `["fish"]` | ✅ |
|
||||
| `teriyaki_sauce` | `["soy", "gluten"]` | ✅ |
|
||||
| `mayonnaise` | `["eggs", "mustard"]` | ✅ |
|
||||
| `peanut_butter` | `["peanuts"]` | ✅ |
|
||||
| `mustard` | `["mustard"]` | ✅ |
|
||||
| `sesame_seeds` | `["sesame"]` | ✅ |
|
||||
| `tofu` | `["soy"]` | ✅ |
|
||||
| Ingrediens | Nuvarande `allergens` | Bedömning |
|
||||
| ----------------- | ---------------------- | --------- |
|
||||
| `noodles_egg` | `["gluten", "eggs"]` | ✅ |
|
||||
| `bread_sourdough` | `["gluten"]` | ✅ |
|
||||
| `tortilla` | `["gluten"]` | ✅ |
|
||||
| `hamburger_bun` | `["gluten", "sesame"]` | ✅ |
|
||||
| `breadcrumbs` | `["gluten"]` | ✅ |
|
||||
| `soy_sauce` | `["soy", "gluten"]` | ✅ |
|
||||
| `fish_sauce` | `["fish"]` | ✅ |
|
||||
| `teriyaki_sauce` | `["soy", "gluten"]` | ✅ |
|
||||
| `mayonnaise` | `["eggs", "mustard"]` | ✅ |
|
||||
| `peanut_butter` | `["peanuts"]` | ✅ |
|
||||
| `mustard` | `["mustard"]` | ✅ |
|
||||
| `sesame_seeds` | `["sesame"]` | ✅ |
|
||||
| `tofu` | `["soy"]` | ✅ |
|
||||
|
||||
## Rekommendation
|
||||
|
||||
|
||||
@@ -8,15 +8,16 @@
|
||||
|
||||
## 1. YT-INVENTERING — vilka AI-nära ytor ska personaliseras
|
||||
|
||||
| Yta | Nuvarande läge | Persona-effekt efter Spår 1 | Varför denna yta? |
|
||||
| --- | --- | --- | --- |
|
||||
| **A. "Vad ska vi äta?" / `GET /v1/recommendations/what-to-eat`** | ✅ Byggd. Rankar recept efter täckning, utgångsdatum, näringsfit, smak, betyg, säsong, högtid, tid, budget, variation, väder, craving. | Recept rangordnas med tydlig proveniens som inkluderar *användarens egna minnen* ("Du lagade kycklinggryta tre tisdagar i rad — här är en annan variant"), *hushållsförbrukningsmönster* och *utgångs-varor*. | Appens viktigaste surface; här märks personliga förslag mest. |
|
||||
| **B. Grupperade sök-svar / recept-sök** | 🔧 Delvis byggd (`/v1/recipes/search` finns troligen; söksvar är grundade i katalogen). | Sökresultat tonas av användarens smakprofil, hushållsbegränsningar och lager. Proaktivt utesluter allergener/undvikanden. **Bygger PÅ befintligt grundat svar (lager + plats + trust-hedge); no-fabrication-grundningen bevaras.** | Sök är en aktiv användarintent; personlig rangordning måste vara förklarlig. |
|
||||
| **C. Proaktiva puffar** | 🔧 `SEND_EXPIRY_NOTIFICATION` körs dagligen kl 07. Matlådepåminnelser finns. | Nya puffar: "Gurkan börjar se trött ut — tre recept du brukar gilla med gurka", "Ni har ätit risotto ofta på söndagar; vill du planera en?" | Hög användarnytta, låg frekvens, kräver explicit opt-in. |
|
||||
| **D. Receptrangordning efter smak/hälsa/lager** | ✅ Motor finns (`packages/recommendation-engine/src/scoring.ts`). | Skapa tre fördefinierade vyer: **Smak** (favoritkök + taste_signals), **Hälsa** (näring mot dagsmål + health_profile), **Lager** (täckning + utgår-snart). Användaren växlar; ingen hemlig viktning. | Ger användaren kontroll och transparens. |
|
||||
| **E. Onboarding-relationen** | ✅ Onboarding med mål, allergier, favoritkök. | Onboarding-klassifikationer skrivs som `user_stated`-minnen och `taste_signals`; de synkroniseras tillbaka till "Vad plattformen vet om mig". | Tidigt förtroende: användaren ser att svaren används. |
|
||||
| Yta | Nuvarande läge | Persona-effekt efter Spår 1 | Varför denna yta? |
|
||||
| ---------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- |
|
||||
| **A. "Vad ska vi äta?" / `GET /v1/recommendations/what-to-eat`** | ✅ Byggd. Rankar recept efter täckning, utgångsdatum, näringsfit, smak, betyg, säsong, högtid, tid, budget, variation, väder, craving. | Recept rangordnas med tydlig proveniens som inkluderar _användarens egna minnen_ ("Du lagade kycklinggryta tre tisdagar i rad — här är en annan variant"), _hushållsförbrukningsmönster_ och _utgångs-varor_. | Appens viktigaste surface; här märks personliga förslag mest. |
|
||||
| **B. Grupperade sök-svar / recept-sök** | 🔧 Delvis byggd (`/v1/recipes/search` finns troligen; söksvar är grundade i katalogen). | Sökresultat tonas av användarens smakprofil, hushållsbegränsningar och lager. Proaktivt utesluter allergener/undvikanden. **Bygger PÅ befintligt grundat svar (lager + plats + trust-hedge); no-fabrication-grundningen bevaras.** | Sök är en aktiv användarintent; personlig rangordning måste vara förklarlig. |
|
||||
| **C. Proaktiva puffar** | 🔧 `SEND_EXPIRY_NOTIFICATION` körs dagligen kl 07. Matlådepåminnelser finns. | Nya puffar: "Gurkan börjar se trött ut — tre recept du brukar gilla med gurka", "Ni har ätit risotto ofta på söndagar; vill du planera en?" | Hög användarnytta, låg frekvens, kräver explicit opt-in. |
|
||||
| **D. Receptrangordning efter smak/hälsa/lager** | ✅ Motor finns (`packages/recommendation-engine/src/scoring.ts`). | Skapa tre fördefinierade vyer: **Smak** (favoritkök + taste_signals), **Hälsa** (näring mot dagsmål + health_profile), **Lager** (täckning + utgår-snart). Användaren växlar; ingen hemlig viktning. | Ger användaren kontroll och transparens. |
|
||||
| **E. Onboarding-relationen** | ✅ Onboarding med mål, allergier, favoritkök. | Onboarding-klassifikationer skrivs som `user_stated`-minnen och `taste_signals`; de synkroniseras tillbaka till "Vad plattformen vet om mig". | Tidigt förtroende: användaren ser att svaren används. |
|
||||
|
||||
**Out of scope för Spår 1 (sparas till senare spår):**
|
||||
|
||||
- Generering av helt nya recept utifrån minne.
|
||||
- Community/creator-personalisering.
|
||||
- Extern hälsointegration (HealthKit/Health Connect).
|
||||
@@ -29,13 +30,13 @@
|
||||
|
||||
För varje yta anger tabellen **vilken befintlig data/motor** som är primär källa. Ingen ny AI-motor ska byggas för Spår 1; personalisering = att låta befintliga motorer läsa mer kontext.
|
||||
|
||||
| Yta | Primär befintlig data/motor | Ny data vi läser in (ingen ny motor) | Vad vi INTE bygger |
|
||||
| --- | --- | --- | --- |
|
||||
| A. Vad ska vi äta? | `recommendation-engine` (`scoreCandidate`/`rankAll`) | `memory_items` (smak/faktum/mönster), `taste_signals` (axelriktning), `cooking_assumption_profiles` (förbrukningsmönster), `recipe_cooks` (senast lagat), `recipe_ratings` (egna betyg). | Ingen ny rankningsmotor. |
|
||||
| B. Sök-svar | Recept-katalog + `recipe-engine` (säkerhet/täckning) | Samma som A plus `userPreferences` (favoriteCuisines, avoidIngredientIds, spiceLevelMax). | Ingen separat sök-AI. |
|
||||
| C. Proaktiva puffar | `inventory-engine/forecast.ts`, `reconciliation.ts`, `worker`-schemaläggare | `memory_items.recipe_memory`, `recipe_cooks`, `taste_signals`, `userConsents` (opt-in). | Ingen ny notismotor. |
|
||||
| D. Smak/Hälsa/Lager-vyer | `recommendation-engine` + `nutrition-engine` | `userPreferences`, `userHealthProfiles`, dagens `meals`, lager. | Ingen ny vy-motor; bara fördefinierade vikter. |
|
||||
| E. Onboarding | Onboarding-flödet i `apps/mobile` | Skriver till `memory_items` + `taste_signals` via befintliga API-routes. | Ingen ny onboarding-backend. |
|
||||
| Yta | Primär befintlig data/motor | Ny data vi läser in (ingen ny motor) | Vad vi INTE bygger |
|
||||
| ------------------------ | --------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- |
|
||||
| A. Vad ska vi äta? | `recommendation-engine` (`scoreCandidate`/`rankAll`) | `memory_items` (smak/faktum/mönster), `taste_signals` (axelriktning), `cooking_assumption_profiles` (förbrukningsmönster), `recipe_cooks` (senast lagat), `recipe_ratings` (egna betyg). | Ingen ny rankningsmotor. |
|
||||
| B. Sök-svar | Recept-katalog + `recipe-engine` (säkerhet/täckning) | Samma som A plus `userPreferences` (favoriteCuisines, avoidIngredientIds, spiceLevelMax). | Ingen separat sök-AI. |
|
||||
| C. Proaktiva puffar | `inventory-engine/forecast.ts`, `reconciliation.ts`, `worker`-schemaläggare | `memory_items.recipe_memory`, `recipe_cooks`, `taste_signals`, `userConsents` (opt-in). | Ingen ny notismotor. |
|
||||
| D. Smak/Hälsa/Lager-vyer | `recommendation-engine` + `nutrition-engine` | `userPreferences`, `userHealthProfiles`, dagens `meals`, lager. | Ingen ny vy-motor; bara fördefinierade vikter. |
|
||||
| E. Onboarding | Onboarding-flödet i `apps/mobile` | Skriver till `memory_items` + `taste_signals` via befintliga API-routes. | Ingen ny onboarding-backend. |
|
||||
|
||||
**Centrala datatabeller som används av flera ytor:**
|
||||
|
||||
@@ -61,6 +62,7 @@ För varje yta anger tabellen **vilken befintlig data/motor** som är primär k
|
||||
Persona-chartern översätts till följande tekniska regler. Varje regel ska gå att granska i kod och test.
|
||||
|
||||
### R1. Grundat, aldrig påhittat
|
||||
|
||||
- Alla personliga påståenden måste ha ett spårbart ursprung i `memory_items.origin` (`user_stated`, `observed`, `ai_inferred`).
|
||||
- `ai_inferred`-poster:
|
||||
- Har **låg startkonfidens** (t.ex. 0.5).
|
||||
@@ -70,29 +72,34 @@ Persona-chartern översätts till följande tekniska regler. Varje regel ska gå
|
||||
- Ingen AI får fabricera recept, ingredienser eller påståenden om användaren.
|
||||
|
||||
### R2. Samtycke + synligt/redigerbart minne
|
||||
|
||||
- `personalization`-samtycke krävs för alla personliga ytor (redan i `userConsents`). **Enkeltflaggat samtycke är OK för Spår 1**; granulär uppdelning skjuts till senare spår om behov uppstår.
|
||||
- "Vad plattformen vet om mig" (`GET /v1/me/memory`) visar alla minnen, inklusive ursprung och konfidens.
|
||||
- Användaren kan rätta (`PATCH /v1/me/memory/:id`), pausa (`POST /v1/me/memory/pause-all`) och radera (`DELETE /v1/me/memory/:id` samt `DELETE /v1/me/memory`).
|
||||
- Användarkorrigering ändrar `origin` till `user_stated` och `confidence` till `1`.
|
||||
|
||||
### R3. Ingen skam-ton
|
||||
|
||||
- Förklaringar (`whySv`) får aldrig innehålla skam, moralisering eller värdeomdömen om matvanor.
|
||||
- Exempel på förbjuden copy: "Du borde äta mindre kött", "Din kost är obalanserad".
|
||||
- Tillåten copy: "Denna rätt ger 35 g protein, vilket matchar ditt mål", "Du har lagat risotto ofta på söndagar — här är en variation".
|
||||
- Enhetstester ska validera copy mot en förbudslista.
|
||||
|
||||
### R4. Allergener/hälsa användarsatt
|
||||
|
||||
- Allergener kommer från `userPreferences.allergens` + hushållsaggregering (strängaste gäller), aldrig från AI.
|
||||
- Hälsodata (`userHealthProfiles`) är frivillig och användarsatt; appen visar icke-medicinsk disclaimer.
|
||||
- Recept-allergener fortsätter att härledas deterministiskt från `canonical_ingredients.allergens` (se Del 33 / allergen-invariant-testet).
|
||||
|
||||
### R5. i18n × 12
|
||||
|
||||
- Alla nya användarvisningstexter ska gå via `localeContext` och `memory-client/renderMemorySummary`.
|
||||
- Proveniessatser genereras med språkspecifika mallar (sv/en/es/it/de/fr/da/nb/fi/nl/pl/pt).
|
||||
- Förklaringar (`whySv`) ska ha motsvarande `whyEn`, `whyEs`, … eller genereras dynamiskt från mallar.
|
||||
|
||||
### R6. Proveniens
|
||||
- Varje personaliserat förslag ska kunna förklara *varför* det valdes.
|
||||
|
||||
- Varje personaliserat förslag ska kunna förklara _varför_ det valdes.
|
||||
- **Personaliseringstexter (proveniens, why, puffar) genereras från mallar med grundade fakta**, inte fri AI-text. Mallarna är översatta, skam- och fabriceringsfria per konstruktion.
|
||||
- Exempel på mallar (svenska):
|
||||
- `"För att du har berättat att du gillar {{cuisine}}."`
|
||||
@@ -104,6 +111,7 @@ Persona-chartern översätts till följande tekniska regler. Varje regel ska gå
|
||||
- Proveniens lagras/transporteras i `ScoredRecommendation.parts` + en ny valfri `provenance` array.
|
||||
|
||||
### R7. Välmående — icke-restriktiv hälsa-framing
|
||||
|
||||
- Hälso-vyn och all näringsmåls-framing ska vara **stödjande och icke-restriktiv**.
|
||||
- Tillåten copy: `"Passar ditt proteinmål"`, `"Bidrar till dina grönsaker i dag"`.
|
||||
- **Förbjuden copy:** `"Du har överskridit ditt kalorimål"`, `"Bara X kcal kvar"`, `"Begränsa dig"`, `"Undvik …"` eller annan negativ/restriktiv framing.
|
||||
@@ -117,7 +125,9 @@ Persona-chartern översätts till följande tekniska regler. Varje regel ska gå
|
||||
Ingen skiva får påbörjas förrän föregående är godkänd. Varje skiva levererar: kod + enhetstester + integrationstest mot staging-DB + dokumentationsuppdatering.
|
||||
|
||||
### Skiva S0 — förberedelse (inga användarfunktioner)
|
||||
|
||||
**Innehåll:**
|
||||
|
||||
- Checka in `docs/31-persona-nordstjärnan.md` och förankra R1–R7 explicit mot varje avsnitt i chartern.
|
||||
- Säkerställ att `userConsents` för `personalization` läses korrekt överallt. **Enkeltflaggat `personalization`-samtycke är OK för Spår 1** (ingen granulär uppdelning av personalisering ännu).
|
||||
- Uppdatera `packages/recommendation-engine/src/types.ts` med `provenance` och `memorySignalIds` på `ScoredRecommendation`.
|
||||
@@ -125,6 +135,7 @@ Ingen skiva får påbörjas förrän föregående är godkänd. Varje skiva leve
|
||||
- Sätt upp budgettak + mock-testmönster för `UPDATE_USER_MEMORY` (samma mönster som skanning: `GEMINI_DAILY_BUDGET_USD`, `MockAamosClient`, hermetiska tester).
|
||||
|
||||
**Testplan:**
|
||||
|
||||
- Typecheck och enhetstester gröna.
|
||||
- Fixturen kan skapa en användare med samtycken, minnen, smaksignaler och matlagningshistorik.
|
||||
- `UPDATE_USER_MEMORY`-anrop kan mockas och kostnadsräknas utan att träffa AAMOS.
|
||||
@@ -134,7 +145,9 @@ Ingen skiva får påbörjas förrän föregående är godkänd. Varje skiva leve
|
||||
---
|
||||
|
||||
### Skiva S1 — "Vad ska vi äta?" tonad mot lager + utgår-snart + smak + proveniens
|
||||
|
||||
**Innehåll:**
|
||||
|
||||
- Utöka `recommendation-engine/src/scoring.ts` med tre nya delpoäng som läser befintliga tabeller:
|
||||
- `memoryFit`: positiv boost om recept matchar `memory_items` (t.ex. favoritkök, gillade rätter).
|
||||
- `tasteFit`: boost/penalty från `taste_signals` per axel.
|
||||
@@ -145,6 +158,7 @@ Ingen skiva får påbörjas förrän föregående är godkänd. Varje skiva leve
|
||||
- Hård krav: `personalization`-samtycke måste vara `granted`; annars används nuvarande beteende.
|
||||
|
||||
**Testplan:**
|
||||
|
||||
- Enhetstest: en kandidat med `taste_signals` (positiv/negativ) får rätt `tasteFit`.
|
||||
- Enhetstest: `memoryFit` boostas av `verifiedByUser`-minne mer än `ai_inferred`.
|
||||
- Enhetstest: `cookingAssumptionFit` boostar recept som använder ingredienser med hög `averageEatenPortions`.
|
||||
@@ -156,7 +170,9 @@ Ingen skiva får påbörjas förrän föregående är godkänd. Varje skiva leve
|
||||
---
|
||||
|
||||
### Skiva S2 — Proaktiva puffar (opt-in)
|
||||
|
||||
**Innehåll:**
|
||||
|
||||
- Ny jobbtyp `SEND_PERSONALIZED_NUDGE` i worker (BullMQ-schemaläggare, t.ex. kl 11:00 på helger och kl 16:00 på vardagar).
|
||||
- Logik:
|
||||
1. Hämta hushåll med `personalization`-samtycke.
|
||||
@@ -167,6 +183,7 @@ Ingen skiva får påbörjas förrän föregående är godkänd. Varje skiva leve
|
||||
- Proveniens: "Gurkan håller på att bli slapp — här är ett recept du brukar gilla."
|
||||
|
||||
**Testplan:**
|
||||
|
||||
- Enhetstest: puff genereras endast om det finns utgående vara + matchande recept + samtycke.
|
||||
- Enhetstest: max 1 puff per hushåll per dag.
|
||||
- Integrationstest: jobbet kör mot staging-DB och skapar rätt notis.
|
||||
@@ -177,7 +194,9 @@ Ingen skiva får påbörjas förrän föregående är godkänd. Varje skiva leve
|
||||
---
|
||||
|
||||
### Skiva S3 — Minnes-ytan + personliga grundade svar
|
||||
|
||||
**Innehåll:**
|
||||
|
||||
- Förbättra `GET /v1/me/memory` så minnen visas med tydligare ursprung och användbara kategorier.
|
||||
- Koppla minnen till reella ytor: t.ex. "Du gillar italienskt kök" → visar vilka recept som påverkas.
|
||||
- Lägg till `GET /v1/me/memory/impact` som returnerar vilka rekommendationer som påverkas av ett specifikt minne (utan att exponera andras data).
|
||||
@@ -189,6 +208,7 @@ Ingen skiva får påbörjas förrän föregående är godkänd. Varje skiva leve
|
||||
- Avvisa förslag som saknar stöd i events.
|
||||
|
||||
**Testplan:**
|
||||
|
||||
- Enhetstest: `buildMemoryOverview` grupperar rätt och visar pausade poster.
|
||||
- Integrationstest: `PATCH /v1/me/memory/:id` ändrar `origin` till `user_stated`.
|
||||
- Mock-test: `UPDATE_USER_MEMORY` avvisar fabricerat minne (t.ex. påhittad favoriträtt).
|
||||
@@ -199,7 +219,9 @@ Ingen skiva får påbörjas förrän föregående är godkänd. Varje skiva leve
|
||||
---
|
||||
|
||||
### Skiva S4 — Smak / Hälsa / Lager-vyer i "Vad ska vi äta?"
|
||||
|
||||
**Innehåll:**
|
||||
|
||||
- Lägg till query-param `view=default|taste|health|pantry` på `GET /v1/recommendations/what-to-eat`.
|
||||
- Varje vy använder samma `rankAll` men med fördefinierade `ScoringWeights`:
|
||||
- **Taste**: högre `taste`, `craving`, `memoryFit`, `rating`; lägre `nutritionFit`.
|
||||
@@ -208,6 +230,7 @@ Ingen skiva får påbörjas förrän föregående är godkänd. Varje skiva leve
|
||||
- Spara vikterna i `packages/recommendation-engine/src/types.ts` som konstanter.
|
||||
|
||||
**Testplan:**
|
||||
|
||||
- Enhetstest: samma kandidater ger olika ordning beroende på vy.
|
||||
- Integrationstest: API accepterar `view`-parametern och returnerar skilda topplistor.
|
||||
- Regressionstest: `default` är oförändrad gentemot idag.
|
||||
@@ -217,7 +240,9 @@ Ingen skiva får påbörjas förrän föregående är godkänd. Varje skiva leve
|
||||
---
|
||||
|
||||
### Skiva S5 — Onboarding → minne + smaksignaler
|
||||
|
||||
**Innehåll:**
|
||||
|
||||
- När användaren slutför onboarding skrivs mål, allergier, favoritkök, undvikanden och spice max till:
|
||||
- `userPreferences` (redan idag).
|
||||
- `memory_items` som `user_stated`-poster.
|
||||
@@ -225,6 +250,7 @@ Ingen skiva får påbörjas förrän föregående är godkänd. Varje skiva leve
|
||||
- Visa i onboarding: "Detta sparas i "Vad plattformen vet om mig" och du kan ändra det när som helst."
|
||||
|
||||
**Testplan:**
|
||||
|
||||
- Integrationstest: efter onboarding finns motsvarande `memory_items` och `taste_signals`.
|
||||
- Enhetstest: onboarding-favoritkök skapar rätt `taste_signals`-poster.
|
||||
|
||||
@@ -233,7 +259,9 @@ Ingen skiva får påbörjas förrän föregående är godkänd. Varje skiva leve
|
||||
---
|
||||
|
||||
### Skiva S6 — i18n, proveniens och eval-pipeline
|
||||
|
||||
**Innehåll:**
|
||||
|
||||
- Säkerställ att alla nya provenienssträngar finns på 12 språk.
|
||||
- Uppdatera `memory-client/renderMemorySummary` med nya value-former.
|
||||
- Lägg till AI-eval-fall i `ai_eval_cases` för:
|
||||
@@ -243,6 +271,7 @@ Ingen skiva får påbörjas förrän föregående är godkänd. Varje skiva leve
|
||||
- Kör evals mot AAMOS i staging.
|
||||
|
||||
**Testplan:**
|
||||
|
||||
- Evals körbara via adminpanel eller worker-jobb.
|
||||
- Manuell granskning av 50 genererade `whySv` per språk.
|
||||
|
||||
@@ -256,18 +285,18 @@ Ingen skiva får påbörjas förrän föregående är godkänd. Varje skiva leve
|
||||
|
||||
## 5. RISKER + hur varje charter-regel testas
|
||||
|
||||
| Risk | Mitigering | Test/verifiering |
|
||||
| --- | --- | --- |
|
||||
| Risk | Mitigering | Test/verifiering |
|
||||
| ----------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **AI fabricerar minnen eller påhittar användarpreferenser** | `UPDATE_USER_MEMORY` tar bara events som input; output valideras mot Zod; `ai_inferred`-poster visas med tydlig etikett; användaren kan radera/pausa. | Mock-test där AAMOS föreslår ett minne som inte stöds av events → avvisas. Invariant: alla `memory_items` måste ha `origin` satt. |
|
||||
| **Personliga data läcker till andra hushållsmedlemmar** | `userHealthProfiles`, `memory_items`, `taste_signals` är användarsatta; hushållsaggregering är begränsad till allergener/undvikanden/spice max. | Integrationstest: medlem A kan inte läsa medlem B:s `memory_items` eller hälsoprofil. |
|
||||
| **Skam-ton eller värderande copy** | Förbudslista + språkgranskning i `explain.ts` och nudge-generering. | Enhetstest som skannar 100+ genererade förklaringar mot förbudslista; eval-korpus med skam-exempel. |
|
||||
| **Användaren tappar kontroll över minnet** | Full CRUD + paus + radera allt; alla nya minnen kommer från användaren eller observerade events. | Manuell test av "Vad plattformen vet om mig"-flödet; GDPR-raderingstest. |
|
||||
| **Personalisering körs utan samtycke** | Hård check på `userConsents.kind='personalization' AND status='granted'` i API och worker. | Integrationstest: utan samtycke fallback till opersonligt beteende; inga `memory_items` skrivs. |
|
||||
| **Allergener eller hälsa härleds felaktigt av AI** | Allergener kommer från användarpreferenser + canonical-ingredienser; hälsa är användarsatt. AI används aldrig för säkerhetsfiltrering. | Återanvänd befintliga säkerhetstester (`recipe-engine/test/safety.test.ts`) och allergen-invariant-testet. |
|
||||
| **i18n-försämring / fel språk i proveniens** | Alla nya texter via `localeContext` och språkspecifika mallar; eval per språk. | Manuell granskning per språk; automatisk check att `whySv` inte blandas med `whyEn`. |
|
||||
| **Prestandaförsämring i "Vad ska vi äta?"** | Läsning av `memory_items` och `taste_signals` är indexerad; begränsa till aktuell användare/hushåll; cache lämpliga aggregeringar. | Lasttest: 200 anrop/minut under 5 minuter; jämför svarstid före/efter. |
|
||||
| **Cirkulära beroenden** | Alla ändringar i `recommendation-engine` får inte bero på `apps/api`; `memory-client` får inte bero på `recommendation-engine`. | `pnpm typecheck` och dependency-graf (turbo) grön. |
|
||||
| **Seed/reseed bryter personliga data** | Inga personliga data i seed; seed skrivs aldrig över användardata. | Seed-test: efter `db:test-setup` finns inga `memory_items` eller `taste_signals` för testanvändare. |
|
||||
| **Personliga data läcker till andra hushållsmedlemmar** | `userHealthProfiles`, `memory_items`, `taste_signals` är användarsatta; hushållsaggregering är begränsad till allergener/undvikanden/spice max. | Integrationstest: medlem A kan inte läsa medlem B:s `memory_items` eller hälsoprofil. |
|
||||
| **Skam-ton eller värderande copy** | Förbudslista + språkgranskning i `explain.ts` och nudge-generering. | Enhetstest som skannar 100+ genererade förklaringar mot förbudslista; eval-korpus med skam-exempel. |
|
||||
| **Användaren tappar kontroll över minnet** | Full CRUD + paus + radera allt; alla nya minnen kommer från användaren eller observerade events. | Manuell test av "Vad plattformen vet om mig"-flödet; GDPR-raderingstest. |
|
||||
| **Personalisering körs utan samtycke** | Hård check på `userConsents.kind='personalization' AND status='granted'` i API och worker. | Integrationstest: utan samtycke fallback till opersonligt beteende; inga `memory_items` skrivs. |
|
||||
| **Allergener eller hälsa härleds felaktigt av AI** | Allergener kommer från användarpreferenser + canonical-ingredienser; hälsa är användarsatt. AI används aldrig för säkerhetsfiltrering. | Återanvänd befintliga säkerhetstester (`recipe-engine/test/safety.test.ts`) och allergen-invariant-testet. |
|
||||
| **i18n-försämring / fel språk i proveniens** | Alla nya texter via `localeContext` och språkspecifika mallar; eval per språk. | Manuell granskning per språk; automatisk check att `whySv` inte blandas med `whyEn`. |
|
||||
| **Prestandaförsämring i "Vad ska vi äta?"** | Läsning av `memory_items` och `taste_signals` är indexerad; begränsa till aktuell användare/hushåll; cache lämpliga aggregeringar. | Lasttest: 200 anrop/minut under 5 minuter; jämför svarstid före/efter. |
|
||||
| **Cirkulära beroenden** | Alla ändringar i `recommendation-engine` får inte bero på `apps/api`; `memory-client` får inte bero på `recommendation-engine`. | `pnpm typecheck` och dependency-graf (turbo) grön. |
|
||||
| **Seed/reseed bryter personliga data** | Inga personliga data i seed; seed skrivs aldrig över användardata. | Seed-test: efter `db:test-setup` finns inga `memory_items` eller `taste_signals` för testanvändare. |
|
||||
|
||||
---
|
||||
|
||||
@@ -289,9 +318,9 @@ Ingen skiva får påbörjas förrän föregående är godkänd. Varje skiva leve
|
||||
|
||||
## Revisionslogg
|
||||
|
||||
| Datum | Revision | Författare |
|
||||
| --- | --- | --- |
|
||||
| 2026-08-10 | Initial plan för granskning | Sven |
|
||||
| 2026-08-10 | Godkänd med justeringar: R7 välmående, mall-baserade texter, ai_inferred-konfidens, UPDATE_USER_MEMORY-budget, docs/31 incheckad | Sven / Johan |
|
||||
| 2026-08-10 | S1 implementerat: "Vad ska vi äta?" med provenansmallar, samtyckesgrind, personliga delpoäng, integrationstester; commit `0885f5b` | Sven / Johan |
|
||||
| Datum | Revision | Författare |
|
||||
| ---------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------ |
|
||||
| 2026-08-10 | Initial plan för granskning | Sven |
|
||||
| 2026-08-10 | Godkänd med justeringar: R7 välmående, mall-baserade texter, ai_inferred-konfidens, UPDATE_USER_MEMORY-budget, docs/31 incheckad | Sven / Johan |
|
||||
| 2026-08-10 | S1 implementerat: "Vad ska vi äta?" med provenansmallar, samtyckesgrind, personliga delpoäng, integrationstester; commit `0885f5b` | Sven / Johan |
|
||||
| 2026-08-10 | S2 implementerat: proaktiva puffar (expiring-ingredient) med dubbelgrind, 12-språksmallar, frekvens-/duplicate-gate, UTC-matte, integrationstester; commit `f494b18` | Sven / Johan |
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
## Hermetiska testregler
|
||||
|
||||
Alla integrationstester ska vara hermetiska:
|
||||
|
||||
- De får inte bero på innehållet i `.env` (värden som `AAMOS_MODE`, `EMAIL_MODE`, `S3_MODE` skrivs över i `setup-env.ts`).
|
||||
- De får inte bero på förkonfigurerad konfiguration eller externa tjänster.
|
||||
- De får inte bero på databasinnehåll som skapats utanför testet (exempelvis seedade recept från en tidigare körning). Testkörningen ansvarar själv för att testdatabasen är migrerad **och** seedad innan testerna startar; seeden är idempotent.
|
||||
@@ -14,27 +15,29 @@ Alla integrationstester ska vara hermetiska:
|
||||
|
||||
## 1. REUSE/GAP-matris mot befintligt matlagningsflöde
|
||||
|
||||
| §6-krav / område | Befintlig implementation | Status | Rekommenderad åtgärd |
|
||||
|------------------|-------------------------|--------|----------------------|
|
||||
| **Lifecycle PLANNED → STARTED → COMPLETED/CANCELLED** | Saknas. `recipe_cooks` är en enda rad skapad vid `/cook` ("COMPLETED" implicit). Ingen separat sessionstabell. | **Saknas** | Inför `cooking_sessions` (se nedan). Behåll `recipe_cooks` som aggregerad historik för rekommendationer/variation. |
|
||||
| **FEFO-avdrag från inventory (§6)** | `/v1/recipes/:id/cook` kör `allocateFefo()` och skriver `inventory_transactions.type = 'cook_use'` med `refType='recipe_cook'`. | **Finns** | Återanvänd oförändrad. Lägg till `cookingSessionId` i transaktionen för att kunna ångra en hel session. |
|
||||
| **portioner / portionsCooked** | `cookRecipeInputSchema.portionsCooked` (1–24). Skalning i receptdetalj. | **Finns** | Återanvänd. |
|
||||
| **Individuell förbrukning per ätare** | `eaters[]` med `portionFraction` loggas i `meals`. | **Finns** | Återanvänd. |
|
||||
| **Minimala efterfrågor efteråt (§6.2)** | Efterflödet i `cooking/[id].tsx` frågar idag bara antal matlådor. Inga profiler för "hur mycket blev det kvar?" | **Delvis** | Inför 2–3 snabba frågor som extraheras till `cooking_sessions.actualPortionsEaten`, `leftoverEstimate` och `cookingSessionId` på meals. |
|
||||
| **Partiell förbrukning (§6.3)** | Saknas. `/cook` drar allt på en gång. Användaren kan efteråt manuellt justera via `/v1/inventory/items/:id/transactions`. | **Saknas** | Låt `/cook` skapa `cook_use`-transaktioner med `undoUntil = cookingSessionId`; vid efterfrågan skriv reverseringstransaktioner för det som inte åts. |
|
||||
| **undo_until (ångra hel session)** | Transaktionsmodellen stöder bara enskilda nya transaktioner. Ingen koppling tillbaka till en session. | **Saknas** | Lägg `cookingSessionId` på `inventory_transactions`, `meals`, `meal_boxes` och tillåt `POST /v1/cooking-sessions/:id/undo`. |
|
||||
| **Rester till BEFINTLIGA meal_boxes (§6.4)** | `meal_boxes` finns. `/cook` kan skapa EN ny matlåda per tillfälle. | **Delvis** | Utöka så att rester kan läggas i en **befintlig** `meal_box` (matcha på recipeId + hushåll + `status=available`) istället för att alltid skapa ny. |
|
||||
| **Anti-dubblettkartan: ingen prepared_food_batches-tabell** | Finns ingen sådan tabell. | **OK** | Behåll. Använd `meal_boxes` som enda restbehållare. Motivering: samma koncept (portioner kvar, ät-senast-datum, näringssnapshot), sparar dubblettlogik. |
|
||||
| **Näringssnapshot på rester** | `meal_boxes.nutritionPerPortion` sparas vid skapande. | **Finns** | Återanvänd. |
|
||||
| **recommendations läser recipe_cooks** | `recommendations.ts` gör `SELECT recipeId, max(cookedAt) FROM recipe_cooks GROUP BY recipeId`. | **Finns** | **Risk:** om vi ändrar semantiken i `recipe_cooks` måste denna fråga vara bakåtkompatibel. Rekommendation ska fortsätta se sista tillfället ett recept lagades. |
|
||||
| **Testprotokoll steg 5** | "Laga nu → cooking mode → klart → logga → ingredienser dras från lagret; måltiden syns i Min dag". | **Finns** | Måste förbli sant. Nytt flöde får inte kräva fler steg än idag; nya frågor ska vara valbara/skipbara. |
|
||||
| **Testprotokoll steg 8** | "Matlådor: laga recept med portioner till lådor → lådan syns med ät-senast-datum". | **Finns** | Måste förbli sant. Nya rest-flödet ska INTE bryta skapandet av nya lådor. |
|
||||
| **Alla strängar ×12** | i18next + 12 locales. | **Finns** | Lägg till nya nycklar i alla 12 språk. |
|
||||
| **Hermetiska tester** | `apps/api/test/*` använder `TEST_DATABASE_URL` och setup-env. | **Finns** | Fortsätt samma mönster. |
|
||||
| **Mjölkprincipen i texter** | Alla UI-strängar undviker "kassera automatiskt" och skiljer bäst-före/sista-förbrukningsdag. | **Finns** | Fortsätt. Inga texter får garantera matsäkerhet. |
|
||||
| §6-krav / område | Befintlig implementation | Status | Rekommenderad åtgärd |
|
||||
| ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **Lifecycle PLANNED → STARTED → COMPLETED/CANCELLED** | Saknas. `recipe_cooks` är en enda rad skapad vid `/cook` ("COMPLETED" implicit). Ingen separat sessionstabell. | **Saknas** | Inför `cooking_sessions` (se nedan). Behåll `recipe_cooks` som aggregerad historik för rekommendationer/variation. |
|
||||
| **FEFO-avdrag från inventory (§6)** | `/v1/recipes/:id/cook` kör `allocateFefo()` och skriver `inventory_transactions.type = 'cook_use'` med `refType='recipe_cook'`. | **Finns** | Återanvänd oförändrad. Lägg till `cookingSessionId` i transaktionen för att kunna ångra en hel session. |
|
||||
| **portioner / portionsCooked** | `cookRecipeInputSchema.portionsCooked` (1–24). Skalning i receptdetalj. | **Finns** | Återanvänd. |
|
||||
| **Individuell förbrukning per ätare** | `eaters[]` med `portionFraction` loggas i `meals`. | **Finns** | Återanvänd. |
|
||||
| **Minimala efterfrågor efteråt (§6.2)** | Efterflödet i `cooking/[id].tsx` frågar idag bara antal matlådor. Inga profiler för "hur mycket blev det kvar?" | **Delvis** | Inför 2–3 snabba frågor som extraheras till `cooking_sessions.actualPortionsEaten`, `leftoverEstimate` och `cookingSessionId` på meals. |
|
||||
| **Partiell förbrukning (§6.3)** | Saknas. `/cook` drar allt på en gång. Användaren kan efteråt manuellt justera via `/v1/inventory/items/:id/transactions`. | **Saknas** | Låt `/cook` skapa `cook_use`-transaktioner med `undoUntil = cookingSessionId`; vid efterfrågan skriv reverseringstransaktioner för det som inte åts. |
|
||||
| **undo_until (ångra hel session)** | Transaktionsmodellen stöder bara enskilda nya transaktioner. Ingen koppling tillbaka till en session. | **Saknas** | Lägg `cookingSessionId` på `inventory_transactions`, `meals`, `meal_boxes` och tillåt `POST /v1/cooking-sessions/:id/undo`. |
|
||||
| **Rester till BEFINTLIGA meal_boxes (§6.4)** | `meal_boxes` finns. `/cook` kan skapa EN ny matlåda per tillfälle. | **Delvis** | Utöka så att rester kan läggas i en **befintlig** `meal_box` (matcha på recipeId + hushåll + `status=available`) istället för att alltid skapa ny. |
|
||||
| **Anti-dubblettkartan: ingen prepared_food_batches-tabell** | Finns ingen sådan tabell. | **OK** | Behåll. Använd `meal_boxes` som enda restbehållare. Motivering: samma koncept (portioner kvar, ät-senast-datum, näringssnapshot), sparar dubblettlogik. |
|
||||
| **Näringssnapshot på rester** | `meal_boxes.nutritionPerPortion` sparas vid skapande. | **Finns** | Återanvänd. |
|
||||
| **recommendations läser recipe_cooks** | `recommendations.ts` gör `SELECT recipeId, max(cookedAt) FROM recipe_cooks GROUP BY recipeId`. | **Finns** | **Risk:** om vi ändrar semantiken i `recipe_cooks` måste denna fråga vara bakåtkompatibel. Rekommendation ska fortsätta se sista tillfället ett recept lagades. |
|
||||
| **Testprotokoll steg 5** | "Laga nu → cooking mode → klart → logga → ingredienser dras från lagret; måltiden syns i Min dag". | **Finns** | Måste förbli sant. Nytt flöde får inte kräva fler steg än idag; nya frågor ska vara valbara/skipbara. |
|
||||
| **Testprotokoll steg 8** | "Matlådor: laga recept med portioner till lådor → lådan syns med ät-senast-datum". | **Finns** | Måste förbli sant. Nya rest-flödet ska INTE bryta skapandet av nya lådor. |
|
||||
| **Alla strängar ×12** | i18next + 12 locales. | **Finns** | Lägg till nya nycklar i alla 12 språk. |
|
||||
| **Hermetiska tester** | `apps/api/test/*` använder `TEST_DATABASE_URL` och setup-env. | **Finns** | Fortsätt samma mönster. |
|
||||
| **Mjölkprincipen i texter** | Alla UI-strängar undviker "kassera automatiskt" och skiljer bäst-före/sista-förbrukningsdag. | **Finns** | Fortsätt. Inga texter får garantera matsäkerhet. |
|
||||
|
||||
### Slutsats av matrisen
|
||||
|
||||
Cooking Sessions är en **utbyggnad**, inte ett parallellsystem:
|
||||
|
||||
- **Kärnan** (`recipe_cooks`, FEFO-avdrag, meals, meal_boxes) finns redan.
|
||||
- **Det som saknas** är en session-boog (`cooking_sessions`) som knyter samman påbörjat/klart/ångrat, plus möjligheten att (a) skjuta upp delar av förbrukningen till efterfrågan och (b) lägga rester i befintliga matlådor.
|
||||
|
||||
@@ -43,6 +46,7 @@ Cooking Sessions är en **utbyggnad**, inte ett parallellsystem:
|
||||
## 2. Anti-dubblettkartan – varför INGEN `prepared_food_batches`-tabell
|
||||
|
||||
Befintlig `meal_boxes` kan redan representera:
|
||||
|
||||
- Portioner som återstår
|
||||
- Receptursprung (`recipeId`)
|
||||
- Näringsvärden per portion (`nutritionPerPortion`)
|
||||
@@ -52,6 +56,7 @@ Befintlig `meal_boxes` kan redan representera:
|
||||
- Reservering för användare
|
||||
|
||||
Att lägga till `prepared_food_batches` skulle skapa:
|
||||
|
||||
- Dubbla konsumtions-endpoints
|
||||
- Dubbla "ät senast"-logiker
|
||||
- Dubbla måltidsloggningsvägar
|
||||
@@ -65,9 +70,11 @@ Att lägga till `prepared_food_batches` skulle skapa:
|
||||
## 3. STEGPLAN
|
||||
|
||||
### Steg 3a – Lifecycle + planned usage ovanpå befintlig `/cook`
|
||||
|
||||
**Mål:** En cooking session kan startas, slutföras eller avbrytas utan att bryta dagens `/cook`-beteende.
|
||||
|
||||
**Ändringar:**
|
||||
|
||||
1. Ny tabell `cooking_sessions`:
|
||||
- `id`, `recipeId`, `householdId`, `startedByUserId`
|
||||
- `status`: `planned`, `started`, `completed`, `cancelled`
|
||||
@@ -86,6 +93,7 @@ Att lägga till `prepared_food_batches` skulle skapa:
|
||||
4. **Bakåtkompatibilitet:** Behåll `POST /v1/recipes/:id/cook` som ett kortkommando som skapar session + complete i samma anrop (så testprotokoll steg 5 fortsätter fungera).
|
||||
|
||||
**Berörda filer:**
|
||||
|
||||
- `packages/database/src/schema/recipes.ts` (cooking_sessions, recipe_cooks-kolumn)
|
||||
- `packages/database/src/schema/meals.ts` (meal_boxes-kolumn)
|
||||
- `packages/database/src/schema/inventory.ts` (inventory_transactions-kolumn)
|
||||
@@ -100,6 +108,7 @@ Att lägga till `prepared_food_batches` skulle skapa:
|
||||
**Migration:** 0011_cooking_sessions.sql
|
||||
|
||||
**Testplan:**
|
||||
|
||||
- POST /cook/start → status started
|
||||
- POST /cooking-sessions/:id/complete → samma resultat som idag: inventory dras, meals skapas, mealBox skapas
|
||||
- POST /recipes/:id/cook (gammalt) fortfarande grönt
|
||||
@@ -108,9 +117,11 @@ Att lägga till `prepared_food_batches` skulle skapa:
|
||||
---
|
||||
|
||||
### Steg 3b – Minimala efterfrågor §6.2 + antagandeprofiler
|
||||
|
||||
**Mål:** Efter "klart" ställs max 2–3 snabba frågor. Appen kan fylla i svar automatiskt från profiler (t.ex. "vi brukar äta alla portioner" / "vi brukar ha 2 lådor över").
|
||||
|
||||
**Ändringar:**
|
||||
|
||||
1. Lägg kolumner på `cooking_sessions`:
|
||||
- `actualPortionsEaten` (nullable int)
|
||||
- `leftoverEstimatePortions` (nullable int)
|
||||
@@ -124,6 +135,7 @@ Att lägga till `prepared_food_batches` skulle skapa:
|
||||
- Frågorna ska kunna skippas med ett tryck (default antas).
|
||||
|
||||
**Berörda filer:**
|
||||
|
||||
- `packages/database/src/schema/recipes.ts` (kolumner)
|
||||
- `packages/validation/src/recipes.ts`
|
||||
- `apps/api/src/routes/cooking-sessions.ts`
|
||||
@@ -133,6 +145,7 @@ Att lägga till `prepared_food_batches` skulle skapa:
|
||||
**Migration:** 0012_cooking_questions.sql
|
||||
|
||||
**Testplan:**
|
||||
|
||||
- Profil sparas och återanvänds nästa session
|
||||
- Skippa frågor → defaults används
|
||||
- Validering: `actualPortionsEaten + leftoverEstimatePortions ≤ portionsCooked`
|
||||
@@ -140,9 +153,11 @@ Att lägga till `prepared_food_batches` skulle skapa:
|
||||
---
|
||||
|
||||
### Steg 3c – Partiell förbrukning §6.3 + undo_until
|
||||
|
||||
**Mål:** Om användaren säger att de bara åt 3 av 4 portioner, ska FEFO-avdraget justeras så att motsvarande råvaror återstår. En completed session kan ångras inom 24 h via append-only reverseringstransaktioner.
|
||||
|
||||
**Ändringar:**
|
||||
|
||||
1. Vid complete: använd `actualPortionsEaten` för att räkna om råvaror.
|
||||
- Exempel: 4 planerade portioner → 3 ätna = använd 75 % av varje ingrediens.
|
||||
- Om `actualPortionsEaten` är null, använd `portionsCooked` (dagens beteende).
|
||||
@@ -165,6 +180,7 @@ Att lägga till `prepared_food_batches` skulle skapa:
|
||||
5. Lägg `cookingSessionId` i inventory item detail så användaren kan ångra enskilda transaktioner därifrån också.
|
||||
|
||||
**Berörda filer:**
|
||||
|
||||
- `apps/api/src/lib/cooking.ts` (`completeCookingSession`, `undoCookingSession`)
|
||||
- `apps/api/src/routes/cooking-sessions.ts`
|
||||
- `apps/api/src/lib/i18n.ts`
|
||||
@@ -177,6 +193,7 @@ Att lägga till `prepared_food_batches` skulle skapa:
|
||||
**Migration:** 0014_expand_event_types.sql (nya domänhändelser i `event_type`-enum)
|
||||
|
||||
**Testplan:**
|
||||
|
||||
- Laga 4 portioner, ät 3 → lager innehåller 25 % kvar av varje ingrediens
|
||||
- Undo → allt återställs
|
||||
- Invariant: `computeBalance(transaktioner) === item.quantity` efter (a) complete, (b) undo, (c) undo + ny complete
|
||||
@@ -187,9 +204,11 @@ Att lägga till `prepared_food_batches` skulle skapa:
|
||||
---
|
||||
|
||||
### Steg 3d – Rester §6.4 via befintliga meal_boxes
|
||||
|
||||
**Mål:** Efterfrågade rester hamnar i antingen (1) befintlig available matlåda med samma `recipeId`, eller (2) ny matlåda.
|
||||
|
||||
**Ändringar:**
|
||||
|
||||
1. Vid complete, om `leftoverEstimatePortions > 0`:
|
||||
- Sök `meal_boxes` där `recipeId = recipe.id`, `householdId`, `status = 'available'`, `frozen = false` (eller matcha valt frystillstånd), sorterat på `recommendedUseBy`.
|
||||
- Om träff: öka `portions` och `portionsRemaining` med `leftoverEstimatePortions`, uppdatera `recommendedUseBy` till det tidigare av de två datumen (mjölkprincipen: behåll kortaste hållbarhet).
|
||||
@@ -198,6 +217,7 @@ Att lägga till `prepared_food_batches` skulle skapa:
|
||||
3. Frysval: om användaren väljer frys, skapa/utöka fryst matlåda med `frozen=true`, useBy=90 dagar.
|
||||
|
||||
**Berörda filer:**
|
||||
|
||||
- `packages/database/src/schema/meals.ts`
|
||||
- `apps/api/src/routes/recipes.ts` eller `cooking-sessions.ts`
|
||||
- `apps/mobile/src/app/cooking/[id].tsx`
|
||||
@@ -206,6 +226,7 @@ Att lägga till `prepared_food_batches` skulle skapa:
|
||||
**Migration:** 0013_meal_box_leftover_source.sql
|
||||
|
||||
**Testplan:**
|
||||
|
||||
- Två cooking sessions med samma recept → en matlåda har ökade portioner
|
||||
- Frysta rester → separat fryst låda
|
||||
- Ingen dubblett i rekommendationer: matlådor-först-sektionen räknar fortfarande `portionsRemaining > 0`
|
||||
@@ -214,29 +235,29 @@ Att lägga till `prepared_food_batches` skulle skapa:
|
||||
|
||||
## 4. RISKER
|
||||
|
||||
| Risk | Påverkan | Minskning |
|
||||
|------|----------|-----------|
|
||||
| `recommendations.ts` använder `recipe_cooks` för "senast lagat". Om `cookedAt` ändras semantik bryts variation. | Medel | Behåll `recipe_cooks.cookedAt` som tidpunkt för färdig session. Lägg separata tidsstämplar på `cooking_sessions`. |
|
||||
| Testprotokoll steg 5 kräver att "Laga nu → klart → logga" fortsätter fungera utan extra kranar. | Hög | Behåll `/recipes/:id/cook` som kortkommando. Nya frågor ska vara skipbara med tydliga defaults. |
|
||||
| Testprotokoll steg 8 kräver att nya matlådor skapas. | Hög | Nya rest-flödet ska inte blockera skapandet; befintlig-låda-utökning är opt-in. |
|
||||
| Partiell förbrukning kan leda till brutna invarianter om omräkning blir fel. | Hög | Invariantstest efter varje complete/undo; använd samma enhetskonvertering som FEFO. |
|
||||
| Undo kan radera måltider som andra flöden refererar till. | Medel | Använd soft-delete (`deletedAt` på meals) eller markera som `cancelled` istället för hård radering. |
|
||||
| UI-blödning: nya frågor kan kännas som ett formulär. | Medel | Designa som kort med +/- och "kom ihåg mitt val". |
|
||||
| Matsäkerhetstexter kan osynligt bli för skarpa. | Medel | Varje ny i18n-nyckel granskas: aldrig "säkert att äta", alltid "lukta och smaka" / "rekommenderas före". |
|
||||
| Hermetiska tester kan läcka `.env`. | Låg | Fortsätt `setup-env.ts` + `vitest.config.ts`. |
|
||||
| Risk | Påverkan | Minskning |
|
||||
| --------------------------------------------------------------------------------------------------------------- | -------- | ----------------------------------------------------------------------------------------------------------------- |
|
||||
| `recommendations.ts` använder `recipe_cooks` för "senast lagat". Om `cookedAt` ändras semantik bryts variation. | Medel | Behåll `recipe_cooks.cookedAt` som tidpunkt för färdig session. Lägg separata tidsstämplar på `cooking_sessions`. |
|
||||
| Testprotokoll steg 5 kräver att "Laga nu → klart → logga" fortsätter fungera utan extra kranar. | Hög | Behåll `/recipes/:id/cook` som kortkommando. Nya frågor ska vara skipbara med tydliga defaults. |
|
||||
| Testprotokoll steg 8 kräver att nya matlådor skapas. | Hög | Nya rest-flödet ska inte blockera skapandet; befintlig-låda-utökning är opt-in. |
|
||||
| Partiell förbrukning kan leda till brutna invarianter om omräkning blir fel. | Hög | Invariantstest efter varje complete/undo; använd samma enhetskonvertering som FEFO. |
|
||||
| Undo kan radera måltider som andra flöden refererar till. | Medel | Använd soft-delete (`deletedAt` på meals) eller markera som `cancelled` istället för hård radering. |
|
||||
| UI-blödning: nya frågor kan kännas som ett formulär. | Medel | Designa som kort med +/- och "kom ihåg mitt val". |
|
||||
| Matsäkerhetstexter kan osynligt bli för skarpa. | Medel | Varje ny i18n-nyckel granskas: aldrig "säkert att äta", alltid "lukta och smaka" / "rekommenderas före". |
|
||||
| Hermetiska tester kan läcka `.env`. | Låg | Fortsätt `setup-env.ts` + `vitest.config.ts`. |
|
||||
|
||||
---
|
||||
|
||||
## 5. Översiktliga berörda tabeller
|
||||
|
||||
| Tabell | Förändring |
|
||||
|--------|------------|
|
||||
| `cooking_sessions` | **Ny** |
|
||||
| `recipe_cooks` | `cookingSessionId` nullable |
|
||||
| `inventory_transactions` | `cookingSessionId` nullable |
|
||||
| `meals` | `cookingSessionId` nullable, ev. `deletedAt` |
|
||||
| `meal_boxes` | `cookingSessionId` nullable, `leftoverSource` text |
|
||||
| `users` / `households` | post-cook-profil jsonb (steg 3b) |
|
||||
| Tabell | Förändring |
|
||||
| ------------------------ | -------------------------------------------------- |
|
||||
| `cooking_sessions` | **Ny** |
|
||||
| `recipe_cooks` | `cookingSessionId` nullable |
|
||||
| `inventory_transactions` | `cookingSessionId` nullable |
|
||||
| `meals` | `cookingSessionId` nullable, ev. `deletedAt` |
|
||||
| `meal_boxes` | `cookingSessionId` nullable, `leftoverSource` text |
|
||||
| `users` / `households` | post-cook-profil jsonb (steg 3b) |
|
||||
|
||||
---
|
||||
|
||||
@@ -251,15 +272,18 @@ Per steg: `brand-guard`, `typecheck`, `test`, `build`, `first-deploy staging`, p
|
||||
|
||||
---
|
||||
|
||||
*Rapport färdig. Inväntar godkännande innan kod påbörjas.*
|
||||
_Rapport färdig. Inväntar godkännande innan kod påbörjas._
|
||||
|
||||
## 7. Steg 3d – Restflöde till matlådor: merge, undo och mjölkprincipen
|
||||
|
||||
### 7.1 Semantik: alla rester blir matlådor
|
||||
|
||||
När en cooking session avslutas skapas eller uppdateras alltid en `meal_box` för `leftoverEstimatePortions` (default = `mealBoxPortions` om användaren inte anger något annat). `mealBoxPortions` är endast en validering: `leftoverEstimatePortions >= mealBoxPortions`. Det är `leftoverEstimatePortions` som driver boxens totala portionsantal.
|
||||
|
||||
### 7.2 Merge-regel
|
||||
|
||||
En befintlig `meal_box` får rester tillagda endast om:
|
||||
|
||||
- samma `recipeId`
|
||||
- samma `cookedAt`-datum (UTC-datum, inte timestamp)
|
||||
- samma `frozen`-status
|
||||
@@ -270,12 +294,15 @@ Om ingen match finns skapas en ny `meal_box`.
|
||||
`recommendedUseBy` sätts från sessionens lagningsdatum + 3 dagar (kyl) eller + 90 dagar (frys). Vid merge sätts `recommendedUseBy` till det tidigare av de två datumen.
|
||||
|
||||
### 7.3 Spårbarhet och deterministisk undo
|
||||
|
||||
Vid complete sparas `mealBoxMutations` JSONB på `cooking_sessions`. Varje mutation innehåller:
|
||||
|
||||
- `mealBoxId`: UUID för den berörda boxen
|
||||
- `deltaPortions`: hur många portioner som lades till
|
||||
- `frozen`: boxens frystillstånd (redundans för felsäkerhet)
|
||||
|
||||
Undo av en session:
|
||||
|
||||
1. Itererar `mealBoxMutations`.
|
||||
2. För varje mutation minskas både `portions` och `portionsRemaining` med `deltaPortions`.
|
||||
3. Om `portions` blir 0 sätts `status = 'discarded'` och `portionsRemaining = 0`.
|
||||
@@ -284,16 +311,20 @@ Undo av en session:
|
||||
6. Sessionens status sätts till `undone`.
|
||||
|
||||
### 7.4 Mjölkprincipen
|
||||
|
||||
`recommendedUseBy` är en kvalitetssignal. UI använder texter som "Ät senast" och "lukta och smaka". Det finns ingen automatisk kassering när datum passeras; användaren avgör alltid med sinnena.
|
||||
|
||||
### 7.5 UI: undo-knapp
|
||||
|
||||
- **Efter matlagning** (`cooking/[id].tsx`): efter att användaren tryckt "Klart" visas en skärm med en "Ångra"-knapp inom 24 h.
|
||||
- **Matlådelistan** (`meal-boxes.tsx`): varje box som har en `cookingSessionId` och är skapad inom de senaste 24 timmarna visar en "Ångra"-knapp. Knappen anropar `POST /v1/cooking-sessions/:cookingSessionId/undo`.
|
||||
- Felfall visar serverns lokaliserade felmeddelande (aldrig hårdkodade strängar).
|
||||
- i18n-nycklar: `common.undo`, `mealbox.undoConfirmTitle`, `mealbox.undoConfirmBody`, `mealbox.undoSuccess`.
|
||||
|
||||
### 7.6 Wrapper-beteende (OBLIGATORISK PUNKT 1)
|
||||
|
||||
`completeCookingSession()` i `apps/api/src/lib/cooking.ts` är den enda vägen för både legacy `/v1/recipes/:id/cook` och explicit `/v1/cooking-sessions/:id/complete`. I `c1ab2c8` stängdes hålet där legacy `/cook` tidigare lämnade fälten null. Wrappern:
|
||||
|
||||
1. Beräknar `actualPortionsEaten` om det saknas: `Math.max(0, plannedPortions - mealBoxPortions)`.
|
||||
2. Beräknar `leftoverEstimatePortions` om det saknas: samma som `mealBoxPortions`.
|
||||
3. Skickar båda värdena explicit in i `completeCookingSessionCore`.
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
## Hermetiska testregler (samma som Fas 3)
|
||||
|
||||
Alla integrationstester ska vara hermetiska:
|
||||
|
||||
- De får inte bero på innehållet i `.env` (värden som `AAMOS_MODE`, `EMAIL_MODE`, `S3_MODE` skrivs över i `setup-env.ts`).
|
||||
- De får inte bero på förkonfigurerad konfiguration eller externa tjänster.
|
||||
- De får inte bero på databasinnehåll som skapats utanför testet. Testkörningen ansvarar själv för att testdatabasen är migrerad **och** seedad innan testerna startar; seeden är idempotent.
|
||||
@@ -18,26 +19,28 @@ Alla integrationstester ska vara hermetiska:
|
||||
|
||||
## 1. REUSE/GAP-matris mot befintligt hushållsflöde
|
||||
|
||||
| §7-krav / område | Befintlig implementation | Status | Rekommenderad åtgärd |
|
||||
|------------------|-------------------------|--------|----------------------|
|
||||
| **Hushållsgrundstruktur** | Tabellerna `households` och `household_members` finns. `households.invite_code` är unik. Roller: `owner`, `adult`, `member`, `child`. | **Finns** | Återanvänd oförändrat. Utöka med tidsbunden inbjudanslänk, inte ny inbjudningsmodell. |
|
||||
| **Gå med via kod** | `POST /v1/households/join` med `{ inviteCode }`. Nya medlemmar får roll `adult`. Plan-gräns `maxHouseholdMembers` kontrolleras. | **Finns** | Återanvänd. Utöka med djuplänk + QR så att koden kan fyllas i automatiskt, men backend-logiken är densamma. |
|
||||
| **Medlemslista** | `GET /v1/households/:id` returnerar medlemmar med `userId`, `displayName`, `role`, `portionFactor`, `joinedAt`. | **Finns** | Återanvänd. Lägg till UI för att visa/ändra. |
|
||||
| **Ändra roll** | `PATCH /v1/households/:id/members/:userId` med `role`. Endast `owner` får ändra roller. | **Finns** | Återanvänd. Säkerställ att `child` inte kan upphöjas till `owner`. |
|
||||
| **Ta bort medlem** | `DELETE /v1/households/:id/members/:userId`. Endast `owner` kan ta bort andra; själv kan lämna. | **Finns** | Återanvänd. GDPR: avgöra om matdata ska behållas eller anonymiseras (se avsnitt 4). |
|
||||
| **Portionsfaktor** | `household_members.portion_factor` per medlem, default 1, validerat 0.1–3. | **Finns** | Återanvänd. Exponera i mobil-UI för ägare/vuxna. |
|
||||
| **Delade vs privata data** | Lager, plan, inköpslista, matlådor, budget är hushållsdelat. Mål, allergier, smakprofil, måltidshistorik är individuellt (§7/§56). | **Finns** | Fortsätt. Inga ändringar. |
|
||||
| **Länkinbjudan / djuplänk** | Saknas. Idag måste användaren manuellt skriva in `inviteCode`. | **Saknas** | Inför tidsbunden inbjudnings-token (`household_invite_links`) med djuplänk via `brand.config.json` `urlScheme`. |
|
||||
| **QR-inbjudan** | Saknas. | **Saknas** | Generera QR-kod i mobilen som kodar samma djuplänk. Ingen ny backend-modell. |
|
||||
| **Återkalla inbjudan** | Saknas. När en kod/länk delats finns den kvar tills hushållet byter kod. | **Saknas** | Lägg till `revoked_at` på inbjudningslänk och/eller `invite_code_expires_at` på hushållet. |
|
||||
| **Anti-dubblettkartan: inga parallella invite- eller medlemsmodeller** | `households.invite_code` + `household_members` är navet. | **OK** | Behåll. Alla inbjudningsvägar ska peka mot samma `households.invite_code`. |
|
||||
| **Händelsespårning** | `HOUSEHOLD_MEMBER_ADDED` finns i `EVENT_TYPES`. | **Finns** | Lägg till `HOUSEHOLD_MEMBER_REMOVED`, `HOUSEHOLD_MEMBER_ROLE_CHANGED`, `HOUSEHOLD_INVITE_LINK_CREATED`, `HOUSEHOLD_INVITE_LINK_REVOKED`. |
|
||||
| **Analytics** | Analytics-events är consent-gatade via `trackProductAnalytics`. | **Finns** | Lägg till `household_member_added`, `household_member_removed`, `household_invite_link_used` (inga PII, inga fritext-fält). |
|
||||
| **Notifieringar till medlemmar** | `notifications`-tabell + push_tokens finns. | **Delvis** | Återanvänd. Skicka push när någon går med eller lämnar (opt-in via notis-samtycke). |
|
||||
| **Plan-gränser** | `loadEntitlements(app.db, owner.userId)` ger `maxHouseholdMembers`. | **Finns** | Återanvänd vid både kod- och länkinbjudan. |
|
||||
| §7-krav / område | Befintlig implementation | Status | Rekommenderad åtgärd |
|
||||
| ---------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | ---------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **Hushållsgrundstruktur** | Tabellerna `households` och `household_members` finns. `households.invite_code` är unik. Roller: `owner`, `adult`, `member`, `child`. | **Finns** | Återanvänd oförändrat. Utöka med tidsbunden inbjudanslänk, inte ny inbjudningsmodell. |
|
||||
| **Gå med via kod** | `POST /v1/households/join` med `{ inviteCode }`. Nya medlemmar får roll `adult`. Plan-gräns `maxHouseholdMembers` kontrolleras. | **Finns** | Återanvänd. Utöka med djuplänk + QR så att koden kan fyllas i automatiskt, men backend-logiken är densamma. |
|
||||
| **Medlemslista** | `GET /v1/households/:id` returnerar medlemmar med `userId`, `displayName`, `role`, `portionFactor`, `joinedAt`. | **Finns** | Återanvänd. Lägg till UI för att visa/ändra. |
|
||||
| **Ändra roll** | `PATCH /v1/households/:id/members/:userId` med `role`. Endast `owner` får ändra roller. | **Finns** | Återanvänd. Säkerställ att `child` inte kan upphöjas till `owner`. |
|
||||
| **Ta bort medlem** | `DELETE /v1/households/:id/members/:userId`. Endast `owner` kan ta bort andra; själv kan lämna. | **Finns** | Återanvänd. GDPR: avgöra om matdata ska behållas eller anonymiseras (se avsnitt 4). |
|
||||
| **Portionsfaktor** | `household_members.portion_factor` per medlem, default 1, validerat 0.1–3. | **Finns** | Återanvänd. Exponera i mobil-UI för ägare/vuxna. |
|
||||
| **Delade vs privata data** | Lager, plan, inköpslista, matlådor, budget är hushållsdelat. Mål, allergier, smakprofil, måltidshistorik är individuellt (§7/§56). | **Finns** | Fortsätt. Inga ändringar. |
|
||||
| **Länkinbjudan / djuplänk** | Saknas. Idag måste användaren manuellt skriva in `inviteCode`. | **Saknas** | Inför tidsbunden inbjudnings-token (`household_invite_links`) med djuplänk via `brand.config.json` `urlScheme`. |
|
||||
| **QR-inbjudan** | Saknas. | **Saknas** | Generera QR-kod i mobilen som kodar samma djuplänk. Ingen ny backend-modell. |
|
||||
| **Återkalla inbjudan** | Saknas. När en kod/länk delats finns den kvar tills hushållet byter kod. | **Saknas** | Lägg till `revoked_at` på inbjudningslänk och/eller `invite_code_expires_at` på hushållet. |
|
||||
| **Anti-dubblettkartan: inga parallella invite- eller medlemsmodeller** | `households.invite_code` + `household_members` är navet. | **OK** | Behåll. Alla inbjudningsvägar ska peka mot samma `households.invite_code`. |
|
||||
| **Händelsespårning** | `HOUSEHOLD_MEMBER_ADDED` finns i `EVENT_TYPES`. | **Finns** | Lägg till `HOUSEHOLD_MEMBER_REMOVED`, `HOUSEHOLD_MEMBER_ROLE_CHANGED`, `HOUSEHOLD_INVITE_LINK_CREATED`, `HOUSEHOLD_INVITE_LINK_REVOKED`. |
|
||||
| **Analytics** | Analytics-events är consent-gatade via `trackProductAnalytics`. | **Finns** | Lägg till `household_member_added`, `household_member_removed`, `household_invite_link_used` (inga PII, inga fritext-fält). |
|
||||
| **Notifieringar till medlemmar** | `notifications`-tabell + push_tokens finns. | **Delvis** | Återanvänd. Skicka push när någon går med eller lämnar (opt-in via notis-samtycke). |
|
||||
| **Plan-gränser** | `loadEntitlements(app.db, owner.userId)` ger `maxHouseholdMembers`. | **Finns** | Återanvänd vid både kod- och länkinbjudan. |
|
||||
|
||||
### Slutsats av matrisen
|
||||
|
||||
Hushållssamarbete är en **utbyggnad**, inte ett parallellsystem:
|
||||
|
||||
- **Kärnan** (`households`, `household_members`, `invite_code`, roller, plan-gränser) finns redan.
|
||||
- **Det som saknas** är (a) djuplänk/QR för smidigare inbjudan, (b) tidsstyrning/återkallning av inbjudningar, (c) medlemskaps-händelser och analytics, (d) GDPR-klar hantering när medlem lämnar.
|
||||
|
||||
@@ -46,7 +49,9 @@ Hushållssamarbete är en **utbyggnad**, inte ett parallellsystem:
|
||||
## 2. Anti-dubblettkartan – varför INGA nya parallella modeller
|
||||
|
||||
### 2.1 Varför inte en separat `household_invites`-tabell med egen kod?
|
||||
|
||||
`households.invite_code` är redan unik och fungerar. Att skapa en separat tabell med egna koder skulle ge:
|
||||
|
||||
- Dubbla källor på sanning: vilken kod gäller?
|
||||
- Dubbla join-endpoints och valideringar.
|
||||
- Risk att gamla och nya inbjudningar krockar.
|
||||
@@ -54,13 +59,16 @@ Hushållssamarbete är en **utbyggnad**, inte ett parallellsystem:
|
||||
**Beslut:** Använd `households.invite_code` som kanonisk permanent kod (multi-use fallback). En separat `household_invite_links`-tabell får endast lagra **metadata om länken**: `tokenHash`, `createdBy`, `expiresAt`, `revokedAt`, `usedAt`. Själva inbjudningskoden hämtas alltid från `households.invite_code` när länken löses in.
|
||||
|
||||
### 2.2 Varför inte en separat `household_member_history`-tabell?
|
||||
|
||||
Medlemskapet är PK på `(household_id, user_id)`. Aktuellt medlemskap finns i `household_members`. Om vi behöver historik (vem var medlem när?) kan vi använda:
|
||||
|
||||
- `audit_logs` (som redan loggar `household.member_removed`)
|
||||
- `domain_events` (`HOUSEHOLD_MEMBER_ADDED`, `HOUSEHOLD_MEMBER_REMOVED`)
|
||||
|
||||
Ingen ytterligare tabell behövs i Fas 4.
|
||||
|
||||
### 2.3 Varför inte en separat `household_permissions`-tabell?
|
||||
|
||||
Rollerna `owner`, `adult`, `member`, `child` är tillräckligt granulära för Fas 4. Rollbaserade behörigheter hårdkodas i routes (som idag). Om vi senare behöver finare rättigheter kan vi migrera då.
|
||||
|
||||
---
|
||||
@@ -68,9 +76,11 @@ Rollerna `owner`, `adult`, `member`, `child` är tillräckligt granulära för F
|
||||
## 3. STEGPLAN
|
||||
|
||||
### Steg 4a – Djuplänk/QR för inbjudan
|
||||
|
||||
**Mål:** En användare kan trycka på en länk/QR och automatiskt hamna i appen med inbjudningskoden ifylld.
|
||||
|
||||
**Ändringar:**
|
||||
|
||||
1. Ny tabell `household_invite_links`:
|
||||
- `id` uuid PK
|
||||
- `householdId` FK → households
|
||||
@@ -98,6 +108,7 @@ Rollerna `owner`, `adult`, `member`, `child` är tillräckligt granulära för F
|
||||
6. `DELETE /v1/households/:id/invite-links/:linkId` (owner/adult): återkalla (sätt `revokedAt`).
|
||||
|
||||
**Berörda filer:**
|
||||
|
||||
- `packages/database/src/schema/households.ts` (`household_invite_links`)
|
||||
- `packages/validation/src/household.ts` (nya schemas)
|
||||
- `apps/api/src/routes/households.ts`
|
||||
@@ -110,6 +121,7 @@ Rollerna `owner`, `adult`, `member`, `child` är tillräckligt granulära för F
|
||||
**Migration:** `0019_household_invite_links.sql`
|
||||
|
||||
**Testplan:**
|
||||
|
||||
- Skapa länk → token returneras EN gång.
|
||||
- Länk med giltig token → användare blir medlem med roll `adult`.
|
||||
- Utgången länk → 410 Gone med i18n-nyckel `household.inviteExpired`.
|
||||
@@ -122,9 +134,11 @@ Rollerna `owner`, `adult`, `member`, `child` är tillräckligt granulära för F
|
||||
---
|
||||
|
||||
### Steg 4b – Medlemshantering i mobilen
|
||||
|
||||
**Mål:** Ägare/vuxna kan se medlemmar, ändra roll, ta bort medlem; alla kan lämna hushållet.
|
||||
|
||||
**Ändringar:**
|
||||
|
||||
1. Uppdatera `apps/mobile/src/app/household.tsx`:
|
||||
- Visa medlemmar med roll och portionsfaktor.
|
||||
- Ägare kan ändra roll och ta bort (utom sig själv om enda ägaren).
|
||||
@@ -135,12 +149,14 @@ Rollerna `owner`, `adult`, `member`, `child` är tillräckligt granulära för F
|
||||
- `DELETE /v1/households/:id/members/:userId`
|
||||
|
||||
**Berörda filer:**
|
||||
|
||||
- `apps/mobile/src/app/household.tsx`
|
||||
- `apps/mobile/src/locales/*/common.json` (+12)
|
||||
|
||||
**Migration:** ingen.
|
||||
|
||||
**Testplan:**
|
||||
|
||||
- Ägare ändrar `member` → `adult`.
|
||||
- Ägare tar bort medlem → medlem försvinner.
|
||||
- Enda ägaren kan inte ta bort sig själv (409).
|
||||
@@ -149,9 +165,11 @@ Rollerna `owner`, `adult`, `member`, `child` är tillräckligt granulära för F
|
||||
---
|
||||
|
||||
### Steg 4c – Händelser, analytics och notiser
|
||||
|
||||
**Mål:** Hushållsförändringar syns i domain_events, analytics och (opt-in) push-notiser.
|
||||
|
||||
**Ändringar:**
|
||||
|
||||
1. Lägg till i `EVENT_TYPES`:
|
||||
- `HOUSEHOLD_MEMBER_REMOVED`
|
||||
- `HOUSEHOLD_MEMBER_ROLE_CHANGED`
|
||||
@@ -169,6 +187,7 @@ Rollerna `owner`, `adult`, `member`, `child` är tillräckligt granulära för F
|
||||
4. Push-notis (opt-in via notis-samtycke) till övriga medlemmar när någon går med eller lämnar.
|
||||
|
||||
**Berörda filer:**
|
||||
|
||||
- `packages/shared-types/src/enums.ts`
|
||||
- `packages/events/src/index.ts`
|
||||
- `packages/analytics/src/builders.ts`
|
||||
@@ -178,6 +197,7 @@ Rollerna `owner`, `adult`, `member`, `child` är tillräckligt granulära för F
|
||||
**Migration:** `0020_expand_event_types_household.sql`
|
||||
|
||||
**Testplan:**
|
||||
|
||||
- Efter join: `HOUSEHOLD_MEMBER_ADDED` + analytics.
|
||||
- Efter borttag: `HOUSEHOLD_MEMBER_REMOVED` + audit-log.
|
||||
- Efter rolländring: `HOUSEHOLD_MEMBER_ROLE_CHANGED`.
|
||||
@@ -187,30 +207,30 @@ Rollerna `owner`, `adult`, `member`, `child` är tillräckligt granulära för F
|
||||
|
||||
## 4. RISKER
|
||||
|
||||
| Risk | Påverkan | Minskning |
|
||||
|------|----------|-----------|
|
||||
| **GDPR: data när medlem lämnar** | Hög | Att lämna/tas bort ur ett hushåll är **inte kontoradering**. Vid borttag av `household_members`-raden tas endast **åtkomsten** bort. Personens `meals`, `taste_signals`, `memory_items`, `user_health_profiles` och `push_tokens` tillhör kontot och rörs inte — användaren kan gå med i ett annat hushåll. Hushållets historiska aggregat (budget, svinn, gemensamma matlådor, inventory-transaktioner) ändras inte retroaktivt. Radering av persondata sker endast via den befintliga kontoraderingsprocessen (`DELETE /v1/me`, dokumenterad i docs/12). Beslutet skrivs in explicit innan 4b kodas. |
|
||||
| **Behörigheter: vem får bjuda in/ta bort** | Hög | Bjud in: `owner` och `adult`. Ta bort andra: endast `owner`. Ta bort sig själv: alla utom enda ägaren. Ändra roller: endast `owner`. `child` får aldrig bjuda in eller ta bort. |
|
||||
| **Inbjudningslänkar läcker** | Medel | Token är slumpmässig ≥128 bitar, tidsbegränsad, engångsbruk. Servern lagrar endast sha256(token). Återkallning sätts omedelbart. Inga PII i länken. |
|
||||
| **Parallella inbjudningsmodeller** | Hög | Enda kanoniska koden är `households.invite_code`. `household_invite_links` är endast metadata. `/v1/households/join` och `/v1/households/join-link` konvergerar till samma join-kärna. |
|
||||
| **Plan-gränser kringgås** | Medel | Både kod- och länkinbjudan kontrollerar `maxHouseholdMembers` mot ägarens entitlements före insert. |
|
||||
| **i18n-paritet fallerar** | Låg | Alla nya nycklar läggs i samtliga 12 locales och testas med `apps/mobile/test/i18n.test.ts`. |
|
||||
| **Djuplänk hårdkodar app-namn** | Medel | `urlScheme` läses dynamiskt från `brand.config.json`. Ingen hårdkodning av app-namn. |
|
||||
| **Testdata läcker mellan tester** | Låg | Hermetiska tester skapar egna hushåll per test; `invite_code` genereras unikt. |
|
||||
| Risk | Påverkan | Minskning |
|
||||
| ------------------------------------------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| **GDPR: data när medlem lämnar** | Hög | Att lämna/tas bort ur ett hushåll är **inte kontoradering**. Vid borttag av `household_members`-raden tas endast **åtkomsten** bort. Personens `meals`, `taste_signals`, `memory_items`, `user_health_profiles` och `push_tokens` tillhör kontot och rörs inte — användaren kan gå med i ett annat hushåll. Hushållets historiska aggregat (budget, svinn, gemensamma matlådor, inventory-transaktioner) ändras inte retroaktivt. Radering av persondata sker endast via den befintliga kontoraderingsprocessen (`DELETE /v1/me`, dokumenterad i docs/12). Beslutet skrivs in explicit innan 4b kodas. |
|
||||
| **Behörigheter: vem får bjuda in/ta bort** | Hög | Bjud in: `owner` och `adult`. Ta bort andra: endast `owner`. Ta bort sig själv: alla utom enda ägaren. Ändra roller: endast `owner`. `child` får aldrig bjuda in eller ta bort. |
|
||||
| **Inbjudningslänkar läcker** | Medel | Token är slumpmässig ≥128 bitar, tidsbegränsad, engångsbruk. Servern lagrar endast sha256(token). Återkallning sätts omedelbart. Inga PII i länken. |
|
||||
| **Parallella inbjudningsmodeller** | Hög | Enda kanoniska koden är `households.invite_code`. `household_invite_links` är endast metadata. `/v1/households/join` och `/v1/households/join-link` konvergerar till samma join-kärna. |
|
||||
| **Plan-gränser kringgås** | Medel | Både kod- och länkinbjudan kontrollerar `maxHouseholdMembers` mot ägarens entitlements före insert. |
|
||||
| **i18n-paritet fallerar** | Låg | Alla nya nycklar läggs i samtliga 12 locales och testas med `apps/mobile/test/i18n.test.ts`. |
|
||||
| **Djuplänk hårdkodar app-namn** | Medel | `urlScheme` läses dynamiskt från `brand.config.json`. Ingen hårdkodning av app-namn. |
|
||||
| **Testdata läcker mellan tester** | Låg | Hermetiska tester skapar egna hushåll per test; `invite_code` genereras unikt. |
|
||||
|
||||
---
|
||||
|
||||
## 5. Översiktliga berörda tabeller
|
||||
|
||||
| Tabell | Förändring |
|
||||
|--------|------------|
|
||||
| `households` | Ingen schemaändring. `invite_code` fortsätter vara kanonisk. |
|
||||
| `household_members` | Ingen schemaändring. Befintlig roll- och delete-logik återanvänds. |
|
||||
| `household_invite_links` | **Ny** (metadata om djuplänkar). |
|
||||
| `users` | Ingen ändring, men vid kontoradering används befintligt GDPR-flöde (docs/12). |
|
||||
| `domain_events` | Nya event-typer: `HOUSEHOLD_MEMBER_REMOVED`, `HOUSEHOLD_MEMBER_ROLE_CHANGED`, `HOUSEHOLD_INVITE_LINK_CREATED`, `HOUSEHOLD_INVITE_LINK_REVOKED`. |
|
||||
| `audit_logs` | Återanvänds för `household.member_removed` (finns redan). |
|
||||
| `notifications` / `push_tokens` | Återanvänds för opt-in-notiser. |
|
||||
| Tabell | Förändring |
|
||||
| ------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `households` | Ingen schemaändring. `invite_code` fortsätter vara kanonisk. |
|
||||
| `household_members` | Ingen schemaändring. Befintlig roll- och delete-logik återanvänds. |
|
||||
| `household_invite_links` | **Ny** (metadata om djuplänkar). |
|
||||
| `users` | Ingen ändring, men vid kontoradering används befintligt GDPR-flöde (docs/12). |
|
||||
| `domain_events` | Nya event-typer: `HOUSEHOLD_MEMBER_REMOVED`, `HOUSEHOLD_MEMBER_ROLE_CHANGED`, `HOUSEHOLD_INVITE_LINK_CREATED`, `HOUSEHOLD_INVITE_LINK_REVOKED`. |
|
||||
| `audit_logs` | Återanvänds för `household.member_removed` (finns redan). |
|
||||
| `notifications` / `push_tokens` | Återanvänds för opt-in-notiser. |
|
||||
|
||||
---
|
||||
|
||||
@@ -227,6 +247,7 @@ Per steg: `brand-guard`, `typecheck`, `test`, `build`, `first-deploy staging`, p
|
||||
## 7. Länk-invites – detaljerade regler
|
||||
|
||||
### 7.1 Token och djuplänk
|
||||
|
||||
- Token: slumpmässig ≥128 bitar, base64url → 22 tecken. Skickas endast till klienten vid skapande.
|
||||
- Servern lagrar **sha256(token)** i `household_invite_links.tokenHash`; uppslag vid inbjudan görs via hash.
|
||||
- Djuplänk: `<urlScheme>://households/join?token=<token>` där `urlScheme` kommer från `brand.config.json`.
|
||||
@@ -238,9 +259,11 @@ Per steg: `brand-guard`, `typecheck`, `test`, `build`, `first-deploy staging`, p
|
||||
- **Owner-transfer ingår inte i Fas 4.**
|
||||
|
||||
### 7.2 Ingen PII i länken
|
||||
|
||||
Länken innehåller endast token. Inget hushållsnamn, ingen e-post, inga medlemsnamn. Den som har länken kan gå med, därför ska användare dela den via säkra kanaler.
|
||||
|
||||
### 7.3 Token-säkerhet och användningsregler
|
||||
|
||||
- Token genereras slumpmässigt med ≥128 bitar.
|
||||
- Servern lagrar **endast sha256(token)** i `tokenHash`; klartext-token returneras bara vid skapande.
|
||||
- Länkar är **single-use by design**: `usedAt` sätts vid lyckad join.
|
||||
@@ -250,7 +273,9 @@ Länken innehåller endast token. Inget hushållsnamn, ingen e-post, inga medlem
|
||||
- **Owner-transfer ingår inte i Fas 4.**
|
||||
|
||||
### 7.4 Konvergens med kod-inbjudan
|
||||
|
||||
Både `POST /v1/households/join` (kod) och `POST /v1/households/join-link` (token) ska i slutändan anropa samma interna `joinHouseholdCore(db, householdId, userId, role)` som:
|
||||
|
||||
- kontrollerar plan-gräns,
|
||||
- sätter in `household_members` med angiven roll (`adult` för länk),
|
||||
- emittar `HOUSEHOLD_MEMBER_ADDED`,
|
||||
@@ -258,12 +283,15 @@ Både `POST /v1/households/join` (kod) och `POST /v1/households/join-link` (toke
|
||||
- skickar notis till övriga medlemmar (opt-in).
|
||||
|
||||
### 7.5 i18n
|
||||
|
||||
Nya nycklar för 4a:
|
||||
|
||||
- `household.inviteLink`, `household.inviteLinkShare`, `household.inviteLinkCopied`
|
||||
- `household.inviteExpired`, `household.inviteRevoked`, `household.inviteUsed`
|
||||
- `household.qrScanHint`
|
||||
|
||||
Nya nycklar för 4b:
|
||||
|
||||
- `household.leaveConfirmTitle`, `household.leaveConfirmBody`
|
||||
- `household.removeMemberConfirmTitle`, `household.removeMemberConfirmBody`
|
||||
- `household.changeRoleConfirmTitle`
|
||||
@@ -271,6 +299,7 @@ Nya nycklar för 4b:
|
||||
Alla läggs i samtliga 12 locales och testas med `apps/mobile/test/i18n.test.ts`.
|
||||
|
||||
### 7.6 Analytics
|
||||
|
||||
- `household_member_added`: `{ household_id_hash, role, via: 'code' | 'link' }` — hashat hushålls-id, inga personuppgifter.
|
||||
- `household_member_removed`: `{ household_id_hash }`.
|
||||
- `household_invite_link_used`: `{ household_id_hash, link_id }` — **aldrig token eller invite_code**.
|
||||
@@ -278,8 +307,9 @@ Alla läggs i samtliga 12 locales och testas med `apps/mobile/test/i18n.test.ts`
|
||||
Alla går via `trackProductAnalytics` och är därmed consent-gatade.
|
||||
|
||||
### 7.7 Webbfallback
|
||||
|
||||
Om mottagaren inte har appen installerad och trycker på länken i en webbläsare krävs en associerad domän + universal link / app link. Detta är ett **fas-1-spår** som ligger utanför Fas 4. I Fas 4 förutsätts att mottagaren har Expo Go/appen installerad.
|
||||
|
||||
---
|
||||
|
||||
*Rapport färdig. Inväntar godkännande innan kod påbörjas.*
|
||||
_Rapport färdig. Inväntar godkännande innan kod påbörjas._
|
||||
|
||||
+22
-22
@@ -17,16 +17,16 @@ Planfil: infrastructure/terraform/cibello.plan
|
||||
|
||||
### Huvudkomponenter
|
||||
|
||||
| Komponent | Beskrivning |
|
||||
|-----------|-------------|
|
||||
| VPC | `cibello-prod-vpc` 10.0.0.0/16, 1 publikt + 2 privata subnät (eu-north-1a/1b) |
|
||||
| Compute | EC2 `t3.small` Ubuntu 22.04, gp3 30 GB, EIP, docker/compose/git bootstrap |
|
||||
| Databas | RDS PostgreSQL 16 `db.t4g.micro`, krypterad, 7 dagars backup, deletion protection |
|
||||
| Lagring | S3 `cibello-production` (lifecycle temporary/7d, scans/90d) + `cibello-web` (CloudFront OAC) |
|
||||
| IAM | `cibello-app`-roll + instansprofil, least-privilege S3/SSM/KMS + SSM Core |
|
||||
| Secrets | 4 genererade SSM SecureString + 6 tomma platshållare under `/cibello/prod/*` |
|
||||
| DNS/CDN | Route53 zon `cibello.app`, ACM-cert (us-east-1), CloudFront-distribution |
|
||||
| Mail | SES identitet `mail.cibello.app` + DKIM + config set `cibello-prod` |
|
||||
| Komponent | Beskrivning |
|
||||
| --------- | -------------------------------------------------------------------------------------------- |
|
||||
| VPC | `cibello-prod-vpc` 10.0.0.0/16, 1 publikt + 2 privata subnät (eu-north-1a/1b) |
|
||||
| Compute | EC2 `t3.small` Ubuntu 22.04, gp3 30 GB, EIP, docker/compose/git bootstrap |
|
||||
| Databas | RDS PostgreSQL 16 `db.t4g.micro`, krypterad, 7 dagars backup, deletion protection |
|
||||
| Lagring | S3 `cibello-production` (lifecycle temporary/7d, scans/90d) + `cibello-web` (CloudFront OAC) |
|
||||
| IAM | `cibello-app`-roll + instansprofil, least-privilege S3/SSM/KMS + SSM Core |
|
||||
| Secrets | 4 genererade SSM SecureString + 6 tomma platshållare under `/cibello/prod/*` |
|
||||
| DNS/CDN | Route53 zon `cibello.app`, ACM-cert (us-east-1), CloudFront-distribution |
|
||||
| Mail | SES identitet `mail.cibello.app` + DKIM + config set `cibello-prod` |
|
||||
|
||||
### DNS-poster planerade
|
||||
|
||||
@@ -36,18 +36,18 @@ Planfil: infrastructure/terraform/cibello.plan
|
||||
|
||||
## Grov månadskostnad (uppskattning)
|
||||
|
||||
| Post | Uppskattad kostnad |
|
||||
|------|-------------------|
|
||||
| EC2 t3.small (on-demand) | ~$15/mån |
|
||||
| RDS db.t4g.micro + 20 GB gp3 | ~$12/mån |
|
||||
| EBS 30 GB gp3 (EC2) | ~$3.20/mån |
|
||||
| S3 (lagring + förfrågningar) | ~$1–10/mån beroende på volym |
|
||||
| CloudFront (dataöverföring) | ~$0–20/mån beroende på trafik |
|
||||
| Route53 hosted zone | $0.50/mån |
|
||||
| ACM-certifikat | $0 |
|
||||
| SES (första 3 000 mail/mån inom gratisnivån, därefter ~$0.10/1 000) | ~$0–1/mån |
|
||||
| Dataöverföring ut | tillkommer vid användning |
|
||||
| **Totalt** | **~$32–62/mån + dataöverföring** |
|
||||
| Post | Uppskattad kostnad |
|
||||
| ------------------------------------------------------------------- | -------------------------------- |
|
||||
| EC2 t3.small (on-demand) | ~$15/mån |
|
||||
| RDS db.t4g.micro + 20 GB gp3 | ~$12/mån |
|
||||
| EBS 30 GB gp3 (EC2) | ~$3.20/mån |
|
||||
| S3 (lagring + förfrågningar) | ~$1–10/mån beroende på volym |
|
||||
| CloudFront (dataöverföring) | ~$0–20/mån beroende på trafik |
|
||||
| Route53 hosted zone | $0.50/mån |
|
||||
| ACM-certifikat | $0 |
|
||||
| SES (första 3 000 mail/mån inom gratisnivån, därefter ~$0.10/1 000) | ~$0–1/mån |
|
||||
| Dataöverföring ut | tillkommer vid användning |
|
||||
| **Totalt** | **~$32–62/mån + dataöverföring** |
|
||||
|
||||
## Domänkostnad
|
||||
|
||||
|
||||
@@ -8,21 +8,21 @@
|
||||
|
||||
## Vad som är grönt ✅
|
||||
|
||||
| Kontroll | Resultat |
|
||||
|----------|----------|
|
||||
| Branch / repo-kontroll | HEAD stämmer, trädet rent |
|
||||
| `pnpm typecheck` | 19/19 paket gröna |
|
||||
| `pnpm test --force` | 34/34 tasks, 328 tester gröna (kört 2× på färsk DB) |
|
||||
| `pnpm build` | `@app/admin`, `@app/api`, `@app/worker` byggda |
|
||||
| `brand-guard.sh` | OK – varumärket lever endast i `brand.config.json` |
|
||||
| Databas | `cibello_test` färskt skapad med `DROP DATABASE + CREATE DATABASE … OWNER app_user`, migrerad + seedad med 254 recept/102 ingredienser/76 tabeller |
|
||||
| Allergen-invariant | `@app/recipe-generation/test/allergen-invariant.test.ts` grön – varje seedat recept har `allergens` som matchar färsk derivering från `canonical_ingredients` |
|
||||
| Safety canary | Worker-testet grön; canary-kontraktet är `{allergen_invariant_brott, overifierade_visade, food_safety_lint_avvisade_7d}` |
|
||||
| GDPR-radering | `DELETE /v1/me` tömmer `memory_items`, `taste_signals`, `feedback`, `ai_corrections`, `ai_training_bank`, `scan_jobs` och tillhörande bilder (residual-test grön) |
|
||||
| i18n-paritet ×12 | Tester för locale-preferenser, minnesrendering och enhetsetiketter gröna |
|
||||
| Consent-grindar | Ingen personalisering/impact utan `personalization=granted` (recommendations + memory-impact tester gröna) |
|
||||
| Integritetsspärrar | Impact för användare A returnerar aldrig användare B:s data |
|
||||
| Ops-kontraktet | `/ops/v1/summary` har 14 toppnycklar + `wall` med 6 tiles; `/ops/v1/health` svarar `{"ok":true,"app":"cibello"}`; tester gröna |
|
||||
| Kontroll | Resultat |
|
||||
| ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Branch / repo-kontroll | HEAD stämmer, trädet rent |
|
||||
| `pnpm typecheck` | 19/19 paket gröna |
|
||||
| `pnpm test --force` | 34/34 tasks, 328 tester gröna (kört 2× på färsk DB) |
|
||||
| `pnpm build` | `@app/admin`, `@app/api`, `@app/worker` byggda |
|
||||
| `brand-guard.sh` | OK – varumärket lever endast i `brand.config.json` |
|
||||
| Databas | `cibello_test` färskt skapad med `DROP DATABASE + CREATE DATABASE … OWNER app_user`, migrerad + seedad med 254 recept/102 ingredienser/76 tabeller |
|
||||
| Allergen-invariant | `@app/recipe-generation/test/allergen-invariant.test.ts` grön – varje seedat recept har `allergens` som matchar färsk derivering från `canonical_ingredients` |
|
||||
| Safety canary | Worker-testet grön; canary-kontraktet är `{allergen_invariant_brott, overifierade_visade, food_safety_lint_avvisade_7d}` |
|
||||
| GDPR-radering | `DELETE /v1/me` tömmer `memory_items`, `taste_signals`, `feedback`, `ai_corrections`, `ai_training_bank`, `scan_jobs` och tillhörande bilder (residual-test grön) |
|
||||
| i18n-paritet ×12 | Tester för locale-preferenser, minnesrendering och enhetsetiketter gröna |
|
||||
| Consent-grindar | Ingen personalisering/impact utan `personalization=granted` (recommendations + memory-impact tester gröna) |
|
||||
| Integritetsspärrar | Impact för användare A returnerar aldrig användare B:s data |
|
||||
| Ops-kontraktet | `/ops/v1/summary` har 14 toppnycklar + `wall` med 6 tiles; `/ops/v1/health` svarar `{"ok":true,"app":"cibello"}`; tester gröna |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ DEPLOY_MODE=staging ./infrastructure/deployment/first-deploy.sh
|
||||
```
|
||||
|
||||
När skriptet är klart kontrollerar du:
|
||||
|
||||
- API health: `curl -fsS http://192.168.8.123:4000/healthz`
|
||||
- `API_BASE_URL` som skrivs ut måste vara `http://192.168.8.123:4000` för LAN-telefontest.
|
||||
|
||||
@@ -60,6 +61,7 @@ Verifiera utdata:
|
||||
3. Testa fotoflödet: skapa → ladda upp bild → granska.
|
||||
|
||||
Om "Network request failed" uppstår:
|
||||
|
||||
- Kontrollera att datorn och telefonen är på samma Wi-Fi.
|
||||
- Kontrollera att `API_BASE_URL` i `.env.telefontest` är datorns LAN-IP.
|
||||
- Kontrollera att Expo-loggen visar `exp://192.168.8.123:8081`.
|
||||
|
||||
+11
-11
@@ -6,17 +6,17 @@ Inga handkopior får drifta.
|
||||
|
||||
## SSM-parameter → env-var
|
||||
|
||||
| SSM-parameter | Miljövariabel | Kommentar |
|
||||
|---------------|---------------|-----------|
|
||||
| `/cibello/prod/aamos-api-key` | `AAMOS_API_KEY` | Endast när `AAMOS_MODE=http` |
|
||||
| `/cibello/prod/db-password` | byggd in i `DATABASE_URL` / `TEST_DATABASE_URL` | RDS-användare `cibello_admin` |
|
||||
| `/cibello/prod/entitlement-signing-secret` | `ENTITLEMENT_SIGNING_SECRET` | Signerar prenumerationsrättigheter |
|
||||
| `/cibello/prod/eoc-push-token` | `EOC_PUSH_TOKEN` | Ops-summary push till EOC |
|
||||
| `/cibello/prod/gemini-api-key` | `GEMINI_API_KEY` | AAMOS_MODE=gemini |
|
||||
| `/cibello/prod/google-service-account-json` | `GOOGLE_SERVICE_ACCOUNT_JSON` | Google Play service account (JSON) |
|
||||
| `/cibello/prod/jwt-access-secret` | `JWT_ACCESS_SECRET` | Access-token-signering |
|
||||
| `/cibello/prod/jwt-refresh-secret` | `JWT_REFRESH_SECRET` | Refresh-token-signering |
|
||||
| `/cibello/prod/smtp-pass` | `SMTP_PASS` | SMTP-lösen för `hello@cibello.app` |
|
||||
| SSM-parameter | Miljövariabel | Kommentar |
|
||||
| ------------------------------------------- | ----------------------------------------------- | ---------------------------------- |
|
||||
| `/cibello/prod/aamos-api-key` | `AAMOS_API_KEY` | Endast när `AAMOS_MODE=http` |
|
||||
| `/cibello/prod/db-password` | byggd in i `DATABASE_URL` / `TEST_DATABASE_URL` | RDS-användare `cibello_admin` |
|
||||
| `/cibello/prod/entitlement-signing-secret` | `ENTITLEMENT_SIGNING_SECRET` | Signerar prenumerationsrättigheter |
|
||||
| `/cibello/prod/eoc-push-token` | `EOC_PUSH_TOKEN` | Ops-summary push till EOC |
|
||||
| `/cibello/prod/gemini-api-key` | `GEMINI_API_KEY` | AAMOS_MODE=gemini |
|
||||
| `/cibello/prod/google-service-account-json` | `GOOGLE_SERVICE_ACCOUNT_JSON` | Google Play service account (JSON) |
|
||||
| `/cibello/prod/jwt-access-secret` | `JWT_ACCESS_SECRET` | Access-token-signering |
|
||||
| `/cibello/prod/jwt-refresh-secret` | `JWT_REFRESH_SECRET` | Refresh-token-signering |
|
||||
| `/cibello/prod/smtp-pass` | `SMTP_PASS` | SMTP-lösen för `hello@cibello.app` |
|
||||
|
||||
**Föråldrade / dubbla parametrar** (rensas vid tillfälle): `/cibello/prod/entitlement-signing`, `/cibello/prod/jwt-access`, `/cibello/prod/jwt-refresh`.
|
||||
|
||||
|
||||
+7
-7
@@ -5,13 +5,13 @@
|
||||
|
||||
## Verifierat grönt (senaste körning)
|
||||
|
||||
| Kontroll | Resultat |
|
||||
| --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `pnpm typecheck` | 19/19 paket |
|
||||
| `pnpm test` | 34 tasks, 326 tester – deterministic: full svit körd två gånger mot nyskapad Postgres 16 utan cache, grönt båda körningarna |
|
||||
| `pnpm build` | api + worker + admin |
|
||||
| `pnpm db:migrate && pnpm db:seed` | 76 tabeller, 254 verifierade recept, kanoniska ingredienser, enhetsetiketter, marknadsprofiler, ops/EOC-integrering |
|
||||
| E2E-smoke | register → hushåll (valuta) → lager (kanoniska enheter) → rekommendationer → AI-översättning → publicering → engelsk användare får engelskt innehåll → accentokänslig sök → marknadsprofiler |
|
||||
| Kontroll | Resultat |
|
||||
| --------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `pnpm typecheck` | 19/19 paket |
|
||||
| `pnpm test` | 34 tasks, 326 tester – deterministic: full svit körd två gånger mot nyskapad Postgres 16 utan cache, grönt båda körningarna |
|
||||
| `pnpm build` | api + worker + admin |
|
||||
| `pnpm db:migrate && pnpm db:seed` | 76 tabeller, 254 verifierade recept, kanoniska ingredienser, enhetsetiketter, marknadsprofiler, ops/EOC-integrering |
|
||||
| E2E-smoke | register → hushåll (valuta) → lager (kanoniska enheter) → rekommendationer → AI-översättning → publicering → engelsk användare får engelskt innehåll → accentokänslig sök → marknadsprofiler |
|
||||
|
||||
## Färdigbyggt
|
||||
|
||||
|
||||
Reference in New Issue
Block a user