Class: Ksef::UPO::Client
- Inherits:
-
Object
- Object
- Ksef::UPO::Client
- Defined in:
- lib/ksef/upo/client.rb
Overview
Retrieving UPO documents, by either of the two available routes (docs/REFERENCE.md §12, §14.2).
Two routes, and which to prefer
The pre-signed link in a session's upo.pages[] or an invoice's upoDownloadUrl
is unmetered, carries an integrity hash, and expires. The API route is metered
against a budget where GET /sessions already allows only 10 requests a minute (§6.1),
and publishes no hash.
So the link is preferred and the API route is the fallback, which is the resolution §14.2 arrived at — after an earlier revision of the ledger got it backwards and concluded the link should be ignored. #fetch implements that preference.
The connection split is the safety mechanism
Requests to a pre-signed link go over storage, a connection built by
HTTP::Connection.storage with no credential and no base URL. Sending the
access token to third-party storage would leak it, and the contract says not to; using
a separate connection means no code path here can.
Instance Method Summary collapse
-
#collective(session_reference, upo_reference) ⇒ Object
The collective UPO for a whole session, over the metered API route.
-
#download(page) ⇒ Document
Follows a pre-signed link, verifying the published hash.
-
#fetch(page, session_reference:) ⇒ Document
Prefers the unmetered link and falls back to the metered route when it has expired or is missing — the resolution of §14.2, in one call.
-
#for_invoice(session_reference, invoice_reference) ⇒ Object
One invoice's UPO, addressed by its submission reference.
-
#for_ksef_number(session_reference, ksef_number) ⇒ Object
One invoice's UPO, addressed by its KSeF number.
-
#initialize(connection, credential, storage:, clock: -> { Time.now }) ⇒ Client
constructor
A new instance of Client.
Constructor Details
#initialize(connection, credential, storage:, clock: -> { Time.now }) ⇒ Client
Returns a new instance of Client.
36 37 38 39 40 41 |
# File 'lib/ksef/upo/client.rb', line 36 def initialize(connection, credential, storage:, clock: -> { Time.now }) @connection = connection @credential = credential @storage = storage @clock = clock end |
Instance Method Details
#collective(session_reference, upo_reference) ⇒ Object
The collective UPO for a whole session, over the metered API route.
62 63 64 65 66 67 |
# File 'lib/ksef/upo/client.rb', line 62 def collective(session_reference, upo_reference) session = Sessions.reference_number!(session_reference) upo = Sessions.reference_number!(upo_reference) via_api("sessions/#{session}/upo/#{upo}") end |
#download(page) ⇒ Document
Follows a pre-signed link, verifying the published hash.
48 49 50 51 52 53 54 55 56 57 58 59 |
# File 'lib/ksef/upo/client.rb', line 48 def download(page) url = page.respond_to?(:download_url) ? page.download_url : page.to_s raise ValidationError, "No download URL to follow" if url.to_s.empty? # No Authorization header is set here, and `@storage` has none by construction. response = @storage.get(absolute(url)) Document.new( xml: response.body.to_s, published_hash: response.headers[HASH_HEADER], source: :storage ).verify! end |
#fetch(page, session_reference:) ⇒ Document
Prefers the unmetered link and falls back to the metered route when it has expired or is missing — the resolution of §14.2, in one call.
The expiry is judged against the injected clock, not Time.now. This read the wall
clock until 2026-09-03, which made it the one route decision in this library the
recorded tier's pinned clock could not reach. KSeF signs the link for about three days,
so spec/recorded/invoice_download_spec.rb replayed correctly for three and then
started taking the metered fallback — a request its cassette had never recorded, since
the recording itself took the link. The tier went red on 2026-08-29 with nothing
changed, and nothing noticed until 2026-09-03 because no push ran the suite in between.
Same failure as the fifteen-minute access token (Client#initialize), one layer down: a recorded response is not a fixture, it decays, and every decision made against it has to be made against the moment it was recorded.
106 107 108 109 110 |
# File 'lib/ksef/upo/client.rb', line 106 def fetch(page, session_reference:) return download(page) unless page.download_url.to_s.empty? || page.expired?(@clock.call) collective(session_reference, page.reference_number) end |
#for_invoice(session_reference, invoice_reference) ⇒ Object
One invoice's UPO, addressed by its submission reference.
70 71 72 73 74 75 |
# File 'lib/ksef/upo/client.rb', line 70 def for_invoice(session_reference, invoice_reference) session = Sessions.reference_number!(session_reference) invoice = Sessions.reference_number!(invoice_reference) via_api("sessions/#{session}/invoices/#{invoice}/upo") end |
#for_ksef_number(session_reference, ksef_number) ⇒ Object
One invoice's UPO, addressed by its KSeF number.
The number is parsed before use, so a mistyped one fails here on its checksum rather than as an opaque 404 (§13).
81 82 83 84 85 86 |
# File 'lib/ksef/upo/client.rb', line 81 def for_ksef_number(session_reference, ksef_number) session = Sessions.reference_number!(session_reference) number = KsefNumber.parse(ksef_number) via_api("sessions/#{session}/invoices/ksef/#{number}/upo") end |