Module: Ksef::FA3::ModelValidator

Extended by:
AdvanceChecks, AttachmentChecks, CorrectionChecks, FieldChecks, SubjectChecks, SummaryChecks
Defined in:
lib/ksef/fa3/model_validator.rb

Overview

Tier 1a — checks on the invoice object, before it becomes a document (DESIGN.md §7.7, as amended 2026-08-24).

Fast, field-addressed, and needing no XML: required fields, enum membership, NIP checksums, string lengths, date sanity. The point is not to duplicate the schema — tier 2 does that better — but to say which value is wrong in terms the caller recognises, before they have to read a libxml2 message about a facet.

It also has a contract tier 2 cannot give

If this passes, #to_xml will not raise. Serialisation refuses several things outright — a NIP that fails its checksum, a seller with no name or address, a rate code with no summary bucket — and each arrives as an exception rather than a message. A line whose net can be neither read nor derived was on that list until 2026-08-26; it is now a legal row that serialises without a P_11, and the objection to it is NO_AMOUNT, which this tier raises on its own account rather than on the serializer's behalf. Checking them here turns every one into a collected, addressed error, which is why Invoice#errors runs this first and stops if anything comes back.

What it deliberately does not check

Arithmetic. Whether the summaries reconcile with the lines is tier 3 (docs/REFERENCE.md §15.6), and it is not built.

Anything global. Duplicate detection (§15.2) is a property of KSeF's ten-year history, not of this document.

Sign. TKwotowy permits negative amounts and corrections need them, so a negative net is not an error here.

Where the checks live

This module decides what an invoice has — a seller, a buyer, a header, annotations, lines — and the checks for each subject live with that subject: SubjectChecks for the four Podmiot roles, CorrectionChecks for the correction group, SummaryChecks for the stated tax summary, AdvanceChecks for Zamowienie and FakturaZaliczkowa, and FieldChecks for anything measured against an xsd:token facet. All five are mixed in here, so they share one self and can call each other.

Constant Summary collapse

ANNOTATIONS_TYPE =

Adnotacje is an anonymous complex type, so the generated metadata keys it by path rather than by name — the same key Serializer resolves when it rejects an unknown element there.

"Faktura/Fa/Adnotacje"
FUTURE_TOLERANCE_DAYS =

§15.4: P_1 must not be later than the date KSeF accepts the document — a date in KSeF's timezone, which is unknowable here. Comparing against a local Date.today would reject perfectly good invoices around midnight for any caller west of Warsaw, so this allows a day of slack and flags only a date that is unambiguously in the future. The same-day boundary is left to the service, which is the only party that can decide it.

1
NO_RATE =
"states an amount but no P_12 rate code, so there is no bucket to put it in. " \
"This invoice derives its summary from its rows, so the amount would simply be " \
"missing from the tax base (docs/REFERENCE.md §8.6)."
NO_AMOUNT =
"states no amount, and this invoice derives its summary from its rows, so it " \
"would contribute nothing to the tax base. Either give the row an amount, or " \
"state the invoice's summary directly (docs/REFERENCE.md §8.6)."

Constants included from FieldChecks

FieldChecks::BUYER_ID_TEXT, FieldChecks::COLLAPSE, FieldChecks::FORBIDDEN_IN_XML, FieldChecks::LONG_TEXT, FieldChecks::SHORT_TEXT

Constants included from SummaryChecks

SummaryChecks::DERIVATION_BLOCKERS

Class Method Summary collapse

Methods included from FieldChecks

utf8?

Class Method Details

.errors_for(invoice) ⇒ Array<Issue>

Returns empty when the model is sound.

Parameters:

Returns:

  • (Array<Issue>) —

    empty when the model is sound



83
84
85
86
87
88
89
90
91
92
93
94
95
96
# File 'lib/ksef/fa3/model_validator.rb', line 83

def errors_for(invoice)
  [
    *subject_errors(invoice.seller, "seller", role: :seller),
    *subject_errors(invoice.buyer, "buyer", role: :buyer),
    *header_errors(invoice),
    *annotation_errors(invoice.annotations),
    *summary_errors(invoice),
    *type_specific_errors(invoice),
    *attachment_errors(invoice),
    *invoice.lines.each_with_index.flat_map do |line, index|
      line_errors(line, index, derived: invoice.totals.nil?)
    end
  ]
end