Files
accounted/.compliance/ropa.yaml
T
MattssonandClaude Opus 5 d54b43f80f Bug/resend and invoices (#1192)
* fix(invoices): anchor the PDF logo to the top-left of its header cell

The logo box is always the full 240x80pt reserved area (any larger logo is
clamped to exactly that), so objectFit: 'contain' placed the image inside it
with the default 50% 50% centering. A wide banner logo fills the width and
lands on the left margin, but a near-square logo scaled down to the 80pt
height cap is only ~117pt wide and got pushed ~60pt in from the margin, which
reads as a misaligned logo and forced companies to reshape their artwork.

Anchor the image top-left so every aspect ratio starts at the margin.

Covered by a test that renders the real PDF and reads the image placement
matrix out of the content stream, for both a wide and a near-square logo.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* feat(invoices): show the real delivery outcome in the send history

"Skickad" only meant the email provider accepted the message, so a bounced
invoice looked identical to one that arrived. Resend reports the outcome
asynchronously; that report now lands on the delivery row and drives the
history: green is reserved for a confirmed delivery, bounce/blocked reads
red, delayed and spam-marked read amber, and an accepted-but-unconfirmed
send is neutral instead of falsely green.

The report arrives on a signed webhook and may only touch the three new
provider status columns of an already sent, unredacted row: the WORM trigger
proves nothing else changed, and a lower ranked or older report can never
downgrade an observed failure. The provider reason text can quote the failing
address, so it is masked on read and cleared by the daily PII redaction job.

Timestamps also formatted in Europe/Stockholm instead of falling back to the
runtime zone, which rendered a 14:05 send as 12:05 on Vercel.

Delivery reports are per message, never per recipient: Resend sends one event
for the whole message, so splitting a send per recipient would be the only way
to get finer granularity, at the cost of CC.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* feat(stripe): make the integration feed-only

Stripe sync now only imports balance transactions into the transactions
inbox, like any bank feed; nothing auto-books. The event/settlement sync
(lib/sync.ts, lib/payouts.ts) stays in the repo but is no longer wired to
any route or cron: the 15-min sync cron is removed from vercel.json.
Payment links on invoice send are unchanged; their payments arrive as
feed rows and are matched manually.

- /sync runs only syncStripeBalanceTransactions; response is { success,
  transactions }
- connecting via OAuth enables the nightly feed by default (toggle stays
  as opt-out)
- panel: needs-review section and plumbing removed, copy rewritten to
  transactions-first (sv + en), toast reports fetched/imported/linked
  and calls out an empty result instead of silent all-zeros

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(api): return the article currency from the v1 article list

The dashboard, importer, export and MCP article surfaces all learned to
carry a non-SEK article price (#1166, #1183, #1184), but the v1
projection still omitted currency. An API or agent caller therefore read
price_excl_vat with nothing marking it as EUR and would copy the number
straight onto a SEK invoice line, at a nine-to-one error.

Adds currency to the projection, the response shape and the example, plus
a pitfall stating the price is not always SEK and that this endpoint does
no FX conversion.

Additive field only; no migration (articles.currency already exists).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* feat(settings): replace the settings modal with a routed panel sheet

Settings now renders as a sheet that fills the main panel, sliding up over the
page the user came from and back down on close, with the sidebar and frame left
visible and usable. Behind it sits one shared master-detail surface: underline
search across every section and subsection, the grouped section rail, and the
active section as a direct-editing accordion. All 11 sections are decomposed
into subsections, and the legacy *SettingsContent components compose the same
pieces so the stacked and accordion layouts cannot drift.

The sheet is the only presentation, on every entry path. The intercepting route
handles in-app navigation and closes by popping the history entry, landing back
on the page underneath. @settingsModal/default.tsx handles cold loads (refresh,
deep link, new tab), where interception never fires; nothing is mounted
underneath there, so it closes to the dashboard. Both branch on one shared
predicate, isSheetSection, together with the settings layout, which must render
nothing for those sections or the surface would stack twice behind the sheet
and run every section's fetches twice.

Closing is deliberate rather than incidental: the X, Esc, or navigating away.
The dialog is non-modal so the sidebar's account popover and company switcher
keep working with settings up, and an outside click no longer dismisses it.
Sections land fully collapsed, and the scroll position of the page behind
survives opening and closing the sheet.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* feat: enhance article management and settings UI

- Add PATCH test for toggling article active state without other fields.
- Remove unused MessageCircle icon from DashboardContent.
- Refactor AccountingFrameworkForm to use SettingsFieldRow for better help text display.
- Update CompanyInfoForm, DimensionsToggle, and various settings forms to replace description with help text.
- Remove redundant headings and intros in several settings components to streamline UI.
- Improve help text for various settings in English and Swedish translations.
- Update structured error messages for better clarity on article deletion.

* refactor(ArticleDetailPage): remove unused imports and duplicate state variable

* fix(settings): own deep-linked settings routes by route list, not nav visibility

Review fixes from the settings panel sheet work:
* isSheetSection reads the full settings route list so a hidden-but-deep-linked
  section (assistant before BankID, banking in sandbox, api without MCP) is
  claimed by the sheet instead of rendering the legacy shell around an empty panel
* keep 503 on the Resend delivery webhook when the signing secret is unset, with
  a test pinning the behaviour
* stripe callback route test coverage

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* refactor: update salary, tax, and templates settings components

- Refactored SalarySettingsContent to use a form wrapper and improved payment settings UI.
- Enhanced TaxSettingsContent with new signals for EU sales, KU obligations, and ROT/RUT deductions.
- Updated TemplatesSettingsContent to remove legacy comments and improve readability.
- Simplified navigation items by removing unnecessary constants and directly using hrefs.
- Cleaned up translation files by removing deprecated keys and adding new descriptions for clarity.

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 22:56:17 +02:00

538 lines
20 KiB
YAML

# Record of Processing Activities (GDPR Art. 30)
#
# Each entry documents one processing activity. Field shape follows EDPB
# Guidelines 9/2022 §3 (controller-side RoPA): purposes, lawful basis,
# data subject + data category, recipients, transfer mechanism, retention,
# security measures. Add an entry whenever a new flow ships that touches
# personal data: onboarding, integrations, support, or regulator filings.
processing_activities:
- id: invoices.delivery_history
name: Leveranshistorik för kundfakturor
purpose: >-
Dokumentera att en kundfaktura skickades, när leveransförsöket skedde,
vilken mottagardomän som användes och vilken exakt PDF som skickades.
Dashboardens listvy visar endast maskerade mottagardomäner och
driftmetadata. Fullständigt meddelandeinnehåll stannar på serversidan.
lawful_basis: art_6_1_b_and_c
special_category_basis: null
controller: gnubok-tenant
processor: supabase_and_resend
data_subjects:
- customer_contact
- company_member
data_categories:
- user.contact.email
- user.communication_content
- user.activity_timestamp
- user.document
recipients:
- name: Supabase
country: EU
role: processor
- name: Resend
country: US
role: processor
international_transfers:
applicable: true
mechanism: scc_2021_c2p
note: Resend processes the outbound delivery under SCC Module 2.
retention:
duration: through_seventh_calendar_year_after_fiscal_year_end
basis: bfl_7_kap_and_gdpr_storage_limitation
stored_in:
- invoice_deliveries
- document_attachments
security_measures:
- active_company_route_context
- rls_company_scoped
- masked_recipient_domains_in_list_api
- no_message_content_in_browser_list_api
- exact_sent_pdf_worm_protection
- metadata_only_immutable_audit_log
- daily_post_retention_pii_redaction
- signed_provider_delivery_webhook_only
- masked_recipient_addresses_in_provider_reason_text
- id: customer.private_identity
name: Personnummer för privatkund
purpose: >-
Identifiera en privatkund när företagets avtal, fakturering eller
kundadministration kräver en entydig identitet. Personnumret får endast
sparas på kundtypen privatkund och exponeras endast maskerat i API och UI.
lawful_basis: art_6_1_b
special_category_basis: null
controller: gnubok-tenant
processor: supabase
data_subjects:
- customer
data_categories:
- user.government_id
recipients:
- name: Supabase
country: EU
role: processor
international_transfers:
applicable: false
mechanism: null
note: EU-only processor.
retention:
duration: customer_relationship_or_7y_if_accounting_record
basis: contract_and_bfl_7_kap
stored_in:
- customers.personal_number
- retained_invoice_documents_when_required
security_measures:
- aes_256_gcm_field_encryption
- masked_api_and_ui_output_last4_only
- write_role_required
- rls_company_scoped
- no_full_value_read_endpoint
- id: agi.submit
name: AGI inlämning till Skatteverket
purpose: >-
Lämna in lagstadgad arbetsgivardeklaration på individnivå (AGI) till
Skatteverket varje månad (Skatteförfarandelagen 26 kap.). Innefattar
huvuduppgift (HU) och individuppgifter (IU) per anställd. Skatteverkets
kvittens sparas i response_data inklusive signeradAv: personnummer för
den som BankID-signerade inlämningen. Ändamålet är bevis för vem som
juridiskt undertecknade deklarationen samt behandlingshistorik; BFL
5 kap. 6 § och BFNAR 2013:2 kap. 8 kräver att räkenskapsinformationen
bevarar den faktiska ingivaren, medan själva inlämningen styrs av SFL.
lawful_basis: art_6_1_c # legal obligation
special_category_basis: null
controller: gnubok-tenant
processor: anthropic-na # software supplier; Skatteverket is recipient, not processor
data_subjects:
- employee
- company_signatory # firmatecknare/deklarationsombud (BankID-signerare av kvittensen)
data_categories:
- user.government_id # personnummer (anställda i IU; signerarens i response_data.signeradAv)
- user.name
- user.financial.employment # gross salary, tax withheld, benefits
- user.financial.tax # avgifter, sjuklönekostnad
recipients:
- name: Skatteverket
country: SE
role: legal_recipient
international_transfers:
applicable: false
mechanism: null
note: >-
Sweden-to-Sweden processor-to-public-authority flow. Adequacy decision
not applicable; no third-country transfer.
retention:
duration: 7y
basis: bfl_7_kap # BFL 7 kap. 2 §: räkenskapsinformation
stored_in:
- agi_declarations.xml_content
- agi_declarations.individuppgifter
- agi_declarations.response_data # SKV kvittens incl. signeradAv (signerarens pnr); access via company-membership RLS
- skatteverket_api_audit_log
security_measures:
- encryption_at_rest_supabase
- rls_company_scoped
- tls_1_3_to_skatteverket
- bankid_signing_required_for_filing
- immutable_audit_log
- personnummer_at_rest_encryption
- id: agi.kontrollera.preflight
name: AGI pre-flight kontrollera (HU/IU)
purpose: >-
Skicka enskild HU eller IU som JSON till Skatteverkets kontrollera-
ändpunkt (v1.7 §7/§8) för dry-run-validering av felmeddelanden och
STOPP-/AVVISANDE-status innan ett fullständigt underlag laddas upp.
Skatteverket lagrar inget från denna kontroll; svaret driver vår UI
som låter användaren rätta felaktigheter i lönekörningen lokalt.
lawful_basis: art_6_1_c # legal obligation (ancillary to AGI filing)
special_category_basis: null
controller: gnubok-tenant
processor: anthropic-na
data_subjects:
- employee
data_categories:
- user.government_id # personnummer (BetalningsmottagarId)
- user.financial.employment # gross salary, tax withheld, benefits
recipients:
- name: Skatteverket
country: SE
role: legal_recipient
international_transfers:
applicable: false
mechanism: null
note: Sweden-to-Sweden flow; no third-country transfer.
retention:
# Skatteverket retains nothing for kontrollera calls. Our outbound
# audit row in skatteverket_api_audit_log records who-called-what-when
# but NOT the payload body, minimising at-rest footprint.
duration: 7y
basis: bfl_7_kap
stored_in:
- skatteverket_api_audit_log
security_measures:
- strict_zod_allow_list_input_validation
- request_size_cap_64kb
- rbac_company_member_role_check
- rls_company_scoped
- tls_1_3_to_skatteverket
- immutable_audit_log
- no_payload_body_persisted
- id: moms.declaration
name: Momsdeklaration till Skatteverket
purpose: >-
Lämna in lagstadgad momsdeklaration (utkast, lås och signering) till
Skatteverkets momsdeklaration-API (Mervärdesskattelagen 17 kap. 24 §).
lawful_basis: art_6_1_c
special_category_basis: null
controller: gnubok-tenant
processor: anthropic-na
data_subjects:
- business_owner
data_categories:
- user.government_id # orgnummer (juridiska personer) eller pnr
- user.financial.tax # rutor 05-62
recipients:
- name: Skatteverket
country: SE
role: legal_recipient
international_transfers:
applicable: false
mechanism: null
note: Sweden-to-Sweden flow; no third-country transfer.
retention:
duration: 7y
basis: bfl_7_kap
stored_in:
- extension_data (utkast metadata)
- skatteverket_api_audit_log
security_measures:
- rls_company_scoped
- tls_1_3_to_skatteverket
- bankid_signing_required_for_filing
- immutable_audit_log
- id: psd2.cash_account_mirror
name: PSD2-konton speglas till cash_accounts
purpose: >-
När en bank-anslutning via Enable Banking returnerar kontolista efter
lyckad PSD2-consent kopieras kontometadata (IBAN, valuta, kontonamn,
external_uid) till cash_accounts så att avstämning, motkontoresolver
och __PRIMARY_SEK__-sentinel kan rutta verifikat utan att läsa JSONB
från bank_connections.accounts_data vid varje fråga.
lawful_basis: art_6_1_b # contract (PSD2 consent + bookkeeping service)
special_category_basis: null
controller: gnubok-tenant
processor: anthropic-na
data_subjects:
- business_owner
data_categories:
- user.financial.bank_account # IBAN
- user.contact # account name (kontotitel)
recipients:
- name: Supabase
country: EU
role: processor
international_transfers:
applicable: false
mechanism: null
note: EU-only processor; no third-country transfer.
retention:
duration: consent_lifetime
basis: psd2_consent
stored_in:
- cash_accounts
- bank_connections.accounts_data
security_measures:
- rls_company_scoped
- data_minimization_no_balance_on_callback
- failure_emitted_to_event_log
- id: transactions.counterparty_iban
name: Motpartens IBAN på transaktioner
purpose: >-
Spara motpartens IBAN på transaktionsraden så att own-account-detector
kan identifiera överföringar mellan företagets egna konton (1930 → 1932
etc.) och bokföra båda benen automatiskt istället för att felbokföra
utflödet som extern kostnad. Krävs även för payment-matchning mot
leverantörsfakturor.
lawful_basis: art_6_1_b # contract (bookkeeping service)
special_category_basis: null
controller: gnubok-tenant
processor: anthropic-na
data_subjects:
- business_owner
- counterparty
data_categories:
- user.financial.bank_account # counterparty IBAN
recipients:
- name: Supabase
country: EU
role: processor
international_transfers:
applicable: false
mechanism: null
note: EU-only processor.
retention:
duration: 7y
basis: bfl_7_kap
stored_in:
- transactions.counterparty_iban
security_measures:
- rls_company_scoped
- immutable_after_post
- id: skattekonto.drift_alert
name: Skattekonto-drift via e-post
purpose: >-
Underrätta företagets kontaktadress när det cachade Skatteverket-saldot
avviker från GL 1630 utöver konfigurerad tolerans (> 1 SEK), så
bokföraren kan granska skattekonto-raderna. E-postmeddelandet
innehåller ingen finansiell siffra utan en länk till autentiserad
dashboard; mottagaren valideras mot company_members innan utskick.
lawful_basis: art_6_1_f # legitimate interest (bookkeeping accuracy)
special_category_basis: null
controller: gnubok-tenant
processor: resend
data_subjects:
- business_owner
- company_member
data_categories:
- user.contact.email
recipients:
- name: Resend
country: US
role: processor
international_transfers:
applicable: true
mechanism: scc_2021_c2p
note: >-
Resend (US): SCC Module 2 (controller-to-processor). Outbound
payload limited to ett notifieringsmail utan finansiella belopp;
TIA dokumenterad i .compliance/tia/resend.md.
retention:
duration: 30d
basis: event_log_ttl
stored_in:
- event_log
security_measures:
- recipient_membership_check_before_send
- no_financial_figures_in_body
- tls_to_resend
- rls_company_scoped
- id: ai.inference
name: AI-inferens (kategorisering + dokumenttolkning) via Amazon Bedrock
purpose: >-
Föreslå bokföringskategori för banktransaktioner och tolka/extrahera
uppladdade underlag (kvitton, leverantörsfakturor) med Anthropic
Claude-modeller körda i Amazon Bedrock. Endast aktiv när tenanten
uttryckligen aktiverat AI-funktioner: kärntjänsten (bokföring, fakturor,
moms, rapporter) fungerar fullt ut utan AI.
lawful_basis: art_6_1_a # consent: opt-in, AI features off by default
special_category_basis: null
controller: gnubok-tenant
processor: aws-bedrock
data_subjects:
- business_owner
- counterparty # namn/belopp på uppladdade underlag
data_categories:
- user.financial # transaktionsdata skickad för kategorisering
- user.document # uppladdade kvitton/fakturor för OCR/extraktion
recipients:
- name: Amazon Web Services (Amazon Bedrock)
country: EU
role: processor
international_transfers:
applicable: false
mechanism: null
note: >-
Inferens körs i AWS-regionen eu-north-1 (Stockholm): ingen överföring
till tredje land. AWS DPA + SCC + DPF-certifiering finns som
skyddsmekanism. Prompter lagras ej hos Bedrock efter anropet och används
ej till modellträning.
retention:
duration: none_at_processor
basis: no_retention_bedrock # prompt not persisted by Bedrock post-inference
stored_in:
- ai_usage_tracking # usage metadata only (tokens/cost), no payload body
security_measures:
- opt_in_consent_required
- eu_region_inference_eu_north_1
- prompts_not_retained_after_inference
- not_used_for_model_training
- rls_company_scoped
- tls_to_bedrock
- id: mcp.telemetry
name: MCP/agent-telemetri i event_log
purpose: >-
Varje MCP-verktygsanrop loggar metadata (verktygsnamn, felkod,
felmeddelande max 500 tecken, latens, aktör, session, frivillig
distributionskanal-markör, t.ex. 'openclaw' via X-Gnubok-Client-header
eller ?client=-parameter, sanerad mot allow-list och aldrig använd för
auktorisation) och varje skill-laddning loggar slug/tier till event_log.
Syftet är tillförlitlighetsanalys av agentgränssnittet: felfrekvens per
verktyg, adoption per distributionskanal, korrelation mellan laddade
skills och efterföljande fel, samt agenters egenrapporterade feedback
(agent.feedback). Inga verktygsargument eller verktygsresultat
persisteras.
lawful_basis: art_6_1_f # legitimate interest (service reliability/improvement)
special_category_basis: null
controller: gnubok-tenant
processor: supabase
data_subjects:
- business_owner
data_categories:
- user.unique_id # userId, actorId (API-nyckel-id), sessionId
- user.contact # actorLabel (användarvald API-nyckeletikett)
recipients:
- name: Supabase
country: EU
role: processor
international_transfers:
applicable: false
mechanism: null
note: EU-only processor; no third-country transfer.
retention:
# Differentiated TTL via /api/events/cleanup/cron: mcp.*/agent.*-rader
# behålls 180 dagar (felfrekvens-trender kräver mer än leveransfönstret);
# övriga event_log-rader (leveranshändelser) 30 dagar.
duration: 180d
basis: legitimate_interest_reliability
stored_in:
- event_log
security_measures:
- rls_user_scoped_select # event_log SELECT: auth.uid() = user_id
- service_role_only_writes
- error_message_truncated_500_chars
- no_tool_args_or_results_persisted
- id: mcp.company_customer_tools
name: MCP-verktyg för företagsinställningar och kunder
purpose: >-
Låta en uttryckligen behörig användare läsa nödvändiga företags- och
kunduppgifter samt förbereda ändringar för användarens godkännande.
Verktygssökningen gnubok_search_tools returnerar endast verktygsmetadata
och JSON-schema, aldrig företags- eller kundposter. Läsresultat begränsas
till det aktiva företaget och till verktygets dokumenterade fält.
lawful_basis: art_6_1_b
special_category_basis: null
controller: gnubok-tenant
processor: supabase
data_subjects:
- business_owner
- company_member
- customer
data_categories:
- user.name
- user.contact
- user.financial.bank_account
recipients:
- name: Supabase
country: EU
role: processor
international_transfers:
applicable: false
mechanism: null
note: EU-only processor.
retention:
duration: pending_operation_lifecycle
basis: contract_and_user_approval
stored_in:
- pending_operations
security_measures:
- api_key_scope_companies_read_or_write
- api_key_scope_customers_read_or_write
- active_company_filter_on_every_query
- rls_company_scoped
- search_returns_tool_metadata_only
- writes_staged_for_explicit_user_approval
- plaintext_personnummer_blocked_from_staging
- id: arsredovisning.signature_evidence
name: Underskriftsbevis för årsredovisning
purpose: >-
Bevara vem som undertecknade en låst årsredovisningsversion, i vilken
roll, med vilken metod och på vilket datum. Bevisreferensen är endast en
ogenomskinlig identifierare med prefix archive:, document: eller receipt:.
Den får inte innehålla namn, e-post, personnummer eller fritext.
lawful_basis: art_6_1_c
special_category_basis: null
controller: gnubok-tenant
processor: supabase
data_subjects:
- company_signatory
data_categories:
- user.name
- user.professional_role
- user.activity_timestamp
recipients:
- name: Supabase
country: EU
role: processor
international_transfers:
applicable: false
mechanism: null
note: EU-only processor.
retention:
duration: 7y
basis: bfl_7_kap_and_arl_signing_evidence
stored_in:
- arsredovisning_signature_requests
- annual_report_versions
security_measures:
- rls_company_scoped
- service_role_only_signature_transitions
- version_company_period_and_state_validation
- signed_rows_immutable
- structured_opaque_evidence_reference
- future_and_pre_finalization_dates_rejected
- id: arsredovisning.bolagsverket.submit
name: Digital inlämning av årsredovisning till Bolagsverket
purpose: >-
Lämna in årsredovisningen (iXBRL) digitalt till Bolagsverkets eget
utrymme (ÅRL 8 kap.). Avsändarens och undertecknarens personnummer
krävs av Bolagsverkets API (skapa-inlamningtoken, lamna-in) och
används endast transient i anropen. Varken personnummer eller hash av
personnummer sparas i inlämningsposten. Fastställelseintyget signeras
med e-legitimation hos Bolagsverket, aldrig i appen.
lawful_basis: art_6_1_c # legal obligation (ÅRL filing duty)
special_category_basis: null
controller: gnubok-tenant
processor: anthropic-na # software supplier; Bolagsverket is recipient, not processor
data_subjects:
- business_owner # avsändare/undertecknare (styrelseledamot, VD)
data_categories:
- user.government_id # personnummer, endast transient till Bolagsverket
- user.name # undertecknarens namn
- user.contact # undertecknarens/kvittens e-post
- user.financial # årsredovisningens finansiella innehåll
recipients:
- name: Bolagsverket
country: SE
role: legal_recipient
international_transfers:
applicable: false
mechanism: null
note: Sweden-to-Sweden flow to a public authority; no third-country transfer.
retention:
duration: 7y
basis: bfl_7_kap # filed .xhtml archived as räkenskapsinformation
stored_in:
- arsredovisning_submissions # status, idnummer and checksums, no PNR values or hashes
- document_attachments # the exact filed iXBRL bytes (WORM)
security_measures:
- mtls_client_certificate_to_bolagsverket
- mtls_key_env_only_injected_from_secret_manager # never in settings/DB; see .env.example custody note
- pinned_endpoint_allowlist_per_environment # HOSTS map in client.ts; not env-configurable
- personnummer_and_hash_never_persisted
- personnummer_never_logged
- webhook_secret_constant_time_validation
- rls_company_scoped
- immutable_status_machine_trigger