feat(whatsapp-inbox): GDPR retention cron and RoPA entry (#1341)

PR5b, the final code piece of the WhatsApp intake track. A daily cron
(04:15) enforces the channel's retention table; the receipt itself stays
7-year WORM under BFL and is never touched.

Retention actions (lib/retention.ts, each isolated and idempotent):
- whatsapp_messages transcripts past 90 days: body_text + raw_payload
  cleared in id batches under a wall-clock budget; the row skeleton
  (wamid, direction, timestamps, status, inbox_item_id) survives for
  audit. Only rows still carrying content match.
- Rows with phone_link_id IS NULL (unknown senders, orphans) past
  30 days: deleted.
- Link codes expired more than 24h ago: deleted, used or not.
- Sender rate counters idle 2+ days: deleted (minute/day window keys
  are dead weight after that).
- Links revoked 90+ days ago: phone_enc crypto-shredded to '' (column
  is NOT NULL), one-shot via neq guard; phone_hash and phone_masked
  kept for uniqueness history and audit display.

Route mirrors the sweep cron exactly: withCronContext + registry gate
(503 EXTENSION_DISABLED when the extension is off). vercel.json gets
the schedule and both Docker crontabs are regenerated.

Compliance: new whatsapp.receipt_intake activity in .compliance/ropa.yaml
covering purpose, Art 6(1)(b)/(c)/(f) bases with the Art 14(5)(b) note
for third-party attendee names, Meta Platforms Ireland as processor
(Cloud API, EU SCC addendum, Local Storage region DE), the differentiated
retention table, and security measures.

Co-authored-by: Jakob Wennberg <jakob.wennberg@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Jakob Wennberg
2026-08-05 15:35:41 +02:00
committed by GitHub
co-authored by Jakob Wennberg Claude Fable 5
parent 629069e281
commit bf5ca2c615
9 changed files with 619 additions and 0 deletions
+90
View File
@@ -662,3 +662,93 @@ processing_activities:
- support_ticket_identity_hmac_signed # POSTHOG_SECRET_API_KEY, serverside
- support_message_body_never_in_event_properties # breadcrumb bär endast subject
- email_remains_the_delivery_guarantee # ärendet är ett komplement, ej leveransväg
- id: whatsapp.receipt_intake
name: Kvittoinlämning och klargörande dialog via WhatsApp
purpose: >-
Ta emot kvitton som foto eller PDF skickade till Accounteds delade
WhatsApp-nummer och landa dem i dokumentinkorgen (Underlag) som
räkenskapsinformation, samt föra en kort klargörande dialog i samma
chatt: bekräfta mottagning, fråga vilket företag underlaget tillhör och
samla in de representationsuppgifter Skatteverket kräver (deltagarnas
namn och företag samt syfte). Avsändare är enbart befintliga
Accounted-användare som själva knutit sitt nummer med en engångskod;
okända nummer får ett enda begränsat standardsvar och behandlas aldrig
vidare (ingen AI, ingen mediahämtning, inget innehåll sparas).
Deltagarnamn i representationssvar är tredjepartsuppgifter som samlas
in från användaren, inte från de registrerade själva; individuell
information till varje deltagare vore oproportionerlig i förhållande
till den lagstadgade dokumentationsplikten och undantaget i art.
14.5 b tillämpas (uppgifterna framgår av verifikationen, som bevaras
enligt BFL).
# art 6.1 b: tjänsten (mottagning, dialog, inkorgsleverans).
# art 6.1 c: BFL 7 kap: kvittot bevaras 7 år som räkenskapsinformation.
# art 6.1 f: tredjeparts deltagarnamn i representationssvar (med
# art 14.5 b-undantaget dokumenterat i purpose ovan).
lawful_basis: art_6_1_b_c_and_f
special_category_basis: null
controller: gnubok-tenant
processor: supabase_and_meta
data_subjects:
- business_owner
- company_member
- representation_attendee # tredjepart: namn + företag i representationssvar
data_categories:
- user.contact.phone_number # HMAC-hash med peppar + AES-256-GCM + mask; aldrig i klartext i DB
- user.communication_content # chattext och rå webhook-payload, 90 dagars retention
- user.document # kvittofiler, 7 år WORM enligt BFL 7 kap
- user.name # deltagarnamn i representationssvar (tredjepart)
- user.activity_timestamp
recipients:
- name: Meta Platforms Ireland Ltd (WhatsApp Business Platform, Cloud API)
country: IE
role: processor
- name: Supabase
country: EU
role: processor
# AI-tolkningen av kvittobilder och fritextsvar sker via Amazon Bedrock
# och täcks av den befintliga aktiviteten ai.inference ovan.
international_transfers:
applicable: true
mechanism: scc_2021_c2p
note: >-
Meta Platforms Ireland behandlar meddelandena under WhatsApp Business
dataskyddsvillkor med EU SCC-addendum (Module 2) för vidareöverföring
till Meta Platforms Inc. (US). Cloud API Local Storage är aktiverat
med region DE, så meddelandeinnehåll lagras i vila inom EU hos Meta.
retention:
# Differentierad per datakategori; verkställs dagligen av
# /api/extensions/whatsapp-inbox/retention/cron:
# chattranskript (body_text, raw_payload) 90 dagar, sedan rensas
# innehållet men radskelettet (wamid, riktning, tidsstämplar,
# status, inbox-koppling) bevaras som behandlingsspår
# okända avsändares rader 30 dagar, sedan radering
# engångskoder 24 h efter utgång, radering
# telefonkoppling (phone_enc) tills användaren återkallar,
# därefter kryptoshredning 90 dagar efter revokering (phone_hash och
# phone_masked bevaras för unikhetshistorik respektive visning)
# kvittofilen 7 år, WORM (BFL 7 kap),
# hanteras av dokumentarkivet, ej av denna cron
duration: differentiated_per_data_category
basis: gdpr_storage_limitation_and_bfl_7_kap
stored_in:
- whatsapp_messages
- whatsapp_phone_links
- whatsapp_link_codes
- whatsapp_sender_rate_counters
- whatsapp_conversations
- invoice_inbox_items.channel_context
- document_attachments # kvittofilen, WORM
security_measures:
- phone_number_hmac_peppered_hash_lookup_only
- phone_number_aes_256_gcm_at_rest
- phone_number_masked_in_ui
- dedicated_whatsapp_key_material_not_bankid_keys
- webhook_signature_verified_over_raw_body
- service_role_only_tables_rls_no_policies
- unknown_senders_no_content_persistence_no_llm
- pre_binding_sender_rate_limits
- daily_retention_cron_with_crypto_shred
- llm_reads_answers_as_untrusted_data_no_tools
- receipt_files_worm_protected_bfl
- opt_out_stop_keyword_honored