Skip to content

Add Slovenia (SI) tax regime#893

Open
daesparza02 wants to merge 4 commits into
invopop:mainfrom
daesparza02:add-si-regime
Open

Add Slovenia (SI) tax regime#893
daesparza02 wants to merge 4 commits into
invopop:mainfrom
daesparza02:add-si-regime

Conversation

@daesparza02

@daesparza02 daesparza02 commented Jul 3, 2026

Copy link
Copy Markdown

This PR adds Slovenia (SI) as a new tax regime.

I picked Slovenia because it wasn't supported yet and fits GOBL's model well: an EU member
state using EUR, with tax identifiers that carry real modulo-11 check digits, so the regime
can do genuine validation rather than only format checks. I used the Netherlands (nl)
regime as a structural reference, since it's close to Slovenia: EU member, EUR, and a VAT
identity with a checksum.

What's included

  • Base regime: EUR, Europe/Ljubljana, the standard invoice scenarios, and credit notes for corrections.
  • VAT rates: the standard rate with its history (20% since 2002, 22% from 1 July 2013), plus the current reduced rate (9.5%) and the special reduced rate of 5% for books, newspapers and periodicals.
  • Tax ID validation: the modulo-11 check-digit algorithm, applied to two identifiers — the VAT number / davčna številka (8 digits) and the domestic registration number / matična številka (10 digits).
  • Invoice rule: the supplier must carry a tax ID code. The registration number identifies a company but is not a substitute for the VAT number on an invoice, so the rule checks specifically for the tax ID (ZDDV-1, Article 82).

Scope and design notes

A few things I decided deliberately, with the reasoning in case it helps the review:

  • I modelled the full date history only for the standard rate, to exercise the Since mechanism. The reduced rates have been stable for years, so I kept only their current values rather than pad the base regime with historical data — happy to add the full history if you'd prefer.
  • The modulo-11 logic lives in one place (tax_identity.go) and takes the weights as a parameter, since the two identifiers share the same scheme with different weights. The registration-number check strips its three-digit branch suffix (which has no check digit of its own) and reuses it.
  • One Slovenian detail worth flagging: a remainder of 0 is never assigned (no valid number produces it), so the shared validator rejects it explicitly.
  • I kept the scope to tax and identity rules — Slovenia's own e-invoicing formats and networks are a separate kind of extension in GOBL, so I left those out.

Testing

  • Table-driven tests across the package: 5 test functions, 29 sub-cases, covering valid and invalid inputs.
  • The official sources publish the number formats but not the check-digit formulas, so I verified each checksum against a real, public identifier from the same company (Krka d.d.: VAT number SI82646716 and registration number 5043611000) and exercised every branch of both algorithms, including the remainder == 0 case that can never yield a valid number.
  • gofmt, go vet and golangci-lint are clean.

Sources

Pre-Review Checklist

  • Opened this PR as a draft
  • Read the CONTRIBUTING.md guide.
  • Performed a self-review of my code.
  • Added thorough tests with at least 90% code coverage.
  • Modified or created example GOBL documents to show my changes in use, if appropriate.
  • Added links to the source of the changes in tax regimes or addons, either structured or in the comments.
  • Run go generate . to ensure that the Schemas and Regime data are up to date.
  • Reviewed and fixed all linter warnings.
  • Been obsessive with pointer nil checks to avoid panics.
  • Updated the CHANGELOG.md with an overview of my changes.
  • Marked this PR as ready for review.

@daesparza02
daesparza02 marked this pull request as ready for review July 3, 2026 13:32
@samlown samlown added the regime Related to a specific regime. label Jul 6, 2026
@codecov

codecov Bot commented Jul 6, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 95.68345% with 6 lines in your changes missing coverage. Please review.
✅ Project coverage is 94.73%. Comparing base (458d352) to head (cdb8fc1).

Files with missing lines Patch % Lines
regimes/si/tax_identity.go 86.04% 3 Missing and 3 partials ⚠️
Additional details and impacted files
@@           Coverage Diff            @@
##             main     #893    +/-   ##
========================================
  Coverage   94.73%   94.73%            
========================================
  Files         316      320     +4     
  Lines       17198    17337   +139     
========================================
+ Hits        16292    16425   +133     
- Misses        560      563     +3     
- Partials      346      349     +3     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@samlown samlown left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! A few style changes for the rule handling required.

Comment thread regimes/si/bill_invoices.go Outdated
is.InContext(tax.RegimeIn(CountryCode)),
rules.Field("supplier",
rules.Assert("01", "invoice SI supplier must have a tax ID code",
is.Func("has tax ID code", hasSupplierTaxIDCode),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Its better to use direct rule checks here instead of a helper.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done — replaced the helper with direct nested field rules using is.Present on tax_id and code.

Comment thread regimes/si/org_identities.go Outdated
},
Sources: []*cbc.Source{
{
Title: i18n.NewString("Uredba o vodenju in vzdrževanju Poslovnega registra Slovenije"),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing English translation.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed, the source title now carries both English and Slovenian.

Comment thread regimes/si/org_identities.go Outdated
// IdentityTypeMaticna represents the Slovenian registration number
// (matična številka) assigned by AJPES to every entity entered in
// the Slovenian Business Register (e.g. "5043611000").
IdentityTypeMaticna cbc.Code = "MATICNA"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Best to avoid local names in variables, there is likely to be a good translation. I'd also question the MATICNA code itself, it looks like a name that's been capitalized as opposed to a name, I'd expect there to be some type of abbreviated version.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed the code from the spelled-out word to MŠ, the official Slovenian abbreviation for matična številka. f you'd rather avoid the non-ASCII character, I'm happy to use an ASCII form (MS).

Comment thread regimes/si/org_identities.go Outdated
org.IdentityTypeIn(IdentityTypeMaticna),
rules.Field("code",
rules.Assert("01", "identity code for type MATICNA must be valid",
is.Func("valid", isValidRegistrationNumber),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Checks here could be split up to help users differentiate between length and check digit.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, split into three separate rules: presence, format and the check digit. Each failure now reports its own code and message.

Comment thread regimes/si/tax_identity.go Outdated
Comment on lines +46 to +57
func validateTaxCode(code cbc.Code) error {
if code == "" {
return nil
}
if len(code) != taxCodeLen {
return errors.New("invalid length")
}
if code[0] == '0' {
return errors.New("invalid format")
}
return validateMod11(code, taxCodeMultipliers)
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These errors will never reach anywhere, so the checks should probably be split into separate rules.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, removed validateTaxCode and split it into two rules: a format check (01) using a regex that covers length, digits and the non-zero first digit, and the check digit (02).

Comment thread regimes/si/tax_identity.go Outdated
// assigned (no valid number produces it), a remainder of 1 means a check
// digit of 0, and any other remainder means a check digit of 11 minus the
// remainder.
func validateMod11(code cbc.Code, weights []int) error {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, for a private method, the errors returned are not useful.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, validMod11 now returns a bool rather than building error values that were discarded; the user-facing message lives in the check-digit rule.

@daesparza02

Copy link
Copy Markdown
Author

Thanks for the thorough review, Sam — really helpful. I've addressed all the points: split the identifier validation into separate format/check-digit rules with distinct codes, switched the invoice rule to direct field checks, and cleaned up the naming and translations.

@daesparza02
daesparza02 requested a review from samlown July 14, 2026 07:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

regime Related to a specific regime.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants