Module: Ksef::FA3::RoundingInference

Defined in:
lib/ksef/fa3/rounding_inference.rb

Overview

Works out which rounding strategy produced a document's tax summaries.

The strategy is not a field in an FA(3) invoice. It is how the summaries were computed, and the two legal ways of computing them — round each line, or round each bucket once — differ by at most a grosz (DESIGN.md §7.3). So it cannot be read, only inferred: whichever strategy reproduces the P_14_* values the document carries is the one that made it.

Lives apart from Parser because it is the one piece of parsing that is not reading. Everything else there maps an element to a field; this reasons backwards from an arithmetic result to the procedure that produced it.

Constant Summary collapse

TAX_ELEMENTS =

The tax-summary elements, taken from VatRate::BUCKETS rather than matched by name.

A start_with?("P_14") scan would also pick up P_14_1W and friends — the PLN-equivalent twins that exist for foreign-currency invoices — and those are not what either strategy computes. Deriving the list from the bucket table also means a schema revision cannot leave it stale.

VatRate::BUCKETS.values.filter_map { |(_net, tax)| tax }.uniq.freeze

Class Method Summary collapse

Class Method Details

.stated_from(node, &reader) ⇒ Hash{String => BigDecimal}

Returns the P_14_* values a document states.

Returns:

  • (Hash{String => BigDecimal}) —

    the P_14_* values a document states



56
57
58
59
60
61
# File 'lib/ksef/fa3/rounding_inference.rb', line 56

def stated_from(node, &reader)
  TAX_ELEMENTS.each_with_object({}) do |element, acc|
    value = reader.call(node, element)
    acc[element] = Formatting.decimal(value) unless value.nil?
  end
end

.strategy_for(lines, stated) ⇒ Invoice, Symbol

:per_line wins a tie, and a tie is the common case — the two agree on most invoices. When neither matches, the document's summaries reconcile with its own lines under no strategy at all. That is a business-rule violation (tier 3, still unbuilt — docs/REFERENCE.md §15.6), not a parse failure: the document exists and may well be sitting in KSeF, so it is left as :per_line and not argued with. Decided from the lines alone, so the parser can settle the strategy before constructing the invoice rather than constructing one and copying it.

That ordering is load-bearing. Invoice.scaled_gross drops stated_gross when it equals what the rows derive — and "what the rows derive" depends on the strategy. A parser that built with a provisional :per_line, let the constructor drop the document's P_15, and then copied the invoice to :per_summary lost the figure for good: the copy re-ran the constructor with stated_gross already nil. Przykład 1 would have shown it, except every one of the twenty-six pinned samples infers :per_line, so the corpus is structurally blind to it (docs/REFERENCE.md §17.2).

Parameters:

  • invoice (Invoice) —

    parsed with any strategy; only its lines are consulted

  • stated (Hash{String => BigDecimal}) —

    the document's own P_14_* values

  • lines (Array<Line>)
  • stated (Hash{String => BigDecimal}) —

    the document's own P_14_* values

Returns:

  • (Invoice) —

    the invoice, with the strategy that reproduces stated

  • (Symbol) —

    :per_line unless :per_summary is what reproduces stated



49
50
51
52
53
# File 'lib/ksef/fa3/rounding_inference.rb', line 49

def strategy_for(lines, stated)
  return :per_line if stated.empty? || bucketed(lines, :per_line) == stated

  bucketed(lines, :per_summary) == stated ? :per_summary : :per_line
end