fix(inbox): never extract the receiving company as its own supplier (#2080)

* fix(inbox): never extract the receiving company as its own supplier

The model sometimes reads the Kund/Kunduppgifter block of bank agreements
and similar documents as the issuer, so the inbox offered to create the
user's own company as a leverantor. Two layers: a supplier-direction rule
in the extraction prompt (issuer, never the customer/recipient block), and
a deterministic post-extraction guard that nulls the supplier block when
its org number, derived VAT number, or exact name equals the receiving
company's own. All AI extraction paths (upload, deferred, retry,
attach-document, document-extraction, MCP) pass the company identity.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Hqm9QgdyNAFaWiz6Ww7pgb

* fix(inbox): keep the own-company guard alive for photographed documents

Skeptic findings on PR #2080: normalizeImageForExtraction rebuilt the input
without ownCompany, so the guard never fired for HEIC/oversized phone photos
(the motivating case). Spread the original input instead. Also let a
provably different extracted org number outvote a name coincidence, and
accept 12-digit personnummer-form own org numbers (19/20 century prefix,
enskild firma) alongside the 16-prefixed organisation form.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Hqm9QgdyNAFaWiz6Ww7pgb

* docs(compliance): add RoPA entry for the own-company-identity lookup

Compliance swarm (GDPR Art. 30): fetchOwnCompanyIdentity reads
companies.name and org_number on every extraction; record the processing
activity (transient in-memory comparison, never sent to the model).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Hqm9QgdyNAFaWiz6Ww7pgb

* fix(inbox): log own-company identity lookup failures instead of failing silent

Compliance swarm (SOC 2 CC7.2): the bare catch in fetchOwnCompanyIdentity
made a persistently broken lookup (RLS misconfig, DB outage) disable the
guard invisibly. Also pin the VAT-number match across prefixed and bare
digit forms in the test matrix.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Hqm9QgdyNAFaWiz6Ww7pgb

* fix(inbox): surface maybeSingle query errors in the own-company lookup

CodeRabbit: maybeSingle() reports RLS/query failures in error without
throwing, so the failure log added for SOC 2 CC7.2 never fired for exactly
those cases. Throw the reported error into the existing catch (still fail
open to nulls).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Hqm9QgdyNAFaWiz6Ww7pgb

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Mattsson
2026-08-31 15:13:14 +02:00
committed by GitHub
co-authored by Claude Fable 5
parent fc54e79e08
commit 84c8e1ce59
10 changed files with 369 additions and 7 deletions
+38
View File
@@ -856,3 +856,41 @@ processing_activities:
- claim_based_once_per_day_dedup
- company_name_header_injection_sanitized
- cron_secret_authenticated_trigger
- id: documents.own_company_supplier_guard
name: Egenbolagskontroll vid dokumentextraktion
purpose: >-
Jämföra AI-extraherade leverantörsfält mot det egna bolagets namn och
organisationsnummer (för enskild firma ett personnummer) så att
användarens eget bolag aldrig föreslås som leverantör när modellen läst
dokumentets kundblock. Jämförelsen sker i applikationskoden efter
extraktionen; identiteten skickas aldrig till AI-modellen och skrivs
inte till någon ny lagringsyta.
lawful_basis: art_6_1_f
special_category_basis: null
controller: gnubok-tenant
processor: supabase
data_subjects:
- business_owner
data_categories:
- user.name # companies.name
- user.government_id # companies.org_number, personnummer för enskild firma
recipients:
- name: Supabase
country: EU
role: processor
international_transfers:
applicable: false
mechanism: null
note: >-
Transient jämförelse i applikationsprocessen; datat lämnar aldrig
befintlig lagring (companies-tabellen) och skickas inte till Bedrock.
retention:
duration: none
basis: transient_in_memory_comparison
stored_in: []
security_measures:
- rls_company_scoped
- not_sent_to_model
- transient_comparison_no_new_storage
- fail_open_to_nulls_on_lookup_error