Skip to content

guangcode/mva-melding-java

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 

Repository files navigation

mva-melding-java

A production-tested Java library for filing Norwegian VAT returns (MVA-melding) via ID-porten and Altinn.

Built on real accounting platform code. Handles the full 15-step flow — period generation, tax line aggregation, XML building, ID-porten PKCE login, Altinn submission, signing, and feedback polling.

What this library does

  • Generates the correct filing calendar (monthly / bi-monthly / yearly) with Norwegian public-holiday-adjusted deadlines
  • Aggregates ledger entries into the mvaSpesifikasjonslinje lines the XML requires, applying all Norwegian sign rules (negation, zero-VAT, reverse-charge double-entry for codes 81/83/86/88/91)
  • Builds and validates the mvaMelding XML against Skatteetaten's schema
  • Handles the complete Altinn 3 submission flow (create instance → sign → complete → poll feedback)
  • Parses the betalingsinformasjon.xml feedback to extract payment amount, KID, IBAN, and deadline
  • Ports-and-adapters architecture — bring your own ledger data

Quick start

1. Add the dependency

<dependency>
  <groupId>no.skatt</groupId>
  <artifactId>mva-melding-java</artifactId>
  <version>1.0.0</version>
</dependency>

2. Configure

Copy application.yml to your project and fill in the values marked CHANGE_ME.

Key settings:

mva:
  idporten:
    client-id: your-digdir-client-id
    kid: your-key-id
    private-key: your-pkcs8-private-key-base64
    redirect-uri: https://your-app.example.com/api/mva/callback

3. Implement LedgerPort

This is the only interface you must implement. It connects the library to your accounting database:

@Component
public class MyLedgerAdapter implements LedgerPort {

    @Autowired private JournalRepository repo;

    @Override
    public List<LedgerEntry> findVatEntries(VatPeriod period, VatCodeRules rules) {
        return repo.findByDateRange(
            period.getDocStartDate(),
            period.getDocEndDate(),
            rules.getAllVatReturnCodes(),
            period.getOrganisationNumber()
        ).stream().map(this::toLedgerEntry).toList();
    }

    @Override
    public CompanyInfo getCompanyInfo(String orgNo) {
        Company c = repo.findByOrgNo(orgNo);
        return new CompanyInfo(c.getOrgNo(), c.getName(), c.getFirstName(), c.getLastName());
    }

    @Override
    public Map<String, String> getVatCodeNames(String orgNo) {
        return repo.findVatCodes(orgNo).stream()
            .collect(Collectors.toMap(VatCode::getBid, VatCode::getName));
    }
}

Sign convention: return amounts with their natural accounting sign — output VAT (credit accounts 2700–2708) as positive, input VAT (debit accounts 2710–2720) as negative. The library applies the Norwegian MVA sign rules on top.

4. Use MvaFilingService

@Autowired MvaFilingService filing;

// Initialise periods for 2024
List<VatPeriod> periods = filing.initialisePeriods(
    orgNo, 2024, ReportingFrequency.TWO_MONTHLY, VatReportType.GENERAL, holidays);
myRepo.saveAll(periods);

// Validate a period
VatPeriod updated = filing.validate(period, null, null);
myRepo.save(updated);

// Get login URL → redirect user to it
String loginUrl = filing.getLoginUrl(period, userId, "https://my-app.example.com/api/mva/callback");

// In your /callback controller:
filing.handleLoginCallback(request.getParam("code"), request.getParam("state"));

// Submit
updated = filing.submitAndSign(period, userId);
myRepo.save(updated);

// Poll for feedback (call every 5 minutes from a scheduler)
if (period.getFilingState() == FilingState.UPLOAD_COMPLETE) {
    updated = filing.pollFeedback(period, userId);
    myRepo.save(updated);
}

Filing flow

INIT → validate() → VALIDATED
     → submitAndSign() → SUBMITTED → SIGNED → DATA_COMPLETE → UPLOAD_COMPLETE
     → pollFeedback() → FEEDBACK_RECEIVED

Each step returns an updated VatPeriod. Persist it after every step — the library is stateless.

ID-porten setup

  1. Go to selvbetjening.digdir.no
  2. Create a new integration: type = "API-klient"
  3. Scopes: openid skatteetaten:mvameldinginnsending skatteetaten:mvameldingvalidering
  4. Token endpoint auth: private_key_jwt
  5. Generate an RSA key pair, upload the public key JWKS, note the key ID
  6. Put the private key (PKCS8, base64) in mva.idporten.private-key

Generate a key pair:

openssl genrsa -out rsa_key.pem 2048
openssl pkcs8 -topk8 -nocrypt -in rsa_key.pem -out rsa_key_pkcs8.pem
# Upload the public part to Digdir:
openssl rsa -in rsa_key.pem -pubout -out rsa_key_pub.pem

VAT code rules

The mva.codes.* configuration encodes how each Norwegian MVA-kode is treated when building the return. The defaults match the 2024 Norwegian standard chart of accounts — you should not need to change them unless Skatteetaten updates the code set.

Key rule groups:

  • baseNegateCodes / vatNegateCodes: amounts are sign-flipped for these codes (output VAT on sales is a liability, so the sign is inverted in the XML)
  • doubleVatCodes: reverse-charge codes (81, 83, 86, 88, 91) that generate both an output and an input line
  • zeroVatCodes: reported with sats="0" regardless of the ledger rate
  • nullBasicCodes: basis amount suppressed in the XML

Running locally

# Start Redis
docker run -p 6379:6379 redis:7-alpine

# Run tests
mvn test

# Start the app
cp src/main/resources/application.yml src/main/resources/application-local.yml
# Edit application-local.yml and fill in your values
mvn spring-boot:run -Dspring-boot.run.profiles=local

License

Apache 2.0

About

Norwegian VAT return (MVA-melding) filing for Java — ID-porten, Altinn, Skatteetaten

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages