* feat(skatteverket): production-ready momsdeklaration submission
Brings the Skatteverket extension up to a state where it can ship moms
declaration submission to Vercel production. Verified end-to-end against
SKV's Komplett testtjänst — all 8 momsdeklaration operations tested
(kontrollera, spara/hämta/radera utkast, lås/lås upp, hämta inlämnade,
hämta beslutade) plus signing-link return.
Bundles three coherent changes:
1. Skatteverket extension (the main work)
- extensions.config.json: enable `skatteverket`, drop `invoice-inbox`
and `ai-agent` (those were enabled in config but lacked AWS env vars
in prod, so they loaded but failed at runtime)
- lib/reports/vat-declaration.ts: extend ACCOUNT_RUTA to populate
Ruta 06 (uttag 3401–3403), Ruta 20–24 (reverse-charge bases from
4xxx cost accounts), Ruta 50 (import 4545–4547), and Ruta 42
(3404/3994/3980); delete the supplier-type heuristic that made
Ruta 20 and Ruta 23 always 0
- extensions/general/skatteverket/lib/token-store.ts: work around
three real prod schema-drift issues — wrong column on read/delete
(was `company_id`, schema only has `user_id`), missing
UNIQUE(user_id) constraint that makes UPSERT fail (switched to
DELETE+INSERT), missing RLS policies (switched to service-role
client). Refresh path now reuses existing row's company_id when
none is passed.
- extensions/general/skatteverket/index.ts: 9 sites switched from
ctx.companyId to ctx.userId for the token-store key; pass
companyId from the OAuth callback
- extensions/general/skatteverket/types.ts + components/reports/
SkatteverketPanel.tsx: align field names with v1.0.24 RAML
(signeringsLank/kontrollResultat/resultat/kod/status/beskrivning).
Without this, the signing link never displayed.
- SkatteverketPanel: add Lås upp + Radera utkast + Hämta utkast +
Hämta beslut buttons so the full lifecycle is reachable from the UI
- lib/reports/__tests__/vat-declaration.test.ts: rewritten to match
the refactored calculator; new fixtures for cost-account-based
reverse charge (Ruta 20/21/22/23/24), Ruta 50 import, Ruta 06
uttag, Ruta 42 expansion; SKV §4.1.1.4 cross-field contract checks
- supabase/migrations/20260428120000_skatteverket_tokens_user_id_unique.sql:
idempotently adds the missing UNIQUE(user_id) constraint
- scripts/*: dev-only helpers used during the prod-of-test
verification (create test company, seed VAT data, inspect token
state, etc.)
2. Journal-entries cancelled-status filter
- app/api/bookkeeping/journal-entries/route.ts: when no status filter
is supplied, exclude `cancelled` entries by default
- supabase/migrations/20260428153500_journal_entries_with_related_exclude_statuses.sql
3. Swedish e-invoicing skill (reference docs only — no runtime code)
- .claude/skills/swedish-e-invoicing/
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(skatteverket): address PR review findings
- panel: handleFetchDraft read `result.data?.last` (typo) — switched to
`result.data?.locked` to match the field defined in
SkatteverketUtkastResponse and the v1.0.24 RAML. The "(låst)" suffix on
the success message would silently never appear before this fix.
- api-client: getValidToken had no concurrency guard, so two parallel
SKV requests from the same user could both call /token with the same
refresh_token. SKV rotates the refresh_token on first use, so the
second call would 401 with REFRESH_EXHAUSTED-adjacent failures. With
the new 6-button UI on SkatteverketPanel, rapid clicks made this a
realistic trigger. Added an in-process Promise map keyed on userId
that coalesces concurrent refresh attempts; cross-process races are
mitigated by re-reading tokens inside the critical section before
calling refreshAccessToken (if another process refreshed already, we
use the newer token instead of burning the old refresh_token).
- migration 20260428120000: dedup query used `created_at < max(...)`,
which failed to remove duplicates inserted in the same second. The
subsequent ALTER TABLE … ADD CONSTRAINT would then abort. Switched
to ctid (Postgres physical row identifier) to break timestamp ties.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(skatteverket): throw on token-store SELECT error before destructive DELETE
The company_id pre-read in storeTokens used destructuring that discarded
the error field. If the service-role SELECT failed for any reason (network
blip, overloaded DB, transient permissions issue), `existing` became null,
`resolvedCompanyId` stayed undefined, and execution fell through to the
DELETE. The old row got deleted successfully, then the INSERT omitted
company_id and failed with the NOT NULL constraint violation — leaving
the user with no token row at all and forcing a fresh BankID handshake.
Now we capture the SELECT error and throw before the DELETE runs.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
12 KiB
Implementation Guide, Building Peppol into a Swedish Accounting Product
This reference is the practical builder's guide. Open-source library landscape, common rejection patterns, build-vs-buy economics, validation in CI, and a concrete phased plan for a new Swedish accounting platform.
Open-source library landscape
The Java ecosystem dominates Peppol. Most production stacks are JVM.
Java, the canonical stack
- Oxalis (https://github.com/OxalisCommunity/oxalis), originally Norwegian. Oxalis 6.x is end-of-life Dec 2025.
- Oxalis-NG (https://github.com/OxalisCommunity/oxalis-ng), successor, Apache 2.0. Build on this.
- Oxalis-AS4 7.x (https://github.com/OxalisCommunity/oxalis-as4).
- Helger phase4 (https://github.com/phax/phase4), Apache 2.0 AS4 client + server. Most comprehensive alternative to Oxalis.
- Helger phoss-smp, production-grade SMP server.
- Helger peppol-commons, identifiers, codelists, SBDH, SMP/SML clients.
- Helger phive + phive-rules, validation engine and pre-built rules (Schematron compiled to fast XSLT).
- Helger ph-ubl, ph-cii, ph-sbdh, JAXB models for UBL 2.1, UN/CEFACT CII, SBDH v1.2.
- phase4-peppol-standalone, Spring Boot 3 reference implementation. Template, not turn-key.
Trust stores updated to G3-only late 2025. Verify version when integrating.
Python
- invoice-x, UBL/CII generation.
- drafthorse, Factur-X (relevant for German/French cross-border).
- lxml + Schematron-via-saxon-HE for validation.
- No production-grade AS4, bridge to Java via subprocess, container, or REST microservice.
JavaScript / TypeScript
- No mature library. Generate types from UBL 2.1 XSDs (
xsdata,xsd2ts,xmlbuilder2) and validate via Schematron-as-WASM or call out to Java. Most TS shops use Storecove or another reseller for this reason.
.NET
UblLib.Bis3, UBL Peppol BIS 3 generation/parsing.- Several commercial SDKs (Storecove, Pagero).
- Microsoft Dynamics has built-in BIS 3.0 support.
Common rejection patterns, the high-yield checklist
The seven failure modes that cause 80% of production rejections:
- BR-CO-15 rounding mismatches between line totals and tax-inclusive totals. Always use BigDecimal/Decimal with explicit scale; round at the boundary, never in intermediate steps.
- VAT category code rule violations, mixing
Slines with aZsummary, orAEwithoutVATEX-EU-AEreason code. - Missing or malformed BT-10 BuyerReference for Swedish public sector, each authority has its own format. Maintain a per-buyer-Peppol-ID regex map and validate at compose time.
- EndpointID
schemeIDmismatch with what's published in the receiver's SMP. Verify via Peppol Lookup Service before sending. - Date format errors, UBL requires
YYYY-MM-DDxs:date. - Decimal separator and locale serialisation bugs, period only, no thousand separators,
Locale.ROOT/InvariantCulture. - Swedish character encoding, UTF-8 throughout. Many ERPs still serialise å/ä/ö with Windows-1252.
Plus:
- SBDH C1 country code mandatory since January 2024.
- Attachments via
cbc:EmbeddedDocumentBinaryObjectshould stay under 10 MB despite the formal 100 MB AS4 ceiling. - Multi-currency requires
TaxCurrencyCodeplus a SEK-equivalent BT-111 in a secondTaxTotal. - Missing F-skatt declaration (SE-R-005) is the single most common Swedish rejection.
- Forbidden PaymentMeansCode 56 / 50 instead of 30 with
SE:BANKGIRO/SE:PLUSGIRO.
Validation in CI
Three-layer validation, in this order:
[XSD] → [EN 16931 Schematron] → [PEPPOL-EN16931-UBL.sch with SE-R-* overlay]
Run all three on every UBL artifact in CI. Use Helger phive-rules or run Schematron via Saxon-HE.
# Example: Helger CLI
java -jar phive-rules-peppol-billing-3-cli.jar \
--rule "urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0" \
invoice.xml
Validate at three points in your pipeline:
- Immediately after UBL generation locally, fail fast.
- Before handoff to the Access Point, last chance before AS4 transmission.
- On receive in your inbound flow before bookkeeping, catches malformed inbound from less-strict APs.
Many AP rejections happen post-send when the receiver's MLR comes back hours later, front-load validation aggressively.
Test environments
- Peppol Test Network,
acc.edelivery.tech.ec.europa.euSML zone. Free for OpenPeppol members. - Helger Peppol Practical, https://peppol.helger.com, REST + UI validator.
- Storecove peppolvalidator.com, error code lookup.
- EC DG GROW eInvoicing validator, https://itb.ec.europa.eu/invoice
- Norwegian validator, https://anskaffelser.no/verktoy/validator (also useful for Sweden).
- DIGG testbädd, https://www.digg.se/digitala-tjanster/peppol/peppol-testbadd
Build vs buy, the economic break-even
Reseller cost ~€0.10/invoice (mid-volume Storecove) vs. own-AP fixed cost ~€30k/year (membership + cert + ops + staff time).
break-even = €30,000 / €0.10 = 300,000 invoices/year
≈ 3,000–5,000 active SME customers × 50–100 invoices/month
For receive there is no economic case to build initially, Storecove free receive or own Oxalis-NG on a small VPS dominate.
For send, only build above ~25k invoices/month sustained (i.e. ~300k/year). Below that, reseller is cheaper, faster, multi-mandate, and lower ops burden.
The reseller landscape compared
| Reseller | Per-doc | Multi-mandate | DX | Sandbox | Best for |
|---|---|---|---|---|---|
| Storecove | €0.05–€0.30 | Excellent (Peppol + IT SDI + FR PA + BE + DE + PL KSeF + DBNAlliance) | Single REST API, OpenAPI spec | Free 30-day | Embedding in ERP/SaaS |
| Pagero (TR) | EUR 0.20–0.80 + €5–20k/yr | Excellent | Multiple APIs, enterprise-flavoured | Yes | Large enterprise |
| InExchange | SEK 1–5 + 200–400/mo | Nordic | REST | Yes | SE-only SME, Visma ecosystem |
| Maventa (Visma) | SEK 0.50–3 | Nordic+ | REST + SOAP | Yes | Visma ecosystem |
| Galaxy Gateway, Babelway, SBSCon, Comarch, Edicom, Sovos, Avalara, Tradeshift, Basware, Seeburger, Esker | Various | Excellent | Enterprise-tier | Varies | Multi-mandate enterprise |
Recommended stack for a new Swedish accounting platform
Application core (bookkeeping engine, UI, API)
→ typed UBL/EN 16931 generator (ph-ubl JAXB / xsdata-Python / UblLib.Bis3)
→ local Schematron validator (peppol-bis-invoice-3 + phive-rules)
→ split path:
RECEIVE: own Oxalis-NG AP (€100/mo Hetzner + €4,400 yr-1 OpenPeppol fees)
SEND: Storecove API (until ~25k invoices/month, then own phase4 AP)
This hybrid is the dominant strategy:
- Oxalis-NG receive on day one is open-source-friendly, scales linearly without per-doc fees, and gives genuine "native Peppol receive" differentiation that incumbents (white-labelling Crediflow/InExchange) cannot match without rebuilding their stack.
- Storecove send handles Belgium 2026, France 2026 PA flow, Italy SDI, Germany XRechnung, Poland KSeF in one API, you don't want to be re-implementing FatturaPA.
- Migrate send to own AP only when volume justifies it, likely 2028–2029 at the earliest for an early-stage product.
Critical UX features
- Per-buyer BT-10 BuyerReference regex map for the top 200 Swedish public buyers. Validate at compose time. Prevents 30-day public-sector payment delays.
- Free Peppol address out of the box for every customer, major customer-acquisition lever in a market where competitors charge for Peppol setup.
- Inbound triage UI, parse incoming UBL, show structured fields, propose BAS posting based on supplier history and item descriptions, await user approval.
- Multi-mandate send routing, automatically route based on recipient country (
cbc:Country/cbc:IdentificationCode) and recipient identifier scheme.
Phased plan for a new product (12 months)
Phase 0, today (Month 0)
- Join OpenPeppol as End User (€650 sign-up + €1,250/yr) for forum access and early-warning on spec changes.
- Register a Peppol participant ID for the company itself for dogfooding.
- Set up CI with Helger phive-rules.
Phase 1, Months 1–3
- UBL generator with full Peppol BIS Billing 3 + Sweden CIUS rules (SE-R-* including F-skatt).
- Storecove sandbox for outbound test.
- Oxalis-NG TEST environment for inbound test.
- Schematron in CI on every UBL output.
- BT-10 regex map for top 100 Swedish myndigheter.
Phase 2, Months 4–6
- Become Candidate Service Provider (AP-only) with OpenPeppol (~€4,400 one-off + €3,350/year ongoing).
- Pass Conformance Test Suite.
- Sign Service Provider Agreement with DIGG (free).
- Migrate to native Oxalis-NG receive.
- Marketing message: "your customers email PDFs, your suppliers send Peppol e-invoices, both arrive in your inbox."
Phase 3, Months 7–12
- Multi-mandate readiness for BE/DE/FR/PL/RO destinations through Storecove.
- Per-customer compliance dashboard.
- Begin SMP work (€2,200 + €5,000/yr SMP-only or upgrade to AP+SMP S1).
Phase 4, Year 2+
- Switch send to own phase4 AP when monthly volume crosses ~25,000.
- Pre-build Skatteverket DRR ingestion behind feature flag for July 2030 activation.
- Pivot on SOU 2027 (due 30 November 2027), adjust architecture if Sweden chooses non-Peppol model.
Total go-live cost for a credible, differentiated, open-source native-Peppol Swedish bookkeeping product: approximately €10–15k plus 3 months engineering.
Strategic positioning vs incumbents
The competitive landscape has a hole.
- Visma eEkonomi, Fortnox, Bokio, SpeedLedger, Björn Lundén all outsource their Peppol layer to Crediflow or InExchange/Maventa. None differentiate on Peppol-native architecture.
- The 3.00–3.50 SEK per outbound markup these vendors charge has a baked-in reseller cost, building native eliminates that markup AND removes dependency risk now visible after the Pagero→Thomson Reuters acquisition.
- Native Peppol receive as a free out-of-the-box feature, every customer gets a Peppol address by default, every supplier can send them invoices for free, is a brutal customer acquisition anchor in a market where the SMB end is underserved.
- The open-source angle is force multiplier: publishing the UBL generator on GitHub under BSD/MIT earns trust with bookkeeping/Linux-friendly customers without surrendering the moat, which lives in the bookkeeping engine and UX, not the XML serialiser.
Three forces converge to make this strategically valuable:
- B2G compliance has been mandatory since April 2019, any customer with public-sector revenue is a non-starter without native Peppol.
- Large-enterprise customers (Volvo, Ericsson, IKEA-supply, ICA, plus all Belgian/German/French subsidiaries hitting Swedish suppliers from 2026) increasingly require Peppol from suppliers.
- ViDA cross-border B2B mandate of 1 July 2030 is non-negotiable, plus the plausible domestic mandate window of 2029–2031.
The window to build with this advantage is the next 18–24 months before incumbents finish their own native-Peppol projects.
Authoritative source list
- Oxalis-NG: https://github.com/OxalisCommunity/oxalis-ng
- Helger phase4: https://github.com/phax/phase4
- peppol-commons: https://github.com/phax/peppol-commons
- Phive-rules: https://github.com/phax/phive-rules
- peppol-bis-invoice-3: https://github.com/OpenPEPPOL/peppol-bis-invoice-3
- Storecove docs: https://www.storecove.com/docs/
- DIGG Peppol testbädd: https://www.digg.se/digitala-tjanster/peppol/peppol-testbadd
- Peppol Testbed: https://peppol.org/tools-support/testbed/
- OpenPeppol membership: https://peppol.eu/who-is-who/openpeppol-membership/