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 =
Adnotacjeis 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_1must not be later than the date KSeF accepts the document — a date in KSeF's timezone, which is unknowable here. Comparing against a localDate.todaywould 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
-
.errors_for(invoice) ⇒ Array<Issue>
Empty when the model is sound.
Methods included from FieldChecks
Class Method Details
.errors_for(invoice) ⇒ Array<Issue>
Returns 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), *(invoice), *invoice.lines.each_with_index.flat_map do |line, index| line_errors(line, index, derived: invoice.totals.nil?) end ] end |