Module: Ksef::FA3::Parser
- Extended by:
- NodeReader
- Defined in:
- lib/ksef/fa3/parser.rb
Overview
Reads an FA(3) document back into the model (DESIGN.md §7.6).
The asymmetry with the serializer is the whole design
Serializer is total: every model it is given becomes a document. The parser cannot
be, because FA(3) is far larger than this model. A real invoice carries Podmiot3,
PodmiotUpowazniony, DaneKontaktowe, OkresFa, Zalacznik, Platnosc,
currency-conversion twins of every tax bucket — and this models VAT, the correction
KOR, and the advance-payment pair ZAL and ROZ.
So parsing takes what it understands and keeps the whole document on
Invoice#raw_document. Nothing is lost, but nothing is silently invented either:
Ksef::FA3::Provenance#unmapped_elements names exactly what re-serialisation would drop, which a
caller should look at before calling #to_xml on a document they did not write.
parse → to_xml is not a safe edit-in-place operation, and that is a property of
the model's coverage rather than a bug to fix here.
Three things it deliberately does not do
It does not recompute what the document states. P_11 is read straight into
Line#net_amount rather than being derived from P_8B × P_9A. The two disagree in
real documents — upstream's own invoice-template-fa-3-with-custom-Subject3.xml has a
row of 20 × 1000 whose net is 18000, because a line may carry a discount — and the
document's own figure is the authority. Deriving it would quietly rewrite an invoice.
It does not validate. A caller who wants that has Invoice#validate! — which runs
every tier that exists, 1a, 1b and 2 — and can run it on the result. Parsing a document
in order to inspect why KSeF rejected it is a normal thing to want, and a parser that
refuses invalid input cannot do it.
It does not verify the checksum of a NIP it reads. Subject#to_fa3 does that on the way out. Reading is not the moment to reject: an invoice already in KSeF is a fact whatever its NIP, and PROD is the only environment that checks the digits anyway (docs/REFERENCE.md §15.3).
Constant Summary collapse
- SUPPORTED_TYPES =
All seven of
TRodzajFaktury, as of 2026-08-26. A spec asserts this list equals the schema's enumeration, so a future revision that adds a type fails loudly here rather than being refused at runtime by #supported_type!. %w[VAT KOR ZAL ROZ UPR KOR_ZAL KOR_ROZ].freeze
Constants included from NodeReader
NodeReader::NAMESPACES, NodeReader::PREFIX
Class Method Summary collapse
-
.parse(xml) ⇒ Invoice
With Invoice#raw_document set.
Class Method Details
.parse(xml) ⇒ Invoice
Returns with Invoice#raw_document set.
55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 |
# File 'lib/ksef/fa3/parser.rb', line 55 def parse(xml) document = xml.is_a?(Nokogiri::XML::Document) ? xml : Nokogiri::XML(xml) root = verified_root(document) fa_node = require_element(root, "Fa", context: Serializer::ROOT) # **Both before anything else is read**, because `Invoice.new`'s keyword arguments # evaluate in source order: anything a later argument depends on has to exist before # the call rather than be computed inside it. # # Reading the type inside {#build} let the row reader run first — and the Ministry's # collective corrections carry no `FaWiersz` at all, so a `KOR` was refused with # "Invoice has no FaWiersz rows". True, and the wrong diagnosis: it blamed a # perfectly good document for lacking something its type does not need. A ZAL is the # same case, its rows being `minOccurs="0"` and explicitly "opcjonalny dla faktury # zaliczkowej". `totals` is now the field that decides whether rows are required at # all, so it has to be read here too. # # The type is **collapsed before comparing**: `RodzajFaktury` is a token, so # `<RodzajFaktury> VAT </RodzajFaktury>` is schema-valid and means `VAT`. Compared # raw it was refused, with a message reading "This is a VAT invoice". type = supported_type!(Formatting.text(text(fa_node, "RodzajFaktury")) || "VAT") totals = Invoice::STATED_TOTALS_TYPES.include?(type) ? CorrectionReader.totals_from(fa_node) : nil # **Before** building, not after: the strategy decides what the rows derive, and the # constructor drops a `stated_gross` that merely repeats it. Copying an invoice to # change its strategy afterwards therefore threw the document's `P_15` away. build(document, root, fa_node, type: type, totals: totals, rounding: rounding_for(fa_node, totals)) end |