<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Guang Hu</title>
    <description>The latest articles on DEV Community by Guang Hu (@guang_hu).</description>
    <link>https://dev.to/guang_hu</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3864114%2F71abda22-f550-4bb8-a6d4-d159f58599ad.png</url>
      <title>DEV Community: Guang Hu</title>
      <link>https://dev.to/guang_hu</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/guang_hu"/>
    <language>en</language>
    <item>
      <title>How I built a Norwegian VAT filing library in Java (and what nearly broke me)</title>
      <dc:creator>Guang Hu</dc:creator>
      <pubDate>Mon, 06 Apr 2026 15:00:07 +0000</pubDate>
      <link>https://dev.to/guang_hu/how-i-built-a-norwegian-vat-filing-library-in-java-and-what-nearly-broke-me-282j</link>
      <guid>https://dev.to/guang_hu/how-i-built-a-norwegian-vat-filing-library-in-java-and-what-nearly-broke-me-282j</guid>
      <description>&lt;p&gt;After 3 years building a SaaS accounting system for the Norwegian market, &lt;br&gt;
I open-sourced the VAT filing module as a standalone Java library. &lt;br&gt;
This is what I learned.&lt;/p&gt;
&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;If you've ever tried to integrate with Norwegian government APIs, you know &lt;br&gt;
the documentation is thorough — but the gap between reading the docs and &lt;br&gt;
actually getting it working in production is large.&lt;/p&gt;

&lt;p&gt;MVA-melding (Norwegian VAT return) looks simple on the surface: generate &lt;br&gt;
some XML, submit it. In reality it's a 15-step chain involving three &lt;br&gt;
separate systems, each with its own auth model and failure modes.&lt;/p&gt;
&lt;h2&gt;
  
  
  What the flow actually looks like
&lt;/h2&gt;

&lt;p&gt;Most documentation shows you the happy path. Here's what you're actually &lt;br&gt;
dealing with:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1: ID-porten authentication&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;ID-porten requires PKCE with &lt;code&gt;private_key_jwt&lt;/code&gt; — not a simple OAuth flow. &lt;br&gt;
You generate an RSA key pair, upload the public JWKS to Digdir's &lt;br&gt;
self-service portal, then sign your own JWT client assertion on every &lt;br&gt;
token request. The state parameter must survive a browser redirect, which &lt;br&gt;
means you need server-side session storage (we use Redis).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2: Altinn 3 instance lifecycle&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is where most implementations get stuck. Altinn 3 is not a simple &lt;br&gt;
REST API — it's a stateful workflow engine. The sequence is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;create instance → upload mva-melding XML → upload innsending XML → 
complete data → sign → complete → poll feedback
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each step returns an updated instance object. You must persist state &lt;br&gt;
between steps. If you skip a step or call them out of order, you get &lt;br&gt;
cryptic 409 errors with no explanation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3: MVA sign rules&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is the part nobody documents well. Norwegian accounting uses &lt;br&gt;
internal MVA-koder (1-91) that don't map 1:1 to what Skatteetaten &lt;br&gt;
expects in the XML.&lt;/p&gt;

&lt;p&gt;The tricky ones are reverse-charge codes 81, 83, 86, 88, and 91. &lt;br&gt;
These generate &lt;em&gt;two&lt;/em&gt; lines in the XML — one for output VAT (positive) &lt;br&gt;
and one for input VAT (negative). If you treat them as a single line, &lt;br&gt;
Skatteetaten's validator rejects the submission with a validation error &lt;br&gt;
that points at the wrong place.&lt;/p&gt;

&lt;p&gt;Also: output VAT in your ledger is a credit (positive in accounting &lt;br&gt;
terms), but must appear as positive in the XML too — but input VAT is &lt;br&gt;
a debit, and must appear as negative. The sign convention is not &lt;br&gt;
intuitive if you're coming from standard accounting logic.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I open-sourced
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;mva-melding-java&lt;/strong&gt; handles the complete flow:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Filing calendar generation with Norwegian public holiday adjustments&lt;/li&gt;
&lt;li&gt;Ledger aggregation with correct MVA sign rules&lt;/li&gt;
&lt;li&gt;Full ID-porten PKCE login flow
&lt;/li&gt;
&lt;li&gt;Altinn 3 submission, signing, and feedback polling&lt;/li&gt;
&lt;li&gt;Payment info extraction from &lt;code&gt;betalingsinformasjon.xml&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Ports-and-adapters design — implement one interface to connect 
your own ledger&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I also open-sourced a companion library for EHF 3.0 / PEPPOL BIS &lt;br&gt;
Billing 3.0 invoice transmission (&lt;strong&gt;oxalis-spring-boot-starter&lt;/strong&gt;), &lt;br&gt;
which handles the Guice↔Spring isolation problem that makes &lt;br&gt;
oxalis-ng painful to use in Spring Boot applications.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one thing I wish someone had told me
&lt;/h2&gt;

&lt;p&gt;The Altinn 3 feedback endpoint is asynchronous. After you complete &lt;br&gt;
the submission, Skatteetaten processes the return in the background — &lt;br&gt;
this can take anywhere from a few seconds to several minutes. &lt;/p&gt;

&lt;p&gt;You need a polling loop. Don't block a request thread waiting for it. &lt;br&gt;
We run a scheduler that checks every 5 minutes for instances in &lt;br&gt;
&lt;code&gt;UPLOAD_COMPLETE&lt;/code&gt; state and polls for feedback.&lt;/p&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;MVA-melding library: &lt;a href="https://github.com/guangcode/mva-melding-java" rel="noopener noreferrer"&gt;https://github.com/guangcode/mva-melding-java&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;EHF/PEPPOL starter: &lt;a href="https://github.com/guangcode/oxalis-spring-boot-starter" rel="noopener noreferrer"&gt;https://github.com/guangcode/oxalis-spring-boot-starter&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're working on Norwegian e-invoicing or tax integrations, &lt;br&gt;
feel free to open an issue or leave a comment here.&lt;/p&gt;

</description>
      <category>java</category>
      <category>opensource</category>
      <category>norway</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
