Skip to content

Extend verifactu addon rule set#819

Draft
cavalle wants to merge 4 commits intomainfrom
richer-verifactu-rules
Draft

Extend verifactu addon rule set#819
cavalle wants to merge 4 commits intomainfrom
richer-verifactu-rules

Conversation

@cavalle
Copy link
Copy Markdown
Contributor

@cavalle cavalle commented Apr 24, 2026

  • Additional rules to ensure schema-valid verifactu XML will always be produced.

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.

And if you are part of the org:

  • Requested a review from Copilot and fixed or dismissed (with a reason) all the feedback raised.
  • Requested a review from @samlown.

@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 24, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 93.39%. Comparing base (0982364) to head (35ee04d).

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #819      +/-   ##
==========================================
+ Coverage   93.38%   93.39%   +0.01%     
==========================================
  Files         369      369              
  Lines       20048    20103      +55     
==========================================
+ Hits        18721    18776      +55     
  Misses        888      888              
  Partials      439      439              

☔ View full report in Codecov by Sentry.
📢 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.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR extends the Spanish VERIFACTU addon validation rules to better ensure invoices can always be rendered into schema-valid VERIFACTU XML, and updates unit tests + changelog to match.

Changes:

  • Added new invoice validation rules (party name length limits, series+code combined length, non-ES tax ID code length, and max number of non-retained tax rates).
  • Updated verifactu invoice tests to assert rule IDs/messages and added coverage for the new rules.
  • Documented the change in the changelog.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 5 comments.

File Description
data/rules/es-verifactu.json Adds the generated rule definitions for the new VERI*FACTU invoice constraints.
addons/es/verifactu/bill.go Implements the new rule checks in Go (series+code length, party name length, non-ES tax ID code length, max non-retained rates).
addons/es/verifactu/bill_test.go Updates existing assertions to include rule IDs and adds tests for the new constraints.
CHANGELOG.md Notes the addition of extra VERI*FACTU schema-validity rules.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +351 to +354
func seriesCodeFits(series, code cbc.Code) bool {
joined := series.Join(code)
return len(joined) <= 60
}
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

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

seriesCodeFits uses len(joined) which measures UTF-8 bytes, not characters. Since cbc.Code permits non-ASCII characters (e.g., Ñ), this can reject values that are within a 60-character XSD limit but exceed 60 bytes. Consider counting runes (e.g., utf8.RuneCountInString(joined.String())) to align with schema maxLength semantics.

Copilot uses AI. Check for mistakes.
Comment on lines +511 to +518
t.Run("invoice series and code fit within 60 chars", func(t *testing.T) {
inv := testInvoiceStandard(t)
inv.Series = cbc.Code(strings.Repeat("S", 30))
inv.Code = cbc.Code(strings.Repeat("C", 30))
assertValidationError(t, inv, "[GOBL-ES-VERIFACTU-BILL-INVOICE-20] invoice series and code combined must be 60 characters or less")
})

t.Run("preceding series and code fit within 60 chars", func(t *testing.T) {
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

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

These test names say the series+code "fit within 60 chars" but the setup expects a validation error. Given cbc.Code.Join inserts a -, the constructed ID is 61 chars (30+1+30) and should fail; please rename the tests to reflect that they exceed the limit (or adjust the lengths and assert success for the boundary case).

Suggested change
t.Run("invoice series and code fit within 60 chars", func(t *testing.T) {
inv := testInvoiceStandard(t)
inv.Series = cbc.Code(strings.Repeat("S", 30))
inv.Code = cbc.Code(strings.Repeat("C", 30))
assertValidationError(t, inv, "[GOBL-ES-VERIFACTU-BILL-INVOICE-20] invoice series and code combined must be 60 characters or less")
})
t.Run("preceding series and code fit within 60 chars", func(t *testing.T) {
t.Run("invoice series and code exceed 60 chars combined", func(t *testing.T) {
inv := testInvoiceStandard(t)
inv.Series = cbc.Code(strings.Repeat("S", 30))
inv.Code = cbc.Code(strings.Repeat("C", 30))
assertValidationError(t, inv, "[GOBL-ES-VERIFACTU-BILL-INVOICE-20] invoice series and code combined must be 60 characters or less")
})
t.Run("preceding series and code exceed 60 chars combined", func(t *testing.T) {

Copilot uses AI. Check for mistakes.
Comment on lines +518 to +528
t.Run("preceding series and code fit within 60 chars", func(t *testing.T) {
inv := testInvoiceStandard(t)
inv.Type = bill.InvoiceTypeCorrective
inv.Preceding = []*org.DocumentRef{
{
Series: cbc.Code(strings.Repeat("S", 30)),
Code: cbc.Code(strings.Repeat("C", 30)),
},
}
assertValidationError(t, inv, "[GOBL-ES-VERIFACTU-BILL-INVOICE-21] ($.preceding[0]) preceding series and code combined must be 60 characters or less")
})
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

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

This preceding series+code length test triggers other preceding validations too (missing issue_date and, for corrective invoices, tax). That makes the failure less focused and could mask regressions. Consider populating the minimal required preceding fields so the test isolates rule 21.

Copilot uses AI. Check for mistakes.
Comment on lines +592 to 599
func assertValidationError(t *testing.T, inv *bill.Invoice, expected ...string) {
t.Helper()
require.NoError(t, inv.Calculate())
err := rules.Validate(inv)
require.ErrorContains(t, err, expected)
for _, e := range expected {
require.ErrorContains(t, err, e)
}
}
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

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

assertValidationError is now variadic; if it's ever called with no expected strings, it will pass even when rules.Validate(inv) returns nil. Add an explicit require.Error(t, err) (or guard against empty expected) to ensure the helper always asserts that validation failed.

Copilot uses AI. Check for mistakes.
Comment on lines +158 to +169
rules.Field("supplier",
rules.Field("name",
rules.AssertIfPresent("18", "supplier name must be 120 characters or less", is.Length(0, 120)),
),
),
// Customer - universal
// Code 19: customer name max 120 chars
// Code 22: non-ES customer tax ID code max 18 chars
rules.Field("customer",
rules.Field("name",
rules.AssertIfPresent("19", "customer name must be 120 characters or less", is.Length(0, 120)),
),
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

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

The new 120-character limit for party names uses is.Length(0, 120), which counts bytes for strings. XML Schema maxLength constraints are defined in characters (runes), so names containing multi-byte characters (e.g., accented letters) could be incorrectly rejected. Consider switching these name validations to is.RuneLength(0, 120) (and update generated rules JSON accordingly).

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants