25 KiB
Fas 4 – Hushållssamarbete: REUSE/GAP-matris och stegplan
Levereras före kod. Styrande dokument: utbyggnadsdokumentet §7 +
docs/03-funktionskatalog.md+docs/07-datamodell.md+docs/12-sakerhet-gdpr.md+docs/19-beslutslogg.md.Audit först, ingen kod. Invänta godkännande innan 4a påbörjas.
Hermetiska testregler (samma som Fas 3)
Alla integrationstester ska vara hermetiska:
- De får inte bero på innehållet i
.env(värden somAAMOS_MODE,EMAIL_MODE,S3_MODEskrivs över isetup-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.
- Körning:
pnpm --filter @app/database run db:test-setup(migrate + seed) ska alltid föregå API-testerna.
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. |
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.
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.
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 loggarhousehold.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å.
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:
- Ny tabell
household_invite_links:iduuid PKhouseholdIdFK → householdstokenHashtext unik, sha256 av slumpmässigt token (≥128 bitar, base64url)createdByFK → usersexpiresAttimestamp with time zone (default nu()+7 dagar)revokedAttimestamp with time zone nullableusedAttimestamp with time zone nullablecreatedAt,updatedAt- Token lagras ALDRIG i klartext i DB. Uppslag görs via
sha256(token).
- Djuplänk:
<urlScheme>://households/join?token=<token>därurlSchemeläses urbrand.config.json(aldrig hårdkodat). - Ny endpoint
POST /v1/households/join-link:- Tar
{ token }. - Slår upp
household_invite_links→ hushåll viatokenHash = sha256(token). - Kontrollerar
expiresAt,revokedAt,usedAt. - Återanvänder samma plan-gräns- och join-logik som
/v1/households/join. - Efter lyckad join: markera länken
usedAt.
- Tar
- Ny endpoint
POST /v1/households/:id/invite-links(owner/adult):- Skapar en ny länk med 7 dagars giltighet.
- Begränsa antalet aktiva länkar per hushåll (t.ex. max 10).
- Returnerar endast token EN GÅNG (i svaret). Servern sparar bara hash.
- Roll vid join via länk är alltid
adult— aldrigowner. - Owner-transfer ingår INTE i Fas 4.
GET /v1/households/:id/invite-links(owner/adult): lista aktiva länkar medexpiresAt(utan token, såklart).DELETE /v1/households/:id/invite-links/:linkId(owner/adult): återkalla (sättrevokedAt).
Berörda filer:
packages/database/src/schema/households.ts(household_invite_links)packages/validation/src/household.ts(nya schemas)apps/api/src/routes/households.tsapps/api/src/lib/helpers.ts(generateInviteLinkToken,hashInviteLinkToken)apps/mobile/src/app/household.tsx(QR + dela-länk)apps/mobile/src/app/household-join.tsx(ny skärm för att ta emot länk)apps/mobile/app.json/ deep-linking-konfigurationapps/mobile/src/locales/*/common.json(+12)
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. - Återkallad länk → 410 Gone med i18n-nyckel
household.inviteRevoked. - Redan använd länk → 410 Gone med i18n-nyckel
household.inviteUsed. Länkar är engångsbruk (usedAt). - Plan-gräns nådd → 402 Payment Required (samma som kod).maxHouseholdMembers.
- Max aktiva länkar överskridet → 429.
- Token återfinns inte i DB i klartext.
Steg 4b – Medlemshantering i mobilen
Mål: Ägare/vuxna kan se medlemmar, ändra roll, ta bort medlem; alla kan lämna hushållet.
Ändringar:
- 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).
- Varje medlem kan lämna hushållet.
- Lägg till bekräftelsedialoger för borttag/rolländring.
- Använd befintliga endpoints:
PATCH /v1/households/:id/members/:userIdDELETE /v1/households/:id/members/:userId
Berörda filer:
apps/mobile/src/app/household.tsxapps/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).
childkan inte ändra andras roller (403).
Steg 4c – Händelser, analytics och notiser
Mål: Hushållsförändringar syns i domain_events, analytics och (opt-in) push-notiser.
Ändringar:
- Lägg till i
EVENT_TYPES:HOUSEHOLD_MEMBER_REMOVEDHOUSEHOLD_MEMBER_ROLE_CHANGEDHOUSEHOLD_INVITE_LINK_CREATEDHOUSEHOLD_INVITE_LINK_REVOKED
- Lägg till analytics-events (inga PII, inga fritext-fält):
household_member_addedhousehold_member_removedhousehold_invite_link_used
- Emit events vid:
- join (både kod och länk)
- borttag
- rolländring
- länk skapad/återkallad
- 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.tspackages/events/src/index.tspackages/analytics/src/builders.tsapps/api/src/routes/households.tsapps/api/src/lib/notifications.ts(om sådan finns)
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. - Consent-gate: analytics skickas bara om användaren samtyckt.
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. |
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. |
6. Leveransordning
- Steg 4a: djuplänk/QR för inbjudan (
household_invite_links). - Steg 4b: medlemshantering i mobilen.
- Steg 4c: händelser, analytics och notiser.
Per steg: brand-guard, typecheck, test, build, first-deploy staging, patch + bundle.
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ärurlSchemekommer frånbrand.config.json. - Giltighet: 7 dagar (
expiresAt). - Engångsbruk: efter lyckad join sätts
usedAt. Länkar är single-use by design. - Återkallning:
ownerelleradultsätterrevokedAt. - Max 10 aktiva länkar per hushåll.
- Roll vid join: alltid
adult— aldrigowner. - 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:
usedAtsätts vid lyckad join. - Giltighetstid: 7 dagar.
- Max 10 aktiva länkar per hushåll.
- Roll vid join via länk: alltid
adult— aldrigowner. - 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_membersmed angiven roll (adultför länk), - emittar
HOUSEHOLD_MEMBER_ADDED, - loggar analytics (consent-gatat),
- skickar notis till övriga medlemmar (opt-in).
7.5 i18n
Nya nycklar för 4a:
household.inviteLink,household.inviteLinkShare,household.inviteLinkCopiedhousehold.inviteExpired,household.inviteRevoked,household.inviteUsedhousehold.qrScanHint
Nya nycklar för 4b:
household.leaveConfirmTitle,household.leaveConfirmBodyhousehold.removeMemberConfirmTitle,household.removeMemberConfirmBodyhousehold.changeRoleConfirmTitle
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.
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.