Module: Ksef::FA3::Provenance
- Included in:
- Invoice
- Defined in:
- lib/ksef/fa3/provenance.rb
Overview
Everything about the document an Invoice was read from, for invoices that were read rather than built.
Mixed into Invoice, and separate from it because it answers a different question. Invoice is an invoice; this is the paper trail — where these fields came from, whether anything was left behind, and how the pair should behave when compared or printed.
The including class must define an IDENTITY constant listing the members that make up
its identity, i.e. all of them except raw_document.
Instance Method Summary collapse
-
#==(other) ⇒ Object
(also: #eql?)
raw_documentis provenance, not identity. -
#deconstruct_keys(keys) ⇒ Object
Data#deconstruct_keysbacks pattern matching and leaked the same way. -
#fully_mapped? ⇒ Boolean
Whether
#to_xmlreproduces every element the source document had. - #hash ⇒ Object
-
#inspect ⇒ Object
(also: #to_s)
Data#inspectwould dump the entire XML document into any console line or error message that mentions an invoice — including RSpec diffs. -
#source_errors ⇒ Array<Issue>
What libxml2 said about the bytes this invoice was parsed from.
-
#to_h ⇒ Object
Data#to_his public API and was handing out the entire source document. -
#unmapped_elements ⇒ Array<String>
Element paths present in #raw_document but absent from this model's own serialisation — that is, exactly what
#to_xmlwould drop.
Instance Method Details
#==(other) ⇒ Object Also known as: eql?
raw_document is provenance, not identity. Two invoices with the same seller, buyer,
number, dates and lines are the same invoice whether they were built here or read back
from XML — so it takes no part in equality. Without that,
parse(invoice.to_xml) == invoice could never hold and DESIGN.md §7.6's round-trip
law would be unstatable in the form it is written.
is_a? rather than a class equality check, so a future subtype comparing equal to its
parent stays possible.
25 26 27 |
# File 'lib/ksef/fa3/provenance.rb', line 25 def ==(other) other.is_a?(self.class) && identity_fields.all? { |field| public_send(field) == other.public_send(field) } end |
#deconstruct_keys(keys) ⇒ Object
Data#deconstruct_keys backs pattern matching and leaked the same way.
53 |
# File 'lib/ksef/fa3/provenance.rb', line 53 def deconstruct_keys(keys) = super.except(:raw_document) |
#fully_mapped? ⇒ Boolean
Returns whether #to_xml reproduces every element the source document had.
111 |
# File 'lib/ksef/fa3/provenance.rb', line 111 def fully_mapped? = unmapped_elements.empty? |
#hash ⇒ Object
30 |
# File 'lib/ksef/fa3/provenance.rb', line 30 def hash = identity_values.hash |
#inspect ⇒ Object Also known as: to_s
Data#inspect would dump the entire XML document into any console line or error
message that mentions an invoice — including RSpec diffs. Redacted for the same
reason Sessions::InvoiceState#inspect redacts its download URL: an accidental
p invoice should stay readable.
36 37 38 39 40 |
# File 'lib/ksef/fa3/provenance.rb', line 36 def inspect fields = identity_fields.map { |field| "#{field}=#{public_send(field).inspect}" } fields << "raw_document=#{raw_document ? "#<Nokogiri::XML::Document (retained)>" : "nil"}" "#<data #{self.class.name} #{fields.join(", ")}>" end |
#source_errors ⇒ Array<Issue>
What libxml2 said about the bytes this invoice was parsed from.
Nothing read this until 2026-08-26, and that was a hole the size of the one §15.1
records as closed. libxml2 recovers from broken XML by default, so a document with a
mismatched closing tag parses into a perfectly good-looking invoice — and
Invoice#errors runs tier 2 over #to_xml, bytes this gem has just produced and
which are well-formed by construction. Tier 2 is therefore structurally incapable of
seeing the input. valid? answered true for XML that is not XML, exactly as it did
before the fix Validator got, one level up.
Recovery also silently substitutes: an invalid UTF-8 byte becomes U+FFFD, and the only record that it happened is here.
69 70 71 72 73 74 75 |
# File 'lib/ksef/fa3/provenance.rb', line 69 def source_errors return [] if raw_document.nil? raw_document.errors.map do |error| Issue.new(field: "document", message: "the source document is not well-formed: #{error}") end end |
#to_h ⇒ Object
Data#to_h is public API and was handing out the entire source document. #inspect
was written to redact raw_document so p invoice stays readable; to_h is a Data
freebie and was not, so JSON.dump(invoice.to_h) embedded the whole XML — a real
hazard for anyone logging, caching or serialising an invoice into a job queue.
Redacting it here is consistent rather than surprising: raw_document is provenance
and not identity, which is why #== already ignores it.
50 |
# File 'lib/ksef/fa3/provenance.rb', line 50 def to_h = super.except(:raw_document) |
#unmapped_elements ⇒ Array<String>
Element paths present in #raw_document but absent from this model's own
serialisation — that is, exactly what #to_xml would drop.
Computed by difference rather than from a hand-maintained list of mapped fields, so it cannot drift away from what Serializer actually writes. Repeated elements collapse to one path: the question it answers is which kinds of element are lost, not how many times.
It serialises the invoice to find out, so it is a diagnostic to reach for when deciding whether re-serialising a foreign document is safe — not a loop body.
It cannot see a changed value, only a missing element. Two documents whose
Adnotacje/P_16 differ have identical path sets, so a diagnostic built on paths says
nothing about them; the fix for that class of loss is for the model to carry the field
(as Invoice#annotations now does), not for this method to grow. Equally invisible:
attributes, occurrence counts, element order, and comments. What it does catch — a
whole element or subtree the model has no field for — is the common case and the one a
caller can act on.
97 98 99 100 101 102 103 104 105 106 107 108 |
# File 'lib/ksef/fa3/provenance.rb', line 97 def unmapped_elements return [] if raw_document.nil? (element_paths(raw_document) - element_paths(Nokogiri::XML(to_xml))).sort rescue Ksef::Error => e # It has to serialise to find out what serialising would lose, so an invoice that # cannot be written at all has no answer to give. Say that, rather than surfacing a # bare "invalid check digit" from a method the README tells people to call for safety. raise ValidationError, "This invoice cannot be re-serialised, so there is nothing to say about what " \ "re-serialising it would drop: #{e.}" end |