Bokföring och redovisning
At a glance
Bokforing och redovisning is the double-entry bookkeeping core: balanced AccountingEntry vouchers backed by per-tenant chart-of-accounts mapping, exports to Fortnox semicolon CSV, Visma eEkonomi tab text, Xero international CSV, Bokio direct REST API and SIE4 with #KTYP, #IB, #UB and #RES, season-snapshot financial statements with optional period lock on finalisation, consolidated income-statement and balance-sheet reports broken down per OrgNode and an immutable FinancialAuditLog covering every operation including export generation.
How it works
Every booked transaction lives as an AccountingEntry voucher with debit and credit lines balanced to zero. ChartOfAccountsMapping is a tenant-scoped collection that translates internal BAS account categories into external provider accounts; one active mapping per provider per tenant is enforced (duplicate creation returns 409). Exports are provider-specific: Fortnox via /accounting-export/fortnox writes semicolon-separated CSV with VER, Serie, Vernr, Datum, Text, Konto, Debet, Kredit columns and Swedish decimal format (comma); Xero via /accounting-export/xero writes comma-separated CSV with star-prefixed required columns and international decimal format (period); Visma eEkonomi via /accounting-export/visma writes tab-separated text with VER plus Projekt and Resultatenhet, Swedish decimal format, and supports API push via /accounting-integrations/{id}/sync/{entry_id}.
SIE4 export at /accounting-export/sie4 includes header lines #KTYP for account types, #IB for opening balances, #UB for closing balances and #RES for income-statement accounts, encoded CP437 per Swedish standard. Bokio has its own variant with UTF-8 BOM, CRLF line endings, BAS-2024 validation (1000-8999) and mandatory #ORGNR at /accounting/bokio/export/sie4, plus a direct REST API integration with OAuth2 onboarding at /accounting/bokio/configure, idempotency-key vouchers, token refresh five minutes before expiry, and error handling per status (401 retry, 409 ignore, 422 fail, 429 backoff). A sync job runs every 15 minutes (or on demand), POSTing entries with synced_to_bokio_at = None to Bokio with up to three retries on exponential backoff and writing the external reference back.
Financial statements are season snapshots stored at /financial-statements/ with income-statement and balance-sheet snapshots, totals (total_revenue, total_expenses, net_result, total_assets, total_liabilities, total_equity) and a draft -> finalized lifecycle with optional period lock on finalisation. Period locks at /period-locks/ block entry create, post and void within the locked date range, refuse overlapping locks (409) and can be unlocked with a reason. Consolidated reports at /accounting-reports/consolidated-income-statement and /consolidated-balance-sheet break down by org_node_id with a quarter filter and an as_of_date filter for the balance sheet.
Every operation appends to FinancialAuditLog.
Key capabilities
- AccountingEntry double-entry vouchers with chart-of-accounts mapping per tenant per provider
- Provider exports for Fortnox, Visma eEkonomi, Xero, Bokio and SIE4 with IB, UB and RES
- Bokio direct API integration with OAuth2, idempotency keys, token refresh and 15-minute sync job
- Season financial statements with income-statement and balance-sheet snapshots and finalisation lock
- Period locks that block entry create, post and void within a locked range and reject overlap
- Consolidated income statement and balance sheet per OrgNode with quarter and as-of-date filters
- Immutable FinancialAuditLog and export_generated trace on every export
In practice
After year-end the federation accountant opens /financial-statements/, generates a draft statement for fiscal 2026 — the system snapshots the income statement and balance sheet from posted vouchers — and reviews the totals. Satisfied, she finalises with create_period_lock=true; /period-locks/ writes a lock for 2026-01-01 to 2026-12-31 and any further attempts to post or void in that window return 409. She then runs /accounting-export/sie4 with fiscal_year, company_name and org_number; the auditor receives a CP437-encoded file with #KTYP, #IB, #UB and #RES sections.
In parallel she has Bokio configured for direct API push; the 15-minute sync job picks up the last unsynced vouchers, retries one 429 with exponential backoff, succeeds and writes external_refs.bokio back to each entry — the FinancialAuditLog records every step.
Features in this subsystem
32| ID | Status | Features |
|---|---|---|
| F08.10.13 | Shipped | ✅ Bokföringsmotorn — tenant-isolerad dubbel bokföring (BILL m-1 t-1, 4e608d85.m-1) — migration 00217 med ledger_account (BAS-2024-baslinjen per bok, kontotyp + standardmomssats + SRU), fiscal_year (icke-överlappande per tenant), accounting_entry + accounting_line och period_lock. Invarianterna är hårda, inte hygien: balanskravet SUM(debit_minor) = SUM(credit_minor) per verifikat (422 unbalanced_entry, bokförs aldrig), gapfritt löpnummer per (tenant, fiscal_year, series) tilldelat i transaktion vid bokföring (ett utkast förbrukar inget nummer ⇒ — i Ver.nr), oföränderlighet (ett bokfört verifikat ändras eller raderas aldrig — rättelse uteslutande via motverifikat med reverses/reversed_by-paret) och periodlåset (409 period_locked, överlappande lås 409 period_overlap). Pengar är heltal i minsta enhet (*_minor bigint) + valutakod — aldrig float. Rutter: /v1/accounting/{accounts,fiscal-years,entries,period-locks,cost-centres} + entries/{id}:post / :reverse + rapporterna income-statement/balance-sheet (svaret bär serverns egen balanced-dom + de två summorna, med periodens resultat som härledd rad så balansen går ihop utan bokslutsverifikat). Bracket-ops-filter ur whitelist, ETag/If-Match, Idempotency-Key och audit på varje mutation. Tenant-isoleringen är säkerhetsegenskapen: tenant_id injiceras server-side ur sessionen — ytan skickar aldrig ett tenant-fält, och PL:s böcker (tenant-noll) är samma rutter med plattforms-tenanten som aktiv. Capabilities accounting:read/accounting:post/accounting:admin (deny-by-default). Fixtur: go run . seed-bokforing (två böcker, tre identiteter inkl. läsar-negativet, fyra verifikatlägen per bok, ett periodlås). Kontrakt: specs/api/endpoints/accounting-ledger.md; motorns invarianter: docs/domain/accounting-flows.md §1. ✅ BILL m-1 |
| F08.10.14 | Shipped | ✅ Adminytan /bokforing — Verifikationer · Kontoplan · Resultat & balans (BILL m-1 t-2, 4e608d85.m-1) — mockupens (design/src/pages/admin/bokforing.astro) tre första flikar som levande ytor mot motorns egna rutter, uteslutande genom den genererade TS-klienten (ingen rå fetch, ingen egen lagring, inget tenant_id från klienten): *Verifikationer* (periodlåsbanner ur RIKTIGA låsrader, filterpanel mot serverns bracket-ops, tabell med SERVERNS löpnummer, — för utkastet, status som ikon + text + färg per rad, ↩-markering på BÅDA raderna i motverifikatparet, formuläret *Nytt manuellt verifikat* med radtabell + löpande summering som ren visning och *Bokför* / *Spara som utkast* / *Reversera*), *Kontoplan* (tabell, *Lägg till konto* inkl. SRU-fältet, If-Match-låst redigering, serverns 409 account_exists/409 account_in_use i klartext) och *Resultat & balans* (resultat- och balansräkning, balanskontrollen ur serverns balanced-fält, period- och kostnadsställe-väljare — den senare ur GET …/cost-centres, aldrig ur /v1/org-nodes). Ytan dömer aldrig: *Bokför* är alltid tryckbar och 422 unbalanced_entry renderas vid fältet; beloppen matas i minsta enhet (»100 = 1 krona«) utan klientomräkning; läsar-negativet ser listan men får ingen mutationsknapp renderad (aldrig en utgråad attrapp). Vy-spec med komponentträd, ⊘-tabell och den pinnade testid-mängden: specs/admin/views/bokforing.md. ✅ BILL m-1 |
| F08.10.15 | Planned | ⊘ Ännu inte byggt i greenfield — med ägare (uppdaterad efter BILL m-2). *Momsrapport/momsdeklaration* (perioder, output − input, omvänd betalningsskyldighet, OSS, periodisk sammanställning) → BILL M5; dit hör också kontoplansflikens *Chart-of-accounts standard* / *Tax residence*, som hänger ihop med momskontomappningen per land (A6). *Provider-export (Fortnox/Visma/Xero), coa_mapping och Bokio direkt-synk (OAuth2)* → BILL M4 (tenantens export-läge) — m-2 levererade SIE-4 och arkivet, inte provider-formaten. *Konsolidering uppåt i OrgNode-hierarkin och jämförelseperioder* → M5/M6. *Bilagor/kvitton på verifikat* → BILL M3. *Resultatdisposition (2098 → 2091) och periodiseringsautomat* → manuella verifikat, ingen automat byggs. Ingen av dem renderas som attrapp i adminytan: de utelämnade elementen är oklickbara aria-disabled-markeringar med sin ägare i klartext. ⏳ BILL M3/M4/M5/M6 |
| F08.10.16 | Shipped | ✅ Årsstängning, SIE-4-export och det lagstadgade arkivet — kontraktet (BILL m-2 t-1, 4e608d85.m-2) — migration 00218 med accounting_settings (den bokföringsskyldiges namn + organisationsnummer, A-8 — huset saknade kolumnen), year_end_close (utkast → finaliserat, med *fastställt av*, datum, not, låsflagga och den frysta ögonblicksbilden), accounting_archive (paketets bytes, content_hash, manifest, frist) och accounting_export_log (append-only historik), plus tre oföränderlighetstriggar i databasen och den additivt utvidgade source_type-mängden (year_end_close). Bokslutet är EN transaktion (POST …/year-end-closes/{id}:finalise): ögonblicksbilden fryses ur m-1:s egna rapportfunktioner, resultatöverföringen bokförs på 8999 → 2099 med sitt EGET löpnummer ur serieräknaren, periodlåset skapas, året stängs och carry-in rullar balanskontona (2099 → 2098) till nästa år — förvillkoren (409 draft_entries_exist, 422 org_number_required, 409 period_overlap, 409 year_end_close_already_finalised) prövas i skrivningens transaktion, och carry-ins överhopp bär alltid sin orsak. Fristen är lag, inte konfiguration (internal/bookkeeping/retention.go): varje rad bär sin juridiska hänvisning (bokföringslagen 7 kap. 2 § · HGB § 257 · Code de commerce L123-22), regeln är 31 december det kalenderår räkenskapsåret slutade + antalet år (ett brutet år som slutar 2026-04-30 bevaras t.o.m. 2033-12-31, aldrig 2033-04-30) och okänt land ger katalogens längsta frist (fail-closed). Arkivet skrivs bara av sitt svep (run-job accounting_archive_generate, G-10): paketet är en deterministisk zip med SIE-4-filen, de fyra rapporterna som JSON, verifikatindexet som CSV, bokslutet och systemdokumentationen som Markdown plus ett manifest vars coverage[] aldrig är tomt — det räknar upp vad som INTE ingår, med ägare. Rutter: /v1/accounting/{settings, year-end-closes, export/sie4, exports, archives} + fiscal-years/{id}:carry-in. Kontrakt: specs/api/endpoints/accounting-archive-export.md + specs/api/endpoints/accounting-ledger.md; runbook: docs/engineering/runbooks/bokslut-och-arkivering.md. ✅ BILL m-2 |
| F08.10.17 | Shipped | ✅ Adminytan /bokforing — Export (SIE) · Arkiv (lagstadgat) · Bokslut per säsong · verifikatdetaljen (BILL m-2 t-2, 4e608d85.m-2) — mockupens tre ⊘ m-2-element som levande ytor mot t-1:s rutter, plus arkivsektionen (bokfört tillägg mot mockupen, A-13: ett lagstadgat arkiv som inte går att ladda ner eller verifiera är ingen revisor-export). *Export (SIE)*: företagsnamn + organisationsnummer ur GET …/settings med If-Match-låst inline-redigering bakom accounting:admin, exakt två valbara format (SIE4 CP437 · Bokio SIE4 UTF-8 BOM/CRLF — Fortnox/Visma/Xero är ⊘ M4 i väljaren), räkenskapsår med serverns statusmärkning *(öppet)*/*(stängt)*/*(arkiverat)*, serieväljare, *Förhandsgranska* (serverns riktiga #FLAGGA/#SIETYP/#KONTO-rader) och *Exportera ↓* som hämtar serverns bytes genom transportens binärsöm och läser om historiken. Mockupens kryssruta *Posted vouchers only* är statisk text (regeln är serverns — parametern finns inte). *Arkiv (lagstadgat)*: en rad per stängt år med genereringstidpunkt, storlek, hash-prefix, »Bevaras t.o.m. ÅÅÅÅ-12-31« med lagrummet i klartext ur serverns fält (aldrig en klienträknad frist), *Ladda ner*, *Verifiera integritet* (serverns dom som ikon + text + färg) och den ärliga raden *»Arkivpaketet genereras«* för ett nyss stängt år. *Bokslut per säsong* i fliken *Resultat & balans*: mockupens fältmängd, *Spara utkast* och *Finalisera bokslut* med ett bekräftelsesteg som räknar upp konsekvenserna — inklusive att det inte går att ångra (det finns ingen återöppning) — och serverns 409 draft_entries_exist / 422 org_number_required / 409 period_overlap i klartext i ett role="alert", plus det frysta underlaget och carry-ins utfall. *Verifikatdetaljen* (mockupens open) på m-1:s befintliga GET …/entries/{id}: konteringsraderna, summorna, källan, kostnadsstället, motverifikatlänken åt båda håll och bokföringsspåret. Resultaträkningens rad *Periodens resultat* läser nu result_before_transfer_minor, så rapporten läses likadant före och efter en stängning. Läsar-negativet är asymmetriskt (A-9): accounting:read får exportera och läsa arkivet, men ingen inställnings- eller bokslutsknapp renderas. Vy-spec: specs/admin/views/bokforing.md. ✅ BILL m-2 |
| F08.10.18 | Shipped | ✅ PL:s abonnemang, plattformsfakturan och den bokförda betalningen — kontraktet (BILL m-3 t-1, 4e608d85.m-3) — migration 00220 med platform_subscription, platform_invoice och platform_invoice_number_counter, två oföränderlighetstriggar och den additivt utvidgade source_type-mängden (pl_invoice_issued, pl_credit_note — pl_invoice_paid fanns sedan 00217 och får sin FÖRSTA skrivare här). Raden har två parter: owner_tenant_id (PL:s tenant-noll, resolvad server-side på tenant_type='platform' och aldrig ur en kropp) och customer_tenant_id — aldrig tenant_id. Fakturametoden, inte kontantmetoden: utfärdandet bokför kundfordran (debet 1510 · kredit 3010\ 3030 · kredit 2611), betalningen bokför banken mot fordran (debet 1930 · kredit 1510, ingen momsrad — momsen redovisades vid utfärdandet), kreditnotan speglar utfärdandet och återbetalningens penningben är debet 1510 · kredit 1930. Ingen andra betalrail och ingen egen webhook: pengarna kommer hem genom F1-11:s CENTRALA rail, och m-3 bidrar med två hookar för subject_type='platform_invoice'. Löpnumret (PL-<år>-<0000>) allokeras under radlås vid :issue — ett utkast förbrukar inget nummer (invoice_number: null). En enda momsregel, med lagrummet vid raden: svensk köpare ⇒ 25 % (ML 7 kap. 1 §); varje annat land — och ett saknat land — ⇒ 422 vat_treatment_unresolved med ägaren (m-4) i klartext och NOLL skrivna rader. overdue lagras aldrig: förfallenhet är serverns härledning vid läsning, så m-6:s dunning-maskin blir statusordets enda skrivare. Rutter: /v1/sys/billing/{subscriptions,invoices} bakom husets BEFINTLIGA sys.billing.read + finance.subscription.edit (noll nya capabilities) med grindkedjan mandat → fresh-auth → reason ≥ 10 tecken → resursen → status → If-Match → belopp. Fixtur: seed-bokforings m-3-delta (ingen ny CLI-rad). Kontrakt: specs/api/endpoints/platform-subscriptions-invoices.md; bokföringsmönstren: docs/domain/accounting-flows.md §1c. | ✅ BILL m-3 |
| F08.10.19 | Shipped | ✅ Sys-ytan /intakter — abonnemangs- och fakturaregistret + källetiketterna på /bokforing (BILL m-3 t-2, 4e608d85.m-3) — D-SYS b8867909.m-10 lämnade två ⊘-platser med ägare BILL på den befintliga rutten, och m-3 häver dem i samma leverans som källan byts in: fliken *Avgifter & avstämning* får abonnemangstabellen (kund · tier · pricing_version · status som ikon + text + färg · periodslut · utestående — serverns MÄTNING, aldrig en klientsummering) med serverfilter som riktiga bracket-ops och *Nytt abonnemang* / *Säg upp* bakom grindkedjan med bekräftelsesteg och obligatorisk motivering; fliken *Fakturor & betalningar* får fakturatabellen (nr · kund · belopp · förfaller · status · not) där *Förfallen* är serverns härledning och märks som en härledning, plus detaljvyn med raderna, momssatsen per rad, providerdokumentets länk och BOKFÖRINGSSPÅRET (verifikatens serie + löpnummer i klartext) och åtgärderna *Utfärda* · *Kreditera* · *Makulera utkast*. ⊘-korten subscriptions/invoices är borttagna — ett kort som säger *"ägs av BILL"* bredvid en byggd tabell är en lögn i motsatt riktning — medan mrr/arr/churn står kvar med sin nya ägare (BILL m-12: ett abonnemangsREGISTER är inte en MRR-MODELL) och dunning/unmatched_payments/pdf_export/pricing_catalog med sina. På adminytan /bokforing namnger verifikatlistans och detaljvyns *Källa* de tre nya källtyperna i klarspråk med fakturanumret (*"Plattformsfaktura PL-2026-0007 (utfärdad)"*) som en ren regel med fristående test — ingen ny rutt, ingen ny flik, ingen länk över appgränsen. Vy-spec: specs/sys/views/intakter.md; bevis mot körande stack: tools/gate/bill-m3-e2e.mjs. ✅ BILL m-3 |
| F08.10.20 | Shipped | ✅ Momsprofilen och det VIES-styrda beslutsträdet — kontraktet (BILL m-4 t-1, 4e608d85.m-4) — migration 00222 med tax_profile (en rad per tenant: skattehemvist, momsregistrering, eget momsnummer, serverägd VIES-status + tidsstämpel + konsultationsreferens, föreningsflaggan, tröskeln och den sys-ägda oss_registered), den tenant-lösa vat_rate-katalogen (EU-standardsatser med källa och giltighetsdatum), ec_sales_line (periodisk sammanställning per köpar-VAT-nr och kalenderkvartal) och den additiva oss_rate_bps på fakturan, med DB-CHECK-vakter för reverse_charge → buyer_vat_number och oss → buyer_country + oss_rate_bps. A9-RESOLVERN ÄR EN REN FUNKTION (billing.ResolveVATTreatment): SE ⇒ standard 2500 bps · annat EU-land med VIES-giltigt köparnummer ⇒ reverse_charge (0 moms, legend, EC-rad) · annat EU-land utan giltigt nummer = B2C ⇒ standard under den statutära tröskeln (99 680 kr, mätt över BÅDA kalenderåren) och oss med köparlandets sats över den · icke-EU eller okänt land ⇒ fail-closed 422 vat_treatment_unresolved. VIES är en leverantörsneutral PORT med deterministisk fejk och dokumenterade markörer; ett portfel är ett FEL, aldrig en dom (statusen lämnas orörd, utfallet redovisas mätt i verification), och resolvern ringer aldrig synkront — den lagrade statusen är auktoriteten vid utfärdandet (A-3). Momskontot kommer ur vat_account_map (SE BAS · FR PCG · DE SKR) och OSS-momsen krediteras ett eget skuldkonto, aldrig 2611. Rutter: /v1/accounting/tax-profile (tenantens egen, accounting:read/:admin) och /v1/sys/billing/tax-profiles/{tenant_id} (PL:s kundregister, sys.billing.read/finance.subscription.edit) — två skrivvägar, EN söm och EN regel — plus GET /v1/sys/billing/ec-sales-lines. :declare byggs INTE (m-11). Fixtur: seed-bokforings m-4-delta. Kontrakt: specs/api/endpoints/tax-profiles.md; beslutsträdet med lagrum: docs/domain/invoicing.md §0b; bokföringsmönstren: docs/domain/accounting-flows.md §1d. ✅ BILL m-4 |
| F08.10.21 | Shipped | ✅ Momsytan — behandlingen och momsprofilen blir synliga (BILL m-4 t-2, 4e608d85.m-4) — ett bokfört tillägg utöver mockupen (ingen prototyp ritar en momsskärm; utan panelen kan ingen registrera en fransk kunds momsnummer, och då kan hennes faktura aldrig lösas). *sys /intakter*: fakturadetaljen får momsbehandlingen som ikon + text + färg i klarspråk (*Svensk moms 25 %* · *Omvänd betalningsskyldighet* · *OSS (köparlandets sats)*), där satsen är serverns vat_rate_bps och bara bärs när fakturans rader är entydiga; köparens momsidentitet (numret ur fakturan, VIES-statusen och tidsstämpeln ur kundens profil); RC-legenden ORDAGRANT ur vat_legend — aldrig en klientbyggd lagtext; och köparland + tillämpad oss_rate_bps. Momsprofilpanelen per kund läser GET …/tax-profiles/{tenant_id} och erbjuder redigering + *Verifiera VAT-nr* bakom hela grindkedjan (mandat → fresh-auth → motivering ≥ 10 tecken → If-Match), med VIES-utfallet renderat mätt i TRE lägen — giltigt · ogiltigt · kunde inte verifieras (det tredje är ett fel i försöket, aldrig en dom). De fail-closed-grenarna 409 oss_registration_required, 409 platform_vat_number_missing och 422 vat_treatment_unresolved renderas som åtgärdsbesked vid handlingen, aldrig som ett tomt dataläge. EC-sales förblir ⊘ med ägare BILL m-11 (radunderlaget finns och serveras; deklarationsvyn byggs inte). *admin /bokforing*: tenantens egen momsprofil i inställningsblocket i fliken *Export (SIE)* — land, momsregistrerad, VAT-nr med *Verifiera*, föreningsflaggan — bakom accounting:admin med If-Match, och oss_registered i LÄSLÄGE (en verklighetsuppgift om en registrering hos Skatteverket, sys-ägd). Vy-specar: specs/sys/views/intakter.md §BILL 4e608d85.m-4 + specs/admin/views/bokforing.md §17; bevis mot körande stack: tools/gate/bill-m4-e2e.mjs. ✅ BILL m-4 |
| F08.10.22 | Shipped | ✅ Intro-erbjudandet, Price-Lock, HICP-justeringen och förnyelsemotorn — kontraktet (BILL m-5 t-1, 4e608d85.m-5) — migration 00223 med intro-fälten på platform_subscription (intro_until, intro_price_minor, intro_list_price_minor, intro_unconditional), auto_renew, nivåändringens kolumner och tabellen subscription_price_adjustment. Gratisåret är en RABATT till 0 kr, aldrig ett uttag (A8): 0-kr-fakturan får ett riktigt nummer i den obrutna serien, beskattningsunderlag 0 och list_minor/discount_minor som revisorsspår på raden — utan betalrad, utan provider-anrop och utan verifikat, för en rabatt bokförs inte (ML 5 kap. 29 § / mervärdesskattedirektivet art. 26). Ett intro på en icke berättigad tier-grupp avvisas (422 intro_offer_not_eligible), och ett intro utan villkorslöshet likaså (422 intro_offer_requires_unconditional) — med en motprestation är erbjudandet ett byte som ska momsas till marknadsvärde. price_lock_until är en HÄRLEDNING ur signed_at + 36 månader och har därför inget API: kolumnen kan inte sättas, bara läsas. En justering prövas i ordning — avtalet aktivt → effektdatum efter låset (422 price_lock_active) → minst 90 dygns varsel (422 notice_window_too_short) → HICP-taket ur den kod-deklarerade katalogen (422 hicp_cap_exceeded, 422 hicp_reference_unavailable) — och varslet går ut i samma handling med mätt utfall: utan bokfört varsel tillämpas justeringen aldrig (fail-closed). Förnyelsemotorn (run-job platform_subscription_renewal_tick, timkadens) utfärdar förnyelsefakturan 30 dygn före periodslutet genom m-3:s EGEN :issue-söm, omvärderar momsen genom m-4:s A9-resolver vid VARJE förnyelse (ett portfel är ett fel och aldrig en dom), växlar intro → låst pris som en härledning ur intro_until, tillämpar en varslad justering bara med sitt varsel och är expired-ordets enda skrivare (renewal_declined, SYSTEMAKTÖR på audit-raden). Kundens läsfamilj /v1/tenant/billing/{subscription,invoices,invoice-settings} (platform-agreement:read/:manage, tenanten server-side) och sys-mutationerna …:set-auto-renew / :schedule-price-adjustment / :cancel-price-adjustment / :schedule-tier-change / :cancel-tier-change. Kontrakt: specs/api/endpoints/platform-subscriptions-invoices.md §BILL m-5; avtalets livscykel: docs/domain/subscription-lifecycle.md; 0-kr-fakturans form: docs/domain/invoicing.md §0c. ✅ BILL m-5 |
| F08.10.23 | Shipped | ✅ Kundens egen vy /abonnemang + prisskyddsdeltat på sys /intakter (BILL m-5 t-2, 4e608d85.m-5) — mockupen design/src/pages/admin/abonnemang.astro som en levande yta: *Aktuell nivå* (tier, pris/år, nästa förnyelse — avtalets FAKTISKA uppgifter, ingen påhittad spelarräkning), Ditt prisskydd med 🎁 *listpris − rabatt = 0* endast där ett intro FINNS, 🔒 price_lock_until ur API:t (ytan räknar aldrig 36 månader själv — låset är serverns härledning) och 📈 HICP-policyn plus den varslade justeringens gamla/nya pris, effektdatum och rätt till avgiftsfri uppsägning, *Betalhistorik och fakturor* med serverns not (A8:s revisorsspår), serverfiltrerat årsval (year[eq]) och providerdokumentets länk där den finns, och *Fakturainställningar* med If-Match, serverns 422 vid fältet och orgnr/momsnummer i LÄSLÄGE med hänvisning till Bokföring (G-14: två skrivfält för samma uppgift blir två sanningar). De fyra riktiga tillstånden: laddar · tomt vid 204 · fel · nekad som BEHÖRIGHETSBESKED (serverns 403 får aldrig se ut som »du har inget avtal«). På sys /intakter får abonnemangstabellen kolumnerna Auto-förnyelse (false = härledningen »avslutas vid periodslut«, aldrig en lagrad status) och Nästa förnyelse plus 🎁/🔒-markeringarna som ikon + text, en prisskyddspanel mot abonnemangets detalj med SERVERNS HICP-post (det finns med flit inget fält för en operatörsinmatad indexsiffra) och de fyra mutationerna bakom hela grindkedjan (mandat → fresh-auth → reason ≥ 10 → If-Match) med affordanser som följer läget; tecknandets intro-block renderas endast för federation/venue — samma grind som serverns — med en villkorslöshets-kryssruta som aldrig är förikryssad. ⊘ med ägare, aldrig döda knappar: PDF-nedladdning/-export → PDF-sidecarn · betalmetod/kort-vault → BILL m-6 · publik prislista, tier-auto-klassificering och proration → pricing-domänen · nyhetsbrevspanelen (F19.17.20) → D-NEWSLETTER · dunning/past_due → BILL m-6. Vy-specar: specs/admin/views/abonnemang.md (ny) + specs/sys/views/intakter.md §BILL 4e608d85.m-5; bevis mot körande stack: tools/gate/bill-m5-e2e.mjs. ✅ BILL m-5 |
| F08.10.24 | Shipped | ✅ Kort-valvet, utgångspåminnelsen, indrivningskaskaden och dunning-maskinen — kontraktet (BILL m-6 t-1, 4e608d85.m-6) — migration 00227 med dunning_case (PL-ägd rad OM en annan tenant: owner_tenant_id + customer_tenant_id, aldrig tenant_id), platform_card_expiry_reminder, den additiva länken platform_subscription.default_recurring_source_id och kaskadens bokföring PÅ SKULDEN (platform_invoice.charge_attempts/last_charge_attempt_at — visionens payment_failure_count byggs INTE: en andra räknare om samma försök vore en andra sanning, spec A-2). Ingen platform_card-tabell och ingen andra rail (A-1/NFR-1): ett sparat kort ÄR PAY-3:s recurring_payment_source med PlatformDirect mot PL:s eget merchant-konto, aktiverad av den CENTRALA setup_intent.succeeded-webhooken — kortmetadata kommer enbart ur provider-event, och plattformen ser aldrig mer än märke + last4 (NFR-8). Tre F1-7-svep, alla med injicerad klocka, batch-tak och single-fire i WHERE/DB: platform_card_expiry_scan (30/14/7, högst EN notis per kort och körning — det minsta förfallna fönstret bär den; single-fire är den unika nyckeln (source, exp, milestone), så ett FÖRNYAT kort re-armar påminnelserna av sig självt), platform_invoice_collection_tick (off-session-debitering genom husets ENDA rail med application_fee_minor = 0 och deterministisk nyckel; kaskadens 1h/2h/4h och taket 3 är DEKLARERADE konstanter i internal/billing med källan vid raden — aldrig konfig; ett misslyckande bokförs i en EGEN kort transaktion, för en fallen debitering får aldrig se ut som en tyst no-op) och platform_dunning_stage_tick (trappan, högst EN övergång per tick så en missad körning hinns ikapp steg för steg i stället för att hoppa dit klockan råkar peka). Öppningen är svepets, aldrig en människas: en förfallen faktura ELLER en uttömd kaskad med ett TERMINALT sista försök ⇒ EXAKT ett ärende per gäldenär (garantin är det partiella unika indexet, inte ett if) och abonnemanget flippas active → past_due i samma transaktion. Upplösningen följer anti-scopet: en betalning stänger ärendet automatiskt endast t.o.m. final_warning och först när gäldenärens SAMTLIGA förfallna fakturor är reglerade (A-11); därefter krävs POST …/dunning-cases/{id}:waive genom hela grindkedjan (mandat finance.subscription.edit → fresh-auth → reason ≥ 10 tecken → If-Match) med motiveringen ordagrant i WORM-raden. Rutter: /v1/tenant/billing/payment-method* + …/dunning-banner (medlemskaps-guardad — beloppet endast för platform-agreement:read) och /v1/sys/billing/dunning-cases* + …/cards/expiring. Fixtur: seed-bokforings m-6-delta (ingen ny CLI-rad). Kontrakt: specs/api/endpoints/platform-subscriptions-invoices.md §BILL m-6; domänreglerna med källa: docs/domain/dunning-policy.md. ✅ BILL m-6 |
| F08.10.25 | Shipped | ✅ Betalmetoden, indrivningsbannern och sys-fliken *Indrivning* (BILL m-6 t-2, 4e608d85.m-6) — admin /abonnemang: panelen Betalmetod (m-5:s ⊘ *Change payment method* HÄVD i samma leverans som källan) med kortraden *märke · •••• last4 · utgång* ur GET /v1/tenant/billing/payment-method, utgångsmärket (⏰ inom 30 d, rött vid passerad), *Byt betalmetod* som säger att kortet gäller först när leverantören bekräftat det, *Ta bort kort* med bekräftelsesteg och kortets EGEN If-Match (aldrig *), det ärliga tomläget — och aldrig ett PAN. Indrivningens felläge visar *»försök n av 3«* och nästa försök MÄTT ur avtalets collection-block; ytan räknar aldrig kaskaden själv (A-7). Admin-skalets banner (ShellHost, MfaGraceBanner-prejudikatet) är inte avvisbar — ett datum då kontot låses får inte kunna tystas av en enskild inloggad — och dess färg är SERVERNS severity (A-10: gul t.o.m. read_only, röd från pre_deletion_30d), aldrig ensam signal: stadiet står i klartext. Bannern är synlig för alla i organisationen medan beloppet och betalvägen bara skickas till platform-agreement:read-bäraren; 204 ⇒ ingen banner (frånvaro är ett svar), och ett fel på probningen renderar ingenting. sys /intakter får fliken Indrivning (⊘-nyckeln dunning togs ur serverns unavailable[] i samma leverans): stage-chips och lösta-växel som SERVERFILTER (current_stage[eq], is_resolved[eq]), kön *kund · steg · öppnat · trigger · nästa övergång* där »nästa övergång« är SERVERNS härledning, detaljens tidslinje ur stages_history med mätt notisutfall och den HÄRLEDDA fordranslistan, *Efterskänk* som endast renderas för mandatbäraren (frånvaro, aldrig en utgråad knapp) med bekräftelsesteg och serverns 422 vid åtgärden, tomläget *»Inga förbund i indrivning«* och kortlistan »kortet går snart ut« vars påminnelsestatus 30/14/7 är BOKFÖRING ur scanjobbet. Registren får dessutom abonnemangsradens indrivningsläge (*»försök n/3«*) och fakturatabellens Dunning-kolumn med sitt serverfilter dunning_stage[eq] — medan förfallenhet medvetet inte går att filtrera på: den lagras aldrig. ⊘ med ägare, aldrig döda knappar: banner i app/web → respektive yta/D-COMM · manuell ärendeöppning och self-service-efterskänkning → ingen (Icke-mål 9) · hård radering i deleted → D-GDPR/F1-9 · SEPA som sparad metod → PAY-3 · mrr/arr/churn → BILL m-12 · unmatched_payments → BILL m-10. Vy-specar: specs/admin/views/abonnemang.md §8 + specs/sys/views/intakter.md §BILL 4e608d85.m-6; bevis mot körande stack: tools/gate/bill-m6-e2e.mjs (deskriptor tools/gate/e2e.d/999-bill-m6-indrivning.mjs, strukturgrind sys/__acceptance__/bill-m6-indrivning-ui.acceptance.mjs). ✅ BILL m-6 |
| F08.10.26 | Shipped | ✅ PL:s utgiftssida (AP) — leverantörsfakturor, företagskort, kvitton och kostnadsimporten: kontraktet (BILL m-7 t-1, 4e608d85.m-7) — migration 00229 med supplier_invoice, card_transaction, cost_import och accounting_receipt, alla tenant-skopade mot samma isolationsgräns som liggaren (NFR-1). Leverantörsfakturan har två grenar: direktgrenen (:approve utan attestkrav) och F1-6-attestgrenen (requires_approval=true ⇒ ett approval-ärende, och :approve driver MOTORNS Approve — ordet approved finns inte som strängliteral i m-7:s handlare eller seed, grep-vaktat i bill_m7_source_guards_test.go). Skulden bokförs vid ATTESTEN (debet kostnadskonto (netto) + debet 2640 (moms) / kredit 2440 (totalt)) och betalningen som ett EGET verifikat (2440 → 1930) — fakturametoden, inte kontantmetoden. Avdragsrätten är en fråga till momsprofilen, aldrig en default: en bok utan momsregistrering får ingen 2640-rad. Förvärvsmomsen (reverse_charge med vat_minor > 0 ⇒ 422 reverse_charge_requires_zero_vat) bokförs som netto-nollparet 2645/2614 ur bookkeeping.VATAccountFor — aldrig en kontoliteral. Företagskortets CSV-import tolkar beloppen med exakt strängaritmetik till öre (både komma- och punktform, aldrig via float, NFR-2) och dedupar på DB-invarianten (tenant_id, external_id); :book på en okategoriserad rad ⇒ 422 uncategorized_transaction. Kvittot går genom husets media.BlobStore-port med MÄTT sha256/mime/size_bytes (ett klientpåstått hash-värde vore ett integritetsbevis ingen räknat) och utan objektlager ⇒ ärligt 503 blob_backend_unavailable, aldrig en rad utan bytes; ett kvitto i efterhand på en bokförd rad uppdaterar RADEN och lämnar verifikatets jsonb orörd (oföränderligheten vinner). Kostnadsimporten går genom den leverantörsneutrala billing.CostImportProvider vars adapter väljs ur MILJÖN (PL_BILL_COST_IMPORT_BACKEND; tom i drift ⇒ ärligt överhopp med logg, fake i local/test, okänt namn ⇒ fel — aldrig en tyst fallback) och bokförs av run-job platform_cost_import med dubbel DB-idempotens (registrets unique (tenant, scope, period) + verifikatets source_event_key); valuta ≠ bokens ⇒ failed:currency_mismatch utan verifikat (aldrig tyst FX). Fixtur: seed-bokforings m-7-delta (ingen ny CLI-rad). Kontrakt: specs/api/endpoints/accounting-payables.md; bokföringsmönstren: docs/domain/accounting-flows.md §1e. ✅ BILL m-7 |
| F08.10.27 | Shipped | ✅ Adminfliken *Utgifter* på /bokforing (BILL m-7 t-2, 4e608d85.m-7) — ett bokfört tillägg utöver mockupen: ingen app-prototyp ritar ett AP-element, och design/src/pages/visions/bill/utgifter.astro är ARKIV och därmed fältkontrakt, aldrig skärmkontrakt (dess exempeldata grep-vaktas ur produktionskällan). En flik, aldrig en ny rutt (G-12): leverantörsfakturan, kortköpet och kostnadsimporten bokförs i samma bok, av samma människa, genom samma motor som de manuella verifikaten. Kostnadsimport-panelen visar den MÄTTA summan av FAKTISKT bokförda importer inom det aktiva räkenskapsårets fönster (aldrig ett uppskattat »hittills«, aldrig en failed- eller pending-rad inräknad), senaste period/status per scope, failed-orsaken i klartext (aldrig ett rått enum) och *Importera nu* med 409 cost_import_not_configured/409 cost_import_no_backend renderade ärligt vid åtgärden — de är sanna besked om driftläget, inte fel i ytan. Leverantörsfakturor: tabellen, kedjan *Attestera → Betalmarkera*, F1-6-ärendets läge, två verifikatlänkar per betald rad, Annullera endast i registered bakom ett bekräftelsesteg med motivering, och registreringsformuläret med belopp i minsta enhet (hjälptexten »100 = 1 krona«) där total = netto + moms visas som information — knappen är alltid tryckbar och serverns 422 renderas vid fältet ur kuvertets fältpekare, för en klientgrind hade sett likadan ut ända tills den drev isär från serverns CHECK. Företagskort: CSV-dialogen med serverns MÄTTA räknare {importerade, dubbletter, per-rad-fel}, kvittokolumnen som ikon + text (»Bifogat«/»Saknas«, färg aldrig ensam — och en kvittolös rad går ändå att bokföra: ett hårt kvittokrav hade gjort flödet obrukbart varje dag objektlagret ligger nere) och kategoriseringspanelen med kvittouppladdning och det ärliga 503-läget. Tre riktiga tillstånd per sektion (laddar/tomt/läsfel) med åtgärdsfelen skilda från läsfelen, mandatet fail-closed (utan accounting:post renderas ingen mutationsknapp — aldrig en utgråad attrapp), WCAG 2.2 AA och ≥ 48 px. ⊘ med ägare, aldrig döda knappar: bankavstämning/CAMT/partially_paid → BILL m-10 · momsperioder + :declare → BILL m-11 · kostnadsdashboard/runway → BILL m-12 · regelbaserad auto-kategorisering → framtida delta · riktig Azure-adapter → go-live · sys-yta för AP-/jobbhälsa → D-SYS/BILL m-12 · OCR och e-faktura-mottagning → utanför m-7. Vy-spec: specs/admin/views/bokforing.md §18; bevis mot körande stack: tools/gate/bill-m7-e2e.mjs (deskriptor tools/gate/e2e.d/999-bill-m7-utgifter.mjs, strukturgrind api/__acceptance__/bill-m7-yta-kontrakt.acceptance.mjs). ✅ BILL m-7 |
| F08.10.28 | Shipped | ✅ Tenant-bokföringens lägen och RevenueEvent-auto-feeden: kontraktet (BILL m-8 t-1, 4e608d85.m-8) — migration 00230 lägger lägesfälten (mode, feed_toggles) ADDITIVT på m-2:s BEFINTLIGA accounting_settings-rad (ingen andra tabell, ingen andra ETag-serie) och registret revenue_feed_event med sin idempotensnyckel. Ingen extern create-endpoint finns: intäktsverifikat med feed-källtyper skapas ENBART av svepets bokföringssöm, bookkeeping.ManualSourceTypes() förblir {manual, opening_balance, accrual} (grep-vaktat i bill_m8_source_guards_test.go), och mottagningen sker genom RevenueEventIngestor-sinken som installeras på VARJE processor huset konstruerar — en domän skriver aldrig direkt i en annan tenants böcker. Svepet accounting_feed_ingest_tick är garantin (det hittar sitt arbete självt): entitlement-prövning (ej berättigad ⇒ raden LÄMNAS pending, ingen räknare bränns) → total, kod-deklarerad källmappning (ett ord utanför den ⇒ failed:unknown_source_type, aldrig en gissning) → momsdomen genom m-4:s EGEN säljarregel med beloppen ur eventets LÅSTA vat_lines och kontot ur bookkeeping.VATAccountFor (SE 26xx · FR 4457 · DE 1776 — ingen kontoliteral i handlaren) → verifikatet genom m-1:s EGNA sömmar med debet provider-fordran (pengarna står hos betalväxeln tills payout; 1930 direkt vore ett påstående om ett bankkonto som inte fått pengarna — kvittningen är m-10:s). En split ger ETT verifikat PER mottagarnod med egen source_event_key; member_fee momsas aldrig (B3); valuta ≠ bokens ⇒ failed:currency_mismatch (aldrig tyst FX); försökstaket är deklarerat och :requeue är människans väg tillbaka efter en åtgärdad orsak. Plus tröskelsvepen accounting_vat_threshold_scan (A2) och accounting_cash_register_scan (B1), som mäter och flaggar men aldrig beslutar — vat_registered är en verklighetsuppgift och rörs aldrig av ett svep. Fixtur: seed-bokforings m-8-delta (ingen ny CLI-rad; feed-innehållet drivs av en RIKTIG betalkedja, aldrig handskrivna rader). Kontrakt: specs/api/endpoints/accounting-feed.md; mönstret: docs/domain/accounting-flows.md §1f. ✅ BILL m-8 |
| F08.10.29 | Shipped | ✅ Adminfliken *Auto-feed* på /bokforing (BILL m-8 t-2, 4e608d85.m-8) — mockupens auto-feed-spår byggt + ett mockuplöst ytdelta, båda bokförda: prototypen ritar källfiltrets *"Auto-feed (revenue events)"* och tomlägestexten *"…wait for the first auto-feed event"* (nu byggda som fältkontrakt respektive riktigt tomläge), men ingen skärm för bokföringsläge och ingen feed-detaljvy — panelen byggs därför på de BYGGDA /bokforing-mönstren. Mockupens kioskrad med kontot 3510 följs inte (3510 är *Fakturerade kostnader* i BAS-2024; kioskintäkten bokförs mot 3610 — riktiga BAS-konton vinner, och literalen grep-vaktas ur produktionskällan). Lägespanelen visar serverns läge, entitlementet i klartext och per-käll-togglarna; växeln är tryckbar även utan tilläggstjänsten och SERVERNS 409 feature_disabled renderas ärligt vid åtgärden — en knapp ytan själv låst hade sett likadan ut ända tills entitlementet ändrades under dess fötter. I plattformskontexten finns ingen växel: läget är härlett och en PUT svarar 409 platform_mode_derived (spec A-5). Feed-spåret visar registrets rader med källa (liggarens ord i klartext + producentens inom parentes), belopp ur heltalet i minsta enhet, mottagarnod, status som ikon + text + färg, skälet i svensk klartext MED serverns kod (momsdom · hint-utfall · failure_reason), parkeringens förklaring (»källan är avslagen« ≠ »väntar på nästa svep«), verifikatlänk till m-1:s detaljvy och :requeue på en felad rad bakom ett bekräftelsesteg — en bokförd rad rättas med motverifikat, aldrig genom en om-bokföring av samma pengar. Filtren är kontraktets bracket-ops och serverfiltrerar. Tre riktiga tillstånd, mandatet fail-closed i affordansen (aldrig en utgråad attrapp), WCAG 2.2 AA och ≥ 48 px. ⊘ med ägare: 409 feature_disabled renderad i webbläsaren → seed-bokforing/t-1 (FRA saknar en inloggningsbar accounting-aktör; koden bevisas i Go mot riktig Postgres, och att panelen renderar serverns dom vid åtgärden bevisas med en riktig 412 stale_version) · payout-avstämning → m-10 · momsperioder → m-11 · export-lägets integrationslager → m-9 · dashboards/runway → m-12. Vy-spec: specs/admin/views/bokforing.md §19; bevis mot körande stack: tools/gate/bill-m8-e2e.mjs (deskriptor tools/gate/e2e.d/999-bill-m8-autofeed.mjs, strukturgrind api/__acceptance__/bill-m8-yta-kontrakt.acceptance.mjs). ✅ BILL m-8 |
| F08.10.30 | Shipped | ✅ De externa bokföringsmålen — kontoplansmappningen, filexporterna och Bokio-synken: kontraktet (BILL m-9 t-1, 4e608d85.m-9) — migration 00231 med coa_mapping (per tenant + provider, mjuk radering och unik aktiv nyckel som partiellt index), accounting_integration (en rad per tenant + provider: läget sie4_only|api, company_ref, anslutningsstatus, förseglat tokenpar i credential_sealed och hashat engångs-oauth_state), accounting_sync_run (körningarna med sina MÄTTA räknare) och accounting_entry_sync (registret, unique (tenant_id, entry_id, provider)), plus exportloggens tre nya formatord fortnox_csv|visma_txt|xero_csv i m-2:s BEFINTLIGA CHECK (additivt — SIE-raden bryts aldrig). KONTOPLANSMAPPNINGEN (GET/PUT /v1/accounting/coa-mappings…) med :seed-identity för Fortnox och Visma — ensure-semantik, en rad någon redigerat rörs ALDRIG, svaret är MÄTT {created, skipped} — medan Xero med flit saknar identitetsväg (422 identity_mapping_not_available: Xeros standardkontoplan är inte BAS, och en gissad kod hade fått mottagaren att bokföra fel). DE TRE FILEXPORTERNA (GET /v1/accounting/export/{provider}) är FAIL-CLOSED på en komplett mappning (422 coa_mapping_incomplete med unmapped_accounts[]; preview=true skriver INGENTING) och renderas ur husets deklaration — Fortnox ; + decimalkomma + BOM + CRLF · Visma TAB + decimalkomma + BOM + CRLF · Xero , + decimalpunkt + UTAN BOM + CRLF, endast posted verifikat, beloppen ur heltalet i minsta enhet (12345 öre ⇒ 123,45 respektive 123.45, aldrig via float) — och varje verklig export går genom m-2:s EGEN exportsöm, så exporthistoriken förblir EN sanning om vad som lämnat huset. /export/sie4 behåller sitt företräde: Bokios dialekt är m-2:s ?dialect=bokio, ingen andra SIE-rutt byggs. BOKIO-SYNKEN bakom den leverantörsneutrala porten ExternalLedgerClient med sluten utfallsvokabulär (ok · already_exists · unauthorized · unprocessable · rate_limited · transient · network): anslutningskedjan :configure → :connect → callback → :test-connection med engångs-state (10 min, 422 oauth_state_invalid opakt) och förseglat token som ALDRIG lämnar processen (varken svar, audit-diff — byggd av en EXPLICIT redaktionslista — eller logg bär det; klient-id/hemlighet finns aldrig i databasen, bara den opaka env:-pekaren), och svepet accounting_bokio_sync_tick som hittar sitt arbete självt (*/15 * * * *, claim med for update skip locked, EN token-förnyelse per våg, Idempotency-Key = verifikatets id, kaskaden 1 min · 5 min · 25 min med taket 3, rate_limited avslutar VÅGEN, :requeue är människans väg tillbaka och svarar 409 sync_entry_not_failed på en redan skickad rad). Registret ligger utanför verifikatet (spec A-2): accounting_entry.external_refs HÄRLEDS vid läsning och har INGEN skrivare (grep-vaktat) — liggaren är oföränderlig. Adaptern väljs ur miljön (PL_BILL_BOKIO_BACKEND: fake i local/test · tom eller none ⇒ 409 integration_backend_unavailable respektive skipped_no_backend · okänt namn ⇒ ärligt fel, aldrig en tyst fallback), och den riktiga Bokio-adaptern är go-live-arbete med bokförd ägare — den är en ren tilläggsfil som mappar 2xx/409/401/422/429/5xx/timeout mot vokabulären. Fortnox/Visma/Xero som DIREKTSYNK och import FRÅN ett externt system är uttalade icke-mål. Fixtur: seed-bokforings m-9-delta (ingen ny CLI-rad; mappningar, en medvetet ofullständig Xero-mappning, en riktig Fortnox-export och en ansluten Bokio-koppling med EN fallen registerrad — allt genom sömmarna, tokenet passerar aldrig seeden). Kontrakt: specs/api/endpoints/accounting-integrations.md; mönstret: docs/domain/accounting-flows.md §1g; operatörens runbook: docs/engineering/runbooks/bokio-synk.md. Ytan: adminfliken *Export (SIE)*:s mappnings- och Bokio-panel är byggd i t-2 — se F08.10.31. ✅ BILL m-9 |
| F08.10.31 | Shipped | ✅ Adminflikens tre m-9-element på /bokforing *Export (SIE)* (BILL m-9 t-2, 4e608d85.m-9) — mockupen ritar alla tre och de är nu byggda som ett DELTA på den befintliga fliken (G-10: ingen ny rutt — exportens val, dess förutsättning och dess automatiserade motpart hör ihop på samma skärm, så den som får 422 coa_mapping_incomplete kan rätta felet utan att navigera bort). (1) FORMATVÄLJAREN — fem alternativ i mockupens ordning (standard · sie4_bokio · fortnox_csv · visma_txt · xero_csv) där orden är kontraktets enum och etiketterna presenterns (ett nytt format utan svensk etikett gör enhetstestet rött i stället för att visa ett rått maskinord); *Förhandsgranska* VARNAR ur preview.unmapped_accounts[], och *Exportera ↓* — som aldrig gråas ut av ytans egen bedömning (ingen förhandsdöming; en klientgrind hade dolt serverns regel) — renderar serverns 422 coa_mapping_incomplete som feltillståndet *»Exporten avbröts — N konton saknar mappning: …«* byggt ur svarets missing_accounts[], med länken *Öppna kontoplansmappningen →* som filtrerar tabellen till exakt de kontona; m-2:s två ⊘-attrapper är BORTA (en attrapp bredvid en levande sektion är två sanningar). (2) KONTOPLANSMAPPNING — BAS-konto · Namn · Fortnox · Visma eEkonomi · Xero · [redigera] pivoterad per konto ur GET /v1/accounting/coa-mappings (tom cell = omappad som ikon + text, aldrig färg ensam; de konton servern stoppade exporten på finns med som rader även utan en enda mappning), radredigering med If-Match = "<id>:<version>" ur listradens egna fält, och *Fyll med identitet* per BAS-nativ provider som visar serverns MÄTTA {created, skipped} medan Xeros 422 identity_mapping_not_available renderas ärligt (knappen finns, domen visas). (3) BOKIO DIREKTSYNK (API) — statusbadgen ur serverns backend/connection_status där backend: none ⇒ *»Ingen adapter i miljön«* VINNER över anslutningsläget (det är inte samma sak som *Frånkopplad*, och skillnaden avgör om det är någon idé att trycka *Anslut*), synkläge/företags-id/senaste synk, *Anslut* mot serverns egen authorize_url med landningen ?bokio=ansluten kvitterad (ytan bygger aldrig en auktoriserings-URL själv), *Testa anslutning* med MÄTT svar inline, *↻ Synka nu* (202 ⇒ körningen i loggen, 409 sync_already_in_progress visat ärligt), *Koppla från* med bekräftelsesteg, körningsloggen och registrets failed-rader med *Försök igen* (:requeue erbjuds bara i felläge — serverns 409 sync_entry_not_failed speglas, aldrig ersätts, och försökstaket sägs rakt ut). Ytan hanterar ALDRIG ett token-fält (NFR-2; drivrutinens vakt bygger fältnamnen ur fragment så gate-källan själv inte bär dem), skriver aldrig ett statusord (etikettkartor över serverns koder), skickar aldrig ett tenant_id (boken resolvas server-side) och går uteslutande genom den genererade klienten (ingen rå fetch, ingen egen klientlagring). Tre riktiga tillstånd per sektion, mandatet fail-closed i affordansen — men aldrig fail-closed på serverns dom. Bokförd avvikelse: ingen 409 för en dubblettmappning finns att rendera — unikheten är en DB-invariant (partiellt unikt index) och skrivvägen en upsert, så regeln står i klartext i sektionshuvudet i stället för som ett fel som aldrig inträffar. ⊘ med ägare: sys-yta för synk-/jobbhälsa → m-12 (Icke-mål 4) · :requeue-mutationen i drivrutinen → t-1 (fixturens enda failed-rad får inte förbrukas och fejkens felinjektion är process-intern utan env-flagga; affordansen, bekräftelsesteget och serverns icke-muterande domar mäts i stället, och omköningen bevisas i TestBillM9 mot riktig Postgres) · backend: none-copyn renderad i webbläsaren → grindriggen (inbakad NEXT_PUBLIC_API_URL; kontraktets svar mäts mot en riktig adapterlös sidecar och copyns härledning är en ren, enhetstestad regel) · Fortnox/Visma/Xero som direktsynk → framtida BILL-milstolpe (m-9 ger dem filexport). Vy-spec: specs/admin/views/bokforing.md §20; bevis mot körande stack: tools/gate/bill-m9-e2e.mjs (deskriptor tools/gate/e2e.d/999-bill-m9-externa-mal.mjs, strukturgrind api/__acceptance__/bill-m9-yta-kontrakt.acceptance.mjs, presentertester admin/lib/__tests__/bokforing-m9-presenter.test.ts). ✅ BILL m-9 |
| F08.10.32 | Shipped | ✅ Adminfliken *Avstämning* på /bokforing + sys /intakter:s två HÄVDA ⊘ som läsvyer (BILL m-10 t-2, 4e608d85.m-10) — mockupens flik *Reconciliation* i design/src/pages/admin/betalningar.astro är byggd, men som en FLIK på /bokforing (A-8: kontoutdraget matchas mot verifikat, skriver i liggaren och låser en avstämning — det hör till bokföringen) medan /betalningar LÄNKAR dit i stället för att dubblera ytan: två ytor för samma beslut har ingen ansvarig. Importpanelen har EXAKT husets två format (CAMT.053 (ISO 20022) · CSV — husets form) — formatet anges, det gissas aldrig, för ett format som gissas fel importerar fel belopp TYST — bankkontona är bokens EGNA aktiva tillgångskonton, SHA-256-dubblettskyddet och CSV:ns kolumnkontrakt står i klartext, och serverns {lines, matched, unmatched, skipped_duplicate, deduplicated, file_retained} renderas ärligt (en om-import är ett 200 deduplicated: true, inte »0 rader« som ser ut som ett fel). De fyra saldokorten är serverns balance_check — utan saldon står varje kort på ett ärligt streck, aldrig en nolla som ser mätt ut — och låsknappen är ALLTID tryckbar: låsets tre domar (409 statement_has_unmatched_lines med antalet, 409 statement_unbalanced med differensen, 422 statement_balances_missing) är SERVERNS och renderas VID åtgärden, för en knapp ytan låste på sin egen tolkning hade dolt VILKEN dom som gällde. Matchningsinboxen bär mockupens färgprick men aldrig som ensam signal: konfidensen står i TEXT och har bara TVÅ mätta nivåer (10000 referens/OCR eller människans beslut · 8500 belopp + datum ±3 d) — en tredje, lägre nivå hade varit ett förslag som ser mätt ut — orsaken står i klartext ur serverns EGEN kod, förslagskolumnen visar bara det servern matchade, och treknappsflödet *Matcha · Bokför · Ignorera* är STEG med bekräftelse där ingen knapp förhandsdöms (422 amount_mismatch, 422 amount_exceeds_remaining, 422 account_inactive och skälets längd är serverns domar). *Ta bort matchningen* renderas endast för en verifikatmatchning — en matchning som SKREV i böckerna rättas med motverifikat, och servern svarar 409 bank_line_wrote_ledger. Payout-registret visar brutto/avgift/netto som serverns heltal, items[] med sin klass per transaktion, booked_with_residual som VARNING med residualen (den göms aldrig) och :requeue bara på en FALLEN rad; summary-tabellen ger provider · providernetto · bokfört · drift · dom med toleransen som SERVERNS deklarerade tal. Verifikatlistan, formuläret och exporten får serien B — bankhändelser. sys /intakter häver de två ⊘-korten unmatched_payments och provider_reconciliation i samma leverans som källan byts in (m-3/m-6:s prejudikat): tabellen *Omatchade bankbetalningar (PL)* med serverns orsak och hänvisningen att matchningen görs av PL:s bokförare på /bokforing, och kortet *Avstämning mot leverantören* med domen som ikon + text + färg — helt utan mutationsaffordans (A-9: visionen gör sys läs-/övervakning, aldrig redigering av någon tenants böcker), och utan en rad för en clearingtjänst huset saknar adapter för. ⊘ med ägare, aldrig döda knappar: regeltabellen (*Custom auto-matching rules*) och split-matchning → egen BILL-uppföljning · bankspecifika CSV/OFX/SIE4/BGMax → go-live · formatgissning och Jaro-Winkler-fuzzy → bokförda avvikelser · sys-matchning → ägarbeslut · den lyckade fakturaregleringen i L4 → t-1 (fixturens enda obetalda faktura är m-3:s och m-5/m-6 vilar på dess läge; kedjan bevisas i TestBillM10BankLineSettlesInvoice och i t-1:s kontraktssvit). Vy-specar: specs/admin/views/bokforing.md §21 + specs/sys/views/intakter.md §BILL 4e608d85.m-10; bevis mot körande stack: tools/gate/bill-m10-e2e.mjs (deskriptor tools/gate/e2e.d/999-bill-m10-avstamning.mjs, strukturgrind api/__acceptance__/bill-m10-yta-kontrakt.acceptance.mjs). ✅ BILL m-10 |
| F08.10.01 | Shipped | Sverige - Fortnox-export — semikolonseparerad CSV med VER/Serie/Vernr/Datum/Text/Konto/Debet/Kredit. Svenskt decimalformat (komma). Exporterar endast bokförda verifikationer. Använder per-tenant kontoplansmappning. Omtolkad — byggd i F08.10.30 (BILL m-9), men FAIL-CLOSED på komplett mappning ✅ PL-F0810a |
| F08.10.02 | Shipped | International - Xero-export — kommaseparerad CSV med *Date/*Description/*AccountCode/*Debit/*Credit/TaxType/Reference. Internationellt decimalformat (punkt). Per-tenant kontoplansmappning. Omtolkad — byggd i F08.10.30 (BILL m-9); TaxType-mappningen deklarerad, ingen identitetsväg ✅ PL-F0810a |
| F08.10.03 | Shipped | Sverige - Visma eEkonomi-export — tabbseparerad textfil med VER/Serie/Vernr/Datum/Text/Konto/Debet/Kredit/Projekt/Resultatenhet. Svenskt decimalformat. Stödjer API-synkronisering via integrationsconfig. Omtolkad — filexporten byggd i F08.10.30 (BILL m-9); Visma som DIREKTSYNK är icke-mål ✅ PL-F0810a |
| F08.10.04 | Shipped | Per-tenant chart of accounts mapping — ChartOfAccountsMapping-samling tenant-scopad. Översätter interna BAS-konton till externa leverantörskonton. En aktiv mappning per provider per tenant. CRUD med duplikatskydd (409). Omtolkad — byggd i F08.10.30 (BILL m-9) som coa_mapping med mjuk radering + unik aktiv nyckel ✅ PL-F0810a |
| F08.10.05 | Shipped | SIE4-export med IB/UB/RES — utökad SIE4-export (GET /accounting-export/sie4) med #KTYP kontotyper, #IB ingående balanser, #UB utgående balanser, #RES resultaträkningskonton. CP437-encoding, fiscal_year/company_name/org_number-parametrar. ✅ PL-F0810b |
| F08.10.06 | Shipped | Bokslut per säsong — generering av bokslut (financial statements) med resultaträkning- och balansräkningsögonblicksbilder. Status draft→finalized livscykel. Valfri periodlåsning vid finalisering. CRUD /financial-statements/. ✅ PL-F0810b |
| F08.10.07 | Shipped | Revisionsspår och låsning av perioder — periodlåsning förhindrar skapande/bokföring/makulering av verifikationer inom låst datumintervall. CRUD + check + unlock /period-locks/. Överlappande lås avvisas. Komplett audit trail via FinancialAuditLog. ✅ PL-F0810b |
| F08.10.08 | Shipped | Resultaträkning och balansräkning per OrgNode — konsoliderade rapporter per kostnadställe. GET /accounting-reports/consolidated-income-statement och /consolidated-balance-sheet med org_node_id-nedbrytning. Kvartalsfilter. ✅ PL-F0810b |
| F08.10.09 | Shipped | Bokio SIE4-export — Bokio-specifik SIE4-variant med UTF-8 BOM, CRLF-radslut, BAS-2024-validering (1000–8999), obligatoriskt #ORGNR. GET /accounting/bokio/export/sie4. Omtolkad — Bokio-dialekten bor i m-2:s /export/sie4?dialect=bokio; ingen egen rutt (BILL m-9) ✅ PL-T078 |
| F08.10.10 | Shipped | Bokio direkt-API — OAuth2-onboarding + synkron/batch-POST av verifikationer till Bokio REST API med idempotency-nycklar. Token-refresh 5 min före expiry. Error-hantering: 401 retry, 409 ignore, 422 fail, 429 backoff. Omtolkad — porten + fejken byggda i F08.10.30 (BILL m-9); riktig adapter = go-live ✅ PL-T078 |
| F08.10.11 | Shipped | Bokio sync-jobb — batch-sync var 15 min (eller on-demand), plockar entries med synced_to_bokio_at is None, POST till Bokio per entry, markerar synced med external_refs.bokio. Max 3 retries med exponentiell backoff. Omtolkad — run-job accounting_bokio_sync_tick i F08.10.30 (BILL m-9); status utanför verifikatet ✅ PL-T078 |
| F08.10.12 | Shipped | Bokio onboarding-flow — POST /accounting/bokio/configure (company_id, sync_mode api/sie4_only), OAuth-URL för API-läge, test-connection via get_fiscal_years(). Admin-vy med provider-kort, Sync Now-knapp, SIE4-nedladdning. Omtolkad — :configure/:connect/:test-connection i F08.10.30 (BILL m-9); adminpanelen → t-2 ✅ PL-T078 |