Class: Ksef::Sessions::Online

Inherits:
Object
  • Object
show all
Defined in:
lib/ksef/sessions/online.rb

Overview

The three HTTP calls of an interactive session: open, send, close (docs/REFERENCE.md §11).

Deliberately thin and stateless, exactly like Auth::Client: it maps requests and responses and holds no session of its own. That shape is not a guess — both official clients do the same, threading the session reference through as a parameter and offering no session object at all. Deciding when to open a session, and composing open-send-close into one call, belongs a layer up in Client.

The encryptor travels with the session, not with each send

Recorded as a decision at docs/REFERENCE.md §11.2a, alongside the rest of the session-layer choices upstream does not make for us.

Session carries the Crypto::Encryptor that opened it. That is the single most important design decision here. The symmetric key is agreed once, at open, and every invoice in the session is encrypted under it — so sending an invoice encrypted with a different key produces a payload KSeF cannot decrypt, and the only symptom is per-invoice status 435, "błąd odszyfrowania pliku" (§12.1), arriving asynchronously, long after the call returned 202.

Binding the encryptor to the session at open time makes that mistake unrepresentable rather than merely documented — the same reasoning that makes Crypto::Encryptor#seal produce both digests together.

Defined Under Namespace

Classes: Session, Submission

Instance Method Summary collapse

Constructor Details

#initialize(connection, credential) ⇒ Online

Returns a new instance of Online.

Parameters:

  • connection (Faraday::Connection) —
  • credential (Ksef::Auth::AccessToken, String) —

    anything responding to #bearer, or a bare token string. Asked for the bearer per request rather than once, so a long session picks up a proactive refresh (§4.2).



51
52
53
54
# File 'lib/ksef/sessions/online.rb', line 51

def initialize(connection, credential)
  @connection = connection
  @credential = credential
end

Instance Method Details

#close(session) ⇒ nil

Closes the session, which starts asynchronous generation of the collective UPO (§11). The UPO is not available when this returns; poll the session status for upo.pages[].

Returns:

  • (nil) —

    the API answers 204 with no body



131
132
133
134
# File 'lib/ksef/sessions/online.rb', line 131

def close(session)
  post(path(session, "close"))
  nil
end

#open(encryptor:, certificate:, form_code: DEFAULT_FORM_CODE, upo_version: UPO_VERSION) ⇒ Session

Opens a session. "Lightweight and synchronous" per §11 — but not free: a session opened and never used is cancelled with status 440, "nie przesłano faktur" (§12.1).

Parameters:

Returns:



67
68
69
70
71
72
73
74
75
76
77
78
79
80
# File 'lib/ksef/sessions/online.rb', line 67

def open(encryptor:, certificate:, form_code: DEFAULT_FORM_CODE, upo_version: UPO_VERSION)
  body = {
    formCode: Sessions.form_code(form_code),
    encryption: encryptor.encryption_info(certificate)
  }
  headers = upo_version.nil? ? {} : { FEATURE_HEADER => upo_version }
  payload = post("sessions/online", body: body, headers: headers).body

  Session.new(
    reference_number: payload["referenceNumber"],
    valid_until: Ksef::Auth.time(payload["validUntil"]),
    encryptor: encryptor
  )
end

#send_invoice(session, invoice, offline_mode: false, corrected_invoice_hash: nil) ⇒ Submission

Encrypts an invoice and submits it. Returns as soon as KSeF accepts the upload — a 202, not an acceptance of the invoice. Whether the invoice itself is accepted arrives later, per invoice, via the status endpoints (§12.1).

The four integrity values of §11.1 all come from one Crypto::Encryptor#seal call, so the plaintext hash cannot be computed over different bytes than the ciphertext hash.

The two offline parameters (docs/REFERENCE.md §16)

offline_mode declares the taxpayer's offline invoicing mode — one boolean covering all three legal regimes, offline24, offline and awaryjny. Declaring false does not guarantee the invoice is treated as online: KSeF compares P_1 against the moment it accepts the document and marks it offline if P_1's calendar day is earlier, whatever was declared (§16.1). An invoice dated yesterday and sent today is therefore offline by the service's reckoning, one second past midnight included.

corrected_invoice_hash is the base64 SHA-256 of the original, rejected offline invoice, and it makes this submission a technical correction (§16.2): a resend of an invoice KSeF refused for a technical reason — schema mismatch, size, duplicate — with different bytes and therefore a different hash. It is not a way to correct content; that is a KOR, which is an ordinary invoice of a different type. Upstream sends offlineMode: true alongside it, and only in an interactive session, though the invoice being corrected may have been rejected in a batch one. This method does not enforce that pairing: the contract makes both fields independently optional, and imposing a rule it states only in prose would be inventing one.

Parameters:

  • session (Session) —

    from #open

  • invoice (String, #to_xml) —

    the FA(3) document

  • offline_mode (Boolean) (defaults to: false) —

    declares the taxpayer's "offline" invoicing mode

  • corrected_invoice_hash (String, nil) (defaults to: nil) —

    base64 SHA-256 of the rejected offline invoice this one technically corrects

Returns:



115
116
117
118
119
120
121
122
123
124
# File 'lib/ksef/sessions/online.rb', line 115

def send_invoice(session, invoice, offline_mode: false, corrected_invoice_hash: nil)
  # Serialised once. What gets hashed has to be exactly what gets encrypted, so the
  # bytes are settled here and never re-derived.
  xml = invoice.respond_to?(:to_xml) ? invoice.to_xml : invoice.to_s
  body = integrity(session.encryptor.seal(xml))
  body[:offlineMode] = true if offline_mode
  body[:hashOfCorrectedInvoice] = corrected_invoice_hash if corrected_invoice_hash

  Submission.from(post(path(session, "invoices"), body: body).body)
end