ksef_client 0.1.0.rc1 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +1424 -0
  3. data/CONTRIBUTING.md +45 -6
  4. data/README.md +377 -25
  5. data/SECURITY.md +25 -2
  6. data/docs/REFERENCE.md +3099 -34
  7. data/docs/errors.md +33 -4
  8. data/docs/field_mapping.md +390 -0
  9. data/lib/ksef/auth/access_token.rb +150 -0
  10. data/lib/ksef/auth/authorization_policy.rb +88 -0
  11. data/lib/ksef/auth/challenge.rb +53 -0
  12. data/lib/ksef/auth/client.rb +143 -0
  13. data/lib/ksef/auth/initiation.rb +22 -0
  14. data/lib/ksef/auth/operation_status.rb +34 -0
  15. data/lib/ksef/auth/schema/schemat_auth_v2-0.xsd +109 -0
  16. data/lib/ksef/auth/schema/schemat_auth_v2-1.xsd +109 -0
  17. data/lib/ksef/auth/signature_template.rb +120 -0
  18. data/lib/ksef/auth/signer.rb +132 -0
  19. data/lib/ksef/auth/status.rb +69 -0
  20. data/lib/ksef/auth/token.rb +121 -0
  21. data/lib/ksef/auth/token_info.rb +27 -0
  22. data/lib/ksef/auth/token_request.rb +142 -0
  23. data/lib/ksef/auth/tokens.rb +24 -0
  24. data/lib/ksef/auth/validator.rb +84 -0
  25. data/lib/ksef/auth/xades.rb +40 -0
  26. data/lib/ksef/auth.rb +43 -0
  27. data/lib/ksef/client/receipt.rb +37 -0
  28. data/lib/ksef/client/session.rb +68 -0
  29. data/lib/ksef/client.rb +314 -0
  30. data/lib/ksef/crypto/certificate.rb +78 -0
  31. data/lib/ksef/crypto/digest.rb +25 -0
  32. data/lib/ksef/crypto/encryptor.rb +131 -0
  33. data/lib/ksef/crypto/public_keys.rb +146 -0
  34. data/lib/ksef/crypto.rb +66 -0
  35. data/lib/ksef/environments.rb +1 -1
  36. data/lib/ksef/errors.rb +35 -2
  37. data/lib/ksef/fa3/address.rb +64 -0
  38. data/lib/ksef/fa3/advance_checks.rb +55 -0
  39. data/lib/ksef/fa3/advance_invoice.rb +55 -0
  40. data/lib/ksef/fa3/advance_reader.rb +59 -0
  41. data/lib/ksef/fa3/attachment.rb +43 -0
  42. data/lib/ksef/fa3/attachment_checks.rb +121 -0
  43. data/lib/ksef/fa3/attachment_reader.rb +99 -0
  44. data/lib/ksef/fa3/attachment_table.rb +98 -0
  45. data/lib/ksef/fa3/builder/advances.rb +57 -0
  46. data/lib/ksef/fa3/builder/corrections.rb +46 -0
  47. data/lib/ksef/fa3/builder/subjects.rb +71 -0
  48. data/lib/ksef/fa3/builder.rb +149 -0
  49. data/lib/ksef/fa3/business_validator.rb +134 -0
  50. data/lib/ksef/fa3/canonical.rb +44 -0
  51. data/lib/ksef/fa3/corrected_invoice.rb +53 -0
  52. data/lib/ksef/fa3/correction.rb +151 -0
  53. data/lib/ksef/fa3/correction_checks.rb +106 -0
  54. data/lib/ksef/fa3/correction_reader.rb +88 -0
  55. data/lib/ksef/fa3/data_block.rb +75 -0
  56. data/lib/ksef/fa3/document_mapping.rb +142 -0
  57. data/lib/ksef/fa3/document_validator.rb +174 -0
  58. data/lib/ksef/fa3/element_tree.rb +49 -0
  59. data/lib/ksef/fa3/field_checks.rb +151 -0
  60. data/lib/ksef/fa3/formatting.rb +273 -0
  61. data/lib/ksef/fa3/generated/enums.rb +652 -0
  62. data/lib/ksef/fa3/generated/types.rb +3308 -0
  63. data/lib/ksef/fa3/invoice.rb +259 -0
  64. data/lib/ksef/fa3/issue.rb +32 -0
  65. data/lib/ksef/fa3/line.rb +162 -0
  66. data/lib/ksef/fa3/meta_entry.rb +45 -0
  67. data/lib/ksef/fa3/model_validator.rb +219 -0
  68. data/lib/ksef/fa3/nip.rb +59 -0
  69. data/lib/ksef/fa3/node_reader.rb +46 -0
  70. data/lib/ksef/fa3/order.rb +55 -0
  71. data/lib/ksef/fa3/order_line.rb +70 -0
  72. data/lib/ksef/fa3/parser.rb +208 -0
  73. data/lib/ksef/fa3/provenance.rb +137 -0
  74. data/lib/ksef/fa3/rounding_inference.rb +87 -0
  75. data/lib/ksef/fa3/row_reader.rb +66 -0
  76. data/lib/ksef/fa3/serializer.rb +140 -0
  77. data/lib/ksef/fa3/subject.rb +128 -0
  78. data/lib/ksef/fa3/subject_checks.rb +126 -0
  79. data/lib/ksef/fa3/subject_reader.rb +76 -0
  80. data/lib/ksef/fa3/summaries.rb +107 -0
  81. data/lib/ksef/fa3/summary_checks.rb +75 -0
  82. data/lib/ksef/fa3/table_column.rb +49 -0
  83. data/lib/ksef/fa3/totals.rb +119 -0
  84. data/lib/ksef/fa3/validator.rb +79 -0
  85. data/lib/ksef/fa3/vat_rate.rb +94 -0
  86. data/lib/ksef/fa3.rb +59 -0
  87. data/lib/ksef/http/connection.rb +47 -4
  88. data/lib/ksef/http/json_decoder.rb +53 -0
  89. data/lib/ksef/http/retry.rb +105 -0
  90. data/lib/ksef/invoices/client.rb +72 -0
  91. data/lib/ksef/ksef_number.rb +150 -0
  92. data/lib/ksef/sessions/invoice_codes.rb +70 -0
  93. data/lib/ksef/sessions/invoice_state.rb +68 -0
  94. data/lib/ksef/sessions/online.rb +169 -0
  95. data/lib/ksef/sessions/session_codes.rb +73 -0
  96. data/lib/ksef/sessions/session_state.rb +48 -0
  97. data/lib/ksef/sessions/status.rb +138 -0
  98. data/lib/ksef/sessions/upo_page.rb +36 -0
  99. data/lib/ksef/sessions.rb +91 -0
  100. data/lib/ksef/upo/client.rb +154 -0
  101. data/lib/ksef/upo/document.rb +74 -0
  102. data/lib/ksef/upo/schema/upo-v4-3.xsd +293 -0
  103. data/lib/ksef/upo/validation.rb +37 -0
  104. data/lib/ksef/upo/validator.rb +124 -0
  105. data/lib/ksef/upo.rb +55 -0
  106. data/lib/ksef/version.rb +1 -1
  107. data/lib/ksef.rb +18 -0
  108. metadata +105 -4
data/docs/errors.md CHANGED
@@ -6,10 +6,12 @@
6
6
  Ksef::Error #problem → Ksef::ProblemDetails or nil
7
7
  ├── Ksef::ConfigurationError raised locally, before any request
8
8
  ├── Ksef::AuthenticationError challenge / token / JWT problems, and HTTP 401
9
- ├── Ksef::ValidationError raised locally by the FA(3) validator
9
+ ├── Ksef::ValidationError any local input check: FA(3), identifiers, references
10
+ ├── Ksef::CryptoError no usable published key, or bad key material
11
+ ├── Ksef::IntegrityError downloaded bytes did not match the published hash
10
12
  ├── Ksef::ApiError #status #code #details #trace_id #raw
11
13
  │ ├── Ksef::InvoiceRejectedError schema or business rejection by KSeF
12
- │ ├── Ksef::SessionError session could not be opened, used or closed
14
+ │ ├── Ksef::SessionError defined, not yet raised — see below
13
15
  │ ├── Ksef::AuthorizationError 403 — #reason_code, #security
14
16
  │ ├── Ksef::ResourceGoneError 410
15
17
  │ ├── Ksef::RateLimitedError 429 — #retry_after
@@ -22,12 +24,37 @@ Everything descends from `StandardError`, so a bare `rescue Ksef::Error` catches
22
24
  it. `#problem` is `nil` for locally raised errors and populated for anything derived from
23
25
  a response.
24
26
 
27
+ Two branches have no HTTP status behind them.
28
+
29
+ `Ksef::CryptoError` means either that no published KSeF certificate is valid for the usage
30
+ needed, or that key material is the wrong size. The first is worth acting on: after an
31
+ emergency key rotation it is transient, and `Ksef::Crypto::PublicKeys#refresh!` is the
32
+ remedy (docs/REFERENCE.md §10.2, §10.3).
33
+
34
+ `Ksef::SessionError` is **defined but never raised**, as of 2026-08-23. A session that
35
+ cannot be opened, used or closed currently surfaces as a plain `Ksef::ApiError` carrying the
36
+ status. Do not `rescue Ksef::SessionError` expecting to catch that — it will catch nothing.
37
+
38
+ `Ksef::TimeoutError` has **two** meanings, and the second matters more. The first is an open
39
+ or read timeout. The second is a **polling deadline** passing in `wait_until_accepted` or
40
+ `wait_for_session`, where it means the operation has *not failed* — it has outlasted the
41
+ wait. Resending there is precisely the duplicate-invoice hazard the gem works to avoid;
42
+ poll again, or raise the `deadline:` option.
43
+
44
+ `Ksef::IntegrityError` means a downloaded UPO did not match the `x-ms-meta-hash` the
45
+ storage link published for it. **The right response is to fetch it again** — nothing is
46
+ wrong with the request, the credentials or the document, so retrying is not a workaround
47
+ here but the actual fix. It is deliberately not a `ValidationError` (the caller's data is
48
+ fine) and not an `ApiError` (the response was a success). It exists as its own class
49
+ because the artifact is legal proof that an invoice was received: archiving corrupt bytes
50
+ as that proof is the one failure worth refusing loudly (§14.2, §12.3).
51
+
25
52
  ## Status mapping
26
53
 
27
54
  | Status | Class | Notes |
28
55
  |---|---|---|
29
56
  | 400 | `Ksef::ApiError` | Carries `errors[]`; `#code` is the KSeF error code |
30
- | 401 | `Ksef::AuthenticationError` | Refresh and replay once, for idempotent requests only |
57
+ | 401 | `Ksef::AuthenticationError` | **Not** retried. The refresh-and-replay of DESIGN.md §6.3 is not implemented; re-authenticate yourself |
31
58
  | 403 | `Ksef::AuthorizationError` | Check `#reason_code` before retrying anything |
32
59
  | 410 | `Ksef::ResourceGoneError` | The resource existed but is gone |
33
60
  | 429 | `Ksef::RateLimitedError` | Honour `#retry_after` |
@@ -57,7 +84,7 @@ deprecated envelope, `#trace_id` falls back to `referenceNumber`, its closest an
57
84
 
58
85
  ```ruby
59
86
  begin
60
- client.invoice(ksef_number)
87
+ client.download_invoice(ksef_number)
61
88
  rescue Ksef::RateLimitedError => e
62
89
  sleep e.retry_after if e.retry_after
63
90
  retry
@@ -107,6 +134,8 @@ is the earliest signal that something upstream is about to change.
107
134
  | Code | Meaning |
108
135
  |---|---|
109
136
  | 21405 | Input validation failure (e.g. unsupported form code) |
137
+ | 21470 | Symmetric-key certificate unknown or withdrawn. **The one code this library remediates by itself**: `Crypto::PublicKeys#with_key_rotation` re-fetches the published keys and retries once with the new one (`docs/REFERENCE.md` §10.2) |
138
+ | 21111 | Invalid authorisation challenge (`docs/REFERENCE.md` §4.5) |
110
139
  | 21157 | Invalid package part size |
111
140
 
112
141
  This catalogue grows as codes are encountered against the live API; the full published
@@ -0,0 +1,390 @@
1
+ <!-- GENERATED by `rake fa3:field_mapping` from FA(3) 1-0E — DO NOT EDIT. -->
2
+
3
+ # FA(3) field mapping
4
+
5
+ Every field this gem's model carries, and the FA(3) element it reads and writes.
6
+
7
+ **Generated**, not written: attribute names come from the model classes, and element names,
8
+ types, cardinalities and descriptions from the pinned XSD. Edit `tasks/field_mapping.rb`
9
+ rather than this file — a declared element that does not exist in the schema fails the
10
+ build rather than appearing here, and a model field nobody has mapped fails it too.
11
+
12
+ **One language per column.** The *Ministry's description* column is the Ministry's own
13
+ `xsd:documentation`, in Polish, **complete, unabridged and quoted rather than translated** —
14
+ only runs of whitespace are collapsed. Nothing this project wrote appears in it: our own
15
+ remarks are in *Notes*, in English, and every heading and column label is English too.
16
+ Several descriptions are long, and several say different things for a correction than for
17
+ an ordinary invoice — that is exactly why they are quoted whole.
18
+
19
+ The Polish is not translated because it is the operative text: it carries statutory
20
+ citations, and a paraphrase of a tax rule is a different tax rule. **Field-name truth is
21
+ the XSD**, not this table.
22
+
23
+ "Required?" is **effective** cardinality: an element inside an optional group is optional
24
+ however it declares itself, and a branch of a choice is never required on its own.
25
+
26
+ Section references such as (§8.4) are to `docs/REFERENCE.md`, which ships with this gem.
27
+
28
+
29
+ ## Invoice
30
+
31
+ `Ksef::FA3::Invoice` — The document itself. `Faktura` in the schema; its scalar fields live under `Fa`, its parties directly under the root.
32
+
33
+ | Attribute | FA(3) element | Type | Required? | The Ministry's description (Polish, verbatim) | Notes |
34
+ |---|---|---|---|---|---|
35
+ | `attachment` | `Zalacznik` | *(inline)* | optional | Załącznik do faktury VAT | — |
36
+ | `number` | `P_2` | `TZnakowy` | **yes** | Kolejny numer faktury, nadany w ramach jednej lub więcej serii, który w sposób jednoznaczny identyfikuje fakturę | — |
37
+ | `issue_date` | `P_1` | `TDataT` | **yes** | Data wystawienia, z zastrzeżeniem art. 106na ust. 1 ustawy | — |
38
+ | `currency` | `KodWaluty` | `TKodWaluty` | **yes** | Kod waluty (ISO 4217) | — |
39
+ | `invoice_type` | `RodzajFaktury` | `TRodzajFaktury` | **yes** | Rodzaj faktury | — |
40
+ | `issued_at` | `DataWytworzeniaFa` | *(inline)* | **yes** | Data i czas wytworzenia faktury | — |
41
+ | `seller` | `Podmiot1` | *(inline)* | **yes** | Dane podatnika. Imię i nazwisko lub nazwa sprzedawcy towarów lub usług | — |
42
+ | `buyer` | `Podmiot2` | *(inline)* | **yes** | Dane nabywcy | — |
43
+ | `lines` | `FaWiersz` | *(inline)* | optional, up to 10000 | Szczegółowe pozycje faktury w walucie, w której wystawiono fakturę - węzeł opcjonalny dla faktury zaliczkowej, faktury korygującej fakturę zaliczkową oraz faktur korygujących dotyczących wszystkich dostaw towarów lub usług dokonanych lub świadczonych w danym okresie, o których mowa w art. 106j ust. 3 ustawy, dla których należy podać dane dotyczące opustu lub obniżki w podziale na stawki podatku i procedury w części Fa. W przypadku faktur korygujących, o których mowa w art. 106j ust. 3 ustawy, gdy opust lub obniżka ceny odnosi się do części dostaw towarów lub usług dokonanych lub świadczonych w danym okresie w części FaWiersz należy podać nazwy (rodzaje) towarów lub usług objętych korektą. W przypadku faktur, o których mowa w art. 106f ust. 3 ustawy, należy wykazać pełne wartości zamówienia lub umowy. W przypadku faktur korygujących pozycje faktury (w tym faktur korygujących faktury, o których mowa w art. 106f ust. 3 ustawy, jeśli korekta dotyczy wartości zamówienia) należy wykazać różnice wynikające z korekty poszczególnych pozycji lub dane pozycji korygowanych wg stanu przed korektą i po korekcie jako osobne wiersze. W przypadku faktur korygujących faktury, o których mowa w art. 106f ust. 3 ustawy, jeśli korekta nie dotyczy wartości zamówienia i jednocześnie zmienia wysokość podstawy opodatkowania lub podatku, należy wprowadzić zapis wg stanu przed korektą i zapis wg stanu po korekcie w celu potwierdzenia braku zmiany wartości danej pozycji faktury | A true container: everything a line writes is under it. |
44
+ | `annotations` | `Adnotacje` | *(inline)* | **yes** | Inne adnotacje na fakturze | — |
45
+ | `correction` | `DaneFaKorygowanej` | *(inline)* | optional, up to 50000 | Dane faktury korygowanej | **A group, not one element.** The attribute also writes `PrzyczynaKorekty`, `TypKorekty`, `OkresFaKorygowanej`, `NrFaKorygowany`, `Podmiot1K`, `Podmiot2K`, `P_15ZK` and `KursWalutyZK` as siblings; this is the one that is mandatory once any of them is present. See the Correction section. |
46
+ | `totals` | `P_15` | `TKwotowy` | **yes** | Kwota należności ogółem. W przypadku faktur zaliczkowych - kwota zapłaty dokumentowana fakturą. W przypadku faktur, o których mowa w art. 106f ust. 3 ustawy - kwota pozostała do zapłaty. W przypadku faktur korygujących - korekta kwoty wynikającej z faktury korygowanej. W przypadku, o którym mowa w art. 106j ust. 3 ustawy - korekta kwot wynikających z faktur korygowanych | **The buckets are the substance**, and they are listed under Summary buckets. Note `P_15` is written for every invoice, whether or not a summary is stated — see `#gross_total` under Computed readers. |
47
+ | `order` | `Zamowienie` | *(inline)* | optional | Zamówienie lub umowa, o których mowa w art. 106f ust. 1 pkt 4 ustawy (dla faktur zaliczkowych), w walucie, w której wystawiono fakturę zaliczkową. W przypadku faktury korygującej fakturę zaliczkową należy wykazać różnice wynikające z korekty poszczególnych pozycji zamówienia lub umowy lub dane pozycji korygowanych wg stanu przed korektą i po korekcie jako osobne wiersze, jeśli korekta dotyczy wartości zamówienia lub umowy. W przypadku faktur korygujących faktury zaliczkowe, jeśli korekta nie dotyczy wartości zamówienia lub umowy i jednocześnie zmienia wysokość podstawy opodatkowania lub podatku, należy wprowadzić zapis wg stanu przed korektą i zapis wg stanu po korekcie w celu potwierdzenia braku zmiany wartości danej pozycji | — |
48
+ | `advances` | `FakturaZaliczkowa` | *(inline)* | optional, up to 100 | Numery faktur zaliczkowych lub ich numery KSeF, jeśli zostały wystawione z użyciem KSeF | — |
49
+ | `rounding` | — | — | — | — | **Not in the document at all.** Which rounding strategy produced the summaries is inferred when parsing (`RoundingInference`); FA(3) never states it. |
50
+ | `raw_document` | — | — | — | — | **Not an element.** Provenance rather than content — the retained source document, excluded from this invoice's identity. |
51
+ | `stated_gross` | `P_15` | — | — | — | The same `P_15` that `#gross_total` writes, carried only when the document's figure differs from what the rows derive. Not a second element. |
52
+
53
+
54
+ ## Subject as the seller — `Podmiot1`
55
+
56
+ `Ksef::FA3::Subject` — **The seller and the buyer are different XSD types**, `TPodmiot1` and `TPodmiot2`, and they disagree about what is required — so they get a section each rather than one section with a footnote. A seller must state a name and an address; a buyer need not. `Podmiot1K` on a correction uses this type.
57
+
58
+ | Attribute | FA(3) element | Type | Required? | The Ministry's description (Polish, verbatim) | Notes |
59
+ |---|---|---|---|---|---|
60
+ | `nip` | `NIP` | `etd:TNrNIP` | **yes** | Identyfikator podatkowy NIP | — |
61
+ | `name` | `Nazwa` | `TZnakowy512` | **yes** | Imię i nazwisko lub nazwa | — |
62
+ | `address` | `Adres` | `TAdres` | **yes** | Adres podatnika | — |
63
+ | `local_government_unit` | — | — | — | — | **Buyer-only.** `TPodmiot1` declares no `JST`; see the buyer section. |
64
+ | `vat_group_member` | — | — | — | — | **Buyer-only.** `TPodmiot1` declares no `GV`; see the buyer section. |
65
+ | `buyer_id` | — | — | — | — | **Buyer-only.** `TPodmiot1` declares no `IDNabywcy`; see the buyer section. |
66
+
67
+
68
+ ## Subject as the buyer — `Podmiot2`
69
+
70
+ `Ksef::FA3::Subject` — `Podmiot2`, and `Podmiot2K` on a correction. **The buyer's name and address are both optional** — a fact this project got wrong three times before (§8.2a) — and the identity is a four-way choice of which this model carries only the NIP branch.
71
+
72
+ | Attribute | FA(3) element | Type | Required? | The Ministry's description (Polish, verbatim) | Notes |
73
+ |---|---|---|---|---|---|
74
+ | `nip` | `NIP` | `etd:TNrNIP` | one of a choice | Identyfikator podatkowy NIP | — |
75
+ | `name` | `Nazwa` | `TZnakowy512` | optional | Imię i nazwisko lub nazwa | — |
76
+ | `address` | `Adres` | `TAdres` | optional | Adres nabywcy. Pola opcjonalne dla przypadków określonych w art. 106e ust. 5 pkt 3 ustawy | — |
77
+ | `local_government_unit` | `JST` | *(inline)* | **yes** | Znacznik jednostki podrzędnej JST. Wartość "1" oznacza, że faktura dotyczy jednostki podrzędnej JST. W takim przypadku, aby udostępnić fakturę jednostce podrzędnej JST, należy wypełnić sekcję Podmiot3, w szczególności podać NIP lub ID-Wew i określić rolę jako 8. Wartość "2" oznacza, że faktura nie dotyczy jednostki podrzędnej JST | — |
78
+ | `vat_group_member` | `GV` | *(inline)* | **yes** | Znacznik członka grupy VAT. Wartość "1" oznacza, że faktura dotyczy członka grupy VAT. W takim przypadku, aby udostępnić fakturę członkowi grupy VAT, należy wypełnić sekcję Podmiot3, w szczególności podać NIP lub ID-Wew i określić rolę jako 10. Wartość "2" oznacza, że faktura nie dotyczy członka grupy VAT | — |
79
+ | `buyer_id` | `IDNabywcy` | *(inline)* | optional | Unikalny klucz powiązania danych nabywcy na fakturach korygujących, w przypadku gdy dane nabywcy na fakturze korygującej zmieniły się w stosunku do danych na fakturze korygowanej | — |
80
+
81
+
82
+ ## Address
83
+
84
+ `Ksef::FA3::Address` — FA(3) has no street/city/postcode fields: it takes two free-text lines. `Address` composes them at construction and keeps the composed form (§8.2b), so `street`/`city`/`postal_code` are constructor sugar rather than attributes.
85
+
86
+ | Attribute | FA(3) element | Type | Required? | The Ministry's description (Polish, verbatim) | Notes |
87
+ |---|---|---|---|---|---|
88
+ | `line1` | `AdresL1` | `TZnakowy512` | **yes** | Adres [Address] | — |
89
+ | `line2` | `AdresL2` | `TZnakowy512` | optional | Adres [Address] | — |
90
+ | `country` | `KodKraju` | `etd:TKodKraju` | **yes** | Kod Kraju [Country Code] | — |
91
+
92
+
93
+ ## Line — an invoice row
94
+
95
+ `Ksef::FA3::Line` — One `FaWiersz`. Every child but `NrWierszaFa` is optional, so most fields here may be absent from a legal document (§8.6).
96
+
97
+ | Attribute | FA(3) element | Type | Required? | The Ministry's description (Polish, verbatim) | Notes |
98
+ |---|---|---|---|---|---|
99
+ | `name` | `P_7` | `TZnakowy512` | optional | Nazwa (rodzaj) towaru lub usługi. Pole opcjonalne wyłącznie dla przypadku określonego w art 106j ust. 3 pkt 2 ustawy (faktura korygująca) | — |
100
+ | `unit` | `P_8A` | `TZnakowy` | optional | Miara dostarczonych towarów lub zakres wykonanych usług. Pole opcjonalne dla przypadku określonego w art. 106e ust. 5 pkt 3 ustawy | — |
101
+ | `quantity` | `P_8B` | `TIlosci` | optional | Ilość (liczba) dostarczonych towarów lub zakres wykonanych usług. Pole opcjonalne dla przypadku określonego w art. 106e ust. 5 pkt 3 ustawy | Also feeds `P_11` when the row states no net. |
102
+ | `net_unit_price` | `P_9A` | `TKwotowy2` | optional | Cena jednostkowa towaru lub usługi bez kwoty podatku (cena jednostkowa netto). Pole opcjonalne dla przypadków określonych w art. 106e ust. 2 i 3 oraz ust. 5 pkt 3 ustawy | `TKwotowy2` — **eight decimal places**, four times the amount it produces. Also feeds `P_11` when the row states no net. |
103
+ | `net_amount` | `P_11` | `TKwotowy` | optional | Wartość dostarczonych towarów lub wykonanych usług, objętych transakcją, bez kwoty podatku (wartość sprzedaży netto). Pole opcjonalne dla przypadków określonych w art. 106e ust. 2 i 3 oraz ust. 5 pkt 3 ustawy | **`P_11` has two sources.** Stated here when the row states one; otherwise derived from `quantity` × `net_unit_price`. `Line#net` is the reader that answers either way, and nil when the row states no amount at all. |
104
+ | `vat_rate` | `P_12` | `TStawkaPodatku` | optional | Stawka podatku. Pole opcjonalne dla przypadków określonych w art. 106e ust. 2, 3, ust. 4 pkt 3 i ust. 5 pkt 3 ustawy | — |
105
+ | `row_number` | `NrWierszaFa` | `TNaturalny` | **yes** | Kolejny numer wiersza faktury | — |
106
+ | `state_before` | `StanPrzed` | `etd:TWybor1` | optional | Znacznik stanu przed korektą w przypadku faktury korygującej lub faktury korygującej fakturę wystawioną w związku z art. 106f ust. 3 ustawy, w przypadku gdy korekta dotyczy danych wykazanych w pozycjach faktury i jest dokonywana w sposób polegający na wykazaniu danych przed korektą i po korekcie jako osobnych wierszy z odrębną numeracją oraz w przypadku potwierdzania braku zmiany wartości danej pozycji | — |
107
+
108
+
109
+ ## Totals — a stated summary
110
+
111
+ `Ksef::FA3::Totals` — What a document states rather than what its rows imply. Present on the six types in `Invoice::STATED_TOTALS_TYPES`; a `VAT` invoice derives its summary instead (§8.4, §8.5).
112
+
113
+ | Attribute | FA(3) element | Type | Required? | The Ministry's description (Polish, verbatim) | Notes |
114
+ |---|---|---|---|---|---|
115
+ | `gross` | `P_15` | `TKwotowy` | **yes** | Kwota należności ogółem. W przypadku faktur zaliczkowych - kwota zapłaty dokumentowana fakturą. W przypadku faktur, o których mowa w art. 106f ust. 3 ustawy - kwota pozostała do zapłaty. W przypadku faktur korygujących - korekta kwoty wynikającej z faktury korygowanej. W przypadku, o którym mowa w art. 106j ust. 3 ustawy - korekta kwot wynikających z faktur korygowanych | — |
116
+ | `buckets` | `P_13_*`, `P_14_*` | — | — | — | A Hash **keyed by element name**, so each key is its own element. See *Summary buckets* below. |
117
+
118
+
119
+ ## Correction
120
+
121
+ `Ksef::FA3::Correction` — The group that makes a `KOR`, `KOR_ZAL` or `KOR_ROZ` a correction. Optional as a whole; `DaneFaKorygowanej` is mandatory once any of it is present (§8.4).
122
+
123
+ | Attribute | FA(3) element | Type | Required? | The Ministry's description (Polish, verbatim) | Notes |
124
+ |---|---|---|---|---|---|
125
+ | `reason` | `PrzyczynaKorekty` | `TZnakowy` | optional | Przyczyna korekty dla faktur korygujących | — |
126
+ | `effect` | `TypKorekty` | `TTypKorekty` | optional | Typ skutku korekty w ewidencji dla podatku od towarów i usług | — |
127
+ | `corrected` | `DaneFaKorygowanej` | *(inline)* | optional, up to 50000 | Dane faktury korygowanej | — |
128
+ | `period` | `OkresFaKorygowanej` | `TZnakowy` | optional | Dla faktury korygującej, o której mowa w art. 106j ust. 3 ustawy - okres, do którego odnosi się udzielany opust lub udzielana obniżka, w przypadku gdy podatnik udziela opustu lub obniżki ceny w odniesieniu do dostaw towarów lub usług dokonanych lub świadczonych na rzecz jednego odbiorcy w danym okresie | — |
129
+ | `corrected_number` | `NrFaKorygowany` | `TZnakowy` | optional | Poprawny numer faktury korygowanej w przypadku, gdy przyczyną korekty jest błędny numer faktury korygowanej. W takim przypadku błędny numer faktury należy wskazać w polu NrFaKorygowanej | — |
130
+ | `previous_seller` | `Podmiot1K` | *(inline)* | optional | W przypadku korekty danych sprzedawcy należy podać pełne dane sprzedawcy występujące na fakturze korygowanej. Pole nie dotyczy przypadku korekty błędnego NIP występującego na fakturze pierwotnej - wówczas wymagana jest korekta faktury do wartości zerowych | — |
131
+ | `previous_buyers` | `Podmiot2K` | *(inline)* | optional, up to 101 | W przypadku korekty danych nabywcy występującego jako Podmiot2 lub dodatkowego nabywcy występującego jako Podmiot3 należy podać pełne dane tego podmiotu występujące na fakturze korygowanej. Korekcie nie podlegają błędne numery NIP identyfikujące nabywcę oraz dodatkowego nabywcę - wówczas wymagana jest korekta faktury do wartości zerowych. W przypadku korygowania pozostałych danych nabywcy lub dodatkowego nabywcy wskazany numer identyfikacyjny ma być tożsamy z numerem w części Podmiot2 względnie Podmiot3 faktury korygującej | — |
132
+ | `paid_before` | `P_15ZK` | `TKwotowy` | optional | W przypadku korekt faktur zaliczkowych - kwota zapłaty przed korektą. W przypadku korekt faktur, o których mowa w art. 106f ust. 3 ustawy - kwota pozostała do zapłaty przed korektą | — |
133
+ | `exchange_rate_before` | `KursWalutyZK` | `TIlosci` | optional | Kurs waluty stosowany do wyliczenia kwoty podatku w przypadkach, o których mowa w dziale VI ustawy przed korektą | — |
134
+
135
+
136
+ ## CorrectedInvoice — which invoice is corrected
137
+
138
+ `Ksef::FA3::CorrectedInvoice` — One `DaneFaKorygowanej`. Its choice group names the corrected invoice either by KSeF number or by number-and-date, and the branches are not symmetrical with `FakturaZaliczkowa`'s (§8.5).
139
+
140
+ | Attribute | FA(3) element | Type | Required? | The Ministry's description (Polish, verbatim) | Notes |
141
+ |---|---|---|---|---|---|
142
+ | `number` | `NrFaKorygowanej` | `TZnakowy` | **yes** | Numer faktury korygowanej | — |
143
+ | `issue_date` | `DataWystFaKorygowanej` | `TDataT` | **yes** | Data wystawienia faktury korygowanej | — |
144
+ | `ksef_number` | `NrKSeFFaKorygowanej` | `TNumerKSeF` | one of a choice | Numer identyfikujący fakturę korygowaną w KSeF | — |
145
+
146
+
147
+ ## Order — an advance invoice's order
148
+
149
+ `Ksef::FA3::Order` — `Zamowienie`, which replaces `FaWiersz` entirely on a `ZAL`. `WartoscZamowienia` is the whole order **including tax** (§8.5).
150
+
151
+ | Attribute | FA(3) element | Type | Required? | The Ministry's description (Polish, verbatim) | Notes |
152
+ |---|---|---|---|---|---|
153
+ | `total` | `WartoscZamowienia` | `TKwotowy` | **yes** | Wartość zamówienia lub umowy z uwzględnieniem kwoty podatku | — |
154
+ | `lines` | `ZamowienieWiersz` | *(inline)* | yes, 1–10000 | Szczegółowe pozycje zamówienia lub umowy w walucie, w której wystawiono fakturę zaliczkową | — |
155
+
156
+
157
+ ## Attachment — the invoice attachment
158
+
159
+ `Ksef::FA3::Attachment` — `Zalacznik`, a **sibling of `Fa`** rather than one of its children, so it takes part in no summary and no arithmetic. FA(3) carries no bytes and no MIME type: an attachment here is a structured document of headings, key/value metadata, paragraphs and tables. Operational constraints on sending one are out of 0.1 scope (DESIGN.md §7.4).
160
+
161
+ | Attribute | FA(3) element | Type | Required? | The Ministry's description (Polish, verbatim) | Notes |
162
+ |---|---|---|---|---|---|
163
+ | `blocks` | `BlokDanych` | *(inline)* | yes, 1–1000 | Szczegółowe dane załącznika | — |
164
+
165
+
166
+ ## DataBlock — one block of an attachment
167
+
168
+ `Ksef::FA3::DataBlock` — `BlokDanych`. `MetaDane` is the one child the schema requires, which is why a block describing itself only with a heading or a table is refused at construction.
169
+
170
+ | Attribute | FA(3) element | Type | Required? | The Ministry's description (Polish, verbatim) | Notes |
171
+ |---|---|---|---|---|---|
172
+ | `heading` | `ZNaglowek` | `TZnakowy512` | optional | Nagłówek bloku danych | — |
173
+ | `metadata` | `MetaDane` | *(inline)* | yes, 1–1000 | Dane opisowe | — |
174
+ | `paragraphs` | `Akapit` | `TZnakowy512` | optional, up to 10 | Opis | — |
175
+ | `tables` | `Tabela` | *(inline)* | optional, up to 1000 | Tabele | — |
176
+
177
+
178
+ ## MetaEntry — one key/value pair
179
+
180
+ `Ksef::FA3::MetaEntry` — `MetaDane` on a block and `TMetaDane` on a table are the same shape under different names, so one class serves both. Mapped against the block's names; the table's are `TKlucz`/`TWartosc`.
181
+
182
+ | Attribute | FA(3) element | Type | Required? | The Ministry's description (Polish, verbatim) | Notes |
183
+ |---|---|---|---|---|---|
184
+ | `key` | `ZKlucz` | `TZnakowy` | **yes** | Klucz | — |
185
+ | `value` | `ZWartosc` | `TZnakowy` | **yes** | Wartość | — |
186
+
187
+
188
+ ## AttachmentTable — a table inside a block
189
+
190
+ `Ksef::FA3::AttachmentTable` — `Tabela`. **Rows are ragged**: `Kol` and `WKom` each repeat 1..20 and the schema ties them together nowhere, so a row need not carry one cell per column — both Ministry samples alternate one-cell label rows with full-width ones.
191
+
192
+ | Attribute | FA(3) element | Type | Required? | The Ministry's description (Polish, verbatim) | Notes |
193
+ |---|---|---|---|---|---|
194
+ | `metadata` | `TMetaDane` | *(inline)* | optional, up to 1000 | Dane opisowe dotyczące tabeli | — |
195
+ | `caption` | `Opis` | `TZnakowy512` | optional | Opis | — |
196
+ | `columns` | `Kol` | *(inline)* | yes, 1–20 | — | — |
197
+ | `rows` | `Wiersz` | *(inline)* | yes, 1–1000 | Wiersze tabeli | An Array of rows, each an Array of `WKom` cells (1–20, and **ragged** — a row need not carry one per column). |
198
+ | `totals` | `SKom` | `TZnakowy2` | optional, up to 20 | Zawartość pola | — |
199
+
200
+
201
+ ## TableColumn — one column heading
202
+
203
+ `Ksef::FA3::TableColumn` — `Kol`. Its `Typ` attribute is FA(3)'s **only** inline attribute enumeration, and the six permitted values are read from the generated metadata rather than restated in Ruby (`docs/REFERENCE.md` §18.2).
204
+
205
+ | Attribute | FA(3) element | Type | Required? | The Ministry's description (Polish, verbatim) | Notes |
206
+ |---|---|---|---|---|---|
207
+ | `name` | `NKom` | *(inline)* | **yes** | Zawartość pola | — |
208
+ | `type` | `Kol/@Typ` | — | — | — | **An attribute, not an element** — the only one in FA(3) carrying an inline enumeration (`date`, `datetime`, `dec`, `int`, `time`, `txt`). Required by the schema, and the permitted values are read from the generated metadata rather than restated (`docs/REFERENCE.md` §18.2). |
209
+
210
+
211
+ ## OrderLine — an order position
212
+
213
+ `Ksef::FA3::OrderLine` — One `ZamowienieWiersz`. Unlike {Line} nothing here is derived: the document states both the net and the tax, so both are read (§8.5).
214
+
215
+ | Attribute | FA(3) element | Type | Required? | The Ministry's description (Polish, verbatim) | Notes |
216
+ |---|---|---|---|---|---|
217
+ | `name` | `P_7Z` | `TZnakowy512` | optional | Nazwa (rodzaj) towaru lub usługi | — |
218
+ | `unit` | `P_8AZ` | `TZnakowy` | optional | Miara zamówionego towaru lub zakres usługi | — |
219
+ | `quantity` | `P_8BZ` | `TIlosci` | optional | Ilość zamówionego towaru lub zakres usługi | — |
220
+ | `net_unit_price` | `P_9AZ` | `TKwotowy2` | optional | Cena jednostkowa netto | — |
221
+ | `net_amount` | `P_11NettoZ` | `TKwotowy` | optional | Wartość zamówionego towaru lub usługi bez kwoty podatku | — |
222
+ | `vat_amount` | `P_11VatZ` | `TKwotowy` | optional | Kwota podatku od zamówionego towaru lub usługi | — |
223
+ | `vat_rate` | `P_12Z` | `TStawkaPodatku` | optional | Stawka podatku | — |
224
+ | `row_number` | `NrWierszaZam` | `TNaturalny` | **yes** | Kolejny numer wiersza zamówienia lub umowy | — |
225
+ | `state_before` | `StanPrzedZ` | `etd:TWybor1` | optional | Znacznik stanu przed korektą w przypadku faktury korygującej fakturę dokumentującą otrzymanie zapłaty lub jej części przed dokonaniem czynności oraz fakturę wystawioną w związku z art. 106f ust. 4 ustawy (faktura korygująca fakturę zaliczkową), w przypadku gdy korekta dotyczy danych wykazanych w pozycjach zamówienia i jest dokonywana w sposób polegający na wykazaniu danych przed korektą i po korekcie jako osobnych wierszy z odrębną numeracją oraz w przypadku potwierdzania braku zmiany wartości danej pozycji | — |
226
+
227
+
228
+ ## AdvanceInvoice — an advance already settled
229
+
230
+ `Ksef::FA3::AdvanceInvoice` — One `FakturaZaliczkowa` on a `ROZ` or `KOR_ROZ`. Its choice is **inverted** from `DaneFaKorygowanej`'s: the marker `NrKSeFZN` pairs with the plain number, and the KSeF branch is the number alone (§8.5).
231
+
232
+ | Attribute | FA(3) element | Type | Required? | The Ministry's description (Polish, verbatim) | Notes |
233
+ |---|---|---|---|---|---|
234
+ | `number` | `NrFaZaliczkowej` | `TZnakowy` | one of a choice | Numer faktury zaliczkowej wystawionej poza KSeF. Pole obowiązkowe dla faktury wystawianej po wydaniu towaru lub wykonaniu usługi, o której mowa w art. 106f ust. 3 ustawy i ostatniej z faktur, o której mowa w art. 106f ust. 4 ustawy | — |
235
+ | `ksef_number` | `NrKSeFFaZaliczkowej` | `TNumerKSeF` | one of a choice | Numer identyfikujący fakturę zaliczkową w KSeF. Pole obowiązkowe w przypadku, gdy faktura zaliczkowa była wystawiona za pomocą KSeF | — |
236
+
237
+
238
+ ## Computed readers
239
+
240
+ These are methods, not stored fields, and for several elements they are what you actually read. **`P_15` is here, not in the Invoice table** — on a `VAT` invoice nothing stores it.
241
+
242
+ | Reader | FA(3) element | What it does |
243
+ |---|---|---|
244
+ | `Invoice#gross_total` | `P_15` | The total amount due. Read from a stated summary when there is one, otherwise net + VAT from the rows. |
245
+ | `Invoice#net_total` | `P_13_*` (sum) | Total net. Stated summary if present, else the sum of the rows. |
246
+ | `Invoice#vat_total` | `P_14_*` (sum) | Total VAT, by the invoice's `rounding` strategy. |
247
+ | `Invoice#summary_buckets` | `P_13_*`, `P_14_*` | **The way to reach one bucket**: `invoice.summary_buckets["P_13_4"]`. All seven types. |
248
+ | `Invoice#net_by_rate` | — | Net per `P_12` rate code, before bucketing. Several codes share a bucket. |
249
+ | `Invoice#vat_by_rate` | — | VAT per rate code. |
250
+ | `Invoice#unmapped_elements` | — | For a parsed invoice, the element paths `#to_xml` would drop. |
251
+ | `Invoice#errors` | — | Validator tiers 1a, 1b and 2. Empty means the document is well-formed and schema-valid. |
252
+ | `Invoice#warnings` | — | Tier 3, advisory: figures the document states that do not reconcile. |
253
+ | `Line#net` | `P_11` | The row's net — **read from `P_11` when stated**, else quantity × unit price. nil when the row states no amount, which is legal. |
254
+ | `Line#vat` | — | Tax on the row, from its rate code. nil when the row states no amount. |
255
+ | `Line#gross` | — | net + VAT, or nil. |
256
+ | `Totals#net` | `P_13_*` (sum) | Sum of the stated net buckets. |
257
+ | `Totals#vat` | `P_14_*` (sum) | Sum of the stated tax buckets. |
258
+
259
+
260
+ ## Annotations
261
+
262
+ `Invoice#annotations` is a Hash keyed by element name, and these eight are the declarations it carries. Each has tax consequences, and the parser **reads them rather than defaulting them** — emitting the defaults regardless would silently deny every declaration an invoice made.
263
+
264
+ | Element | Required? | The Ministry's description (Polish, verbatim) |
265
+ |---|---|---|
266
+ | `P_16` | **yes** | W przypadku dostawy towarów lub świadczenia usług, w odniesieniu do których obowiązek podatkowy powstaje zgodnie z art. 19a ust. 5 pkt 1 lub art. 21 ust. 1 ustawy - wyrazy "metoda kasowa"; należy podać wartość "1", w przeciwnym przypadku - wartość "2" |
267
+ | `P_17` | **yes** | W przypadku faktur, o których mowa w art. 106d ust. 1 ustawy - wyraz "samofakturowanie"; należy podać wartość "1", w przeciwnym przypadku - wartość "2" |
268
+ | `P_18` | **yes** | W przypadku dostawy towarów lub wykonania usługi, dla których obowiązanym do rozliczenia podatku od wartości dodanej lub podatku o podobnym charakterze jest nabywca towaru lub usługi - wyrazy "odwrotne obciążenie"; należy podać wartość "1", w przeciwnym przypadku - wartość "2" |
269
+ | `P_18A` | **yes** | W przypadku faktur, w których kwota należności ogółem przekracza kwotę 15 000 zł lub jej równowartość wyrażoną w walucie obcej, obejmujących dokonaną na rzecz podatnika dostawę towarów lub świadczenie usług, o których mowa w załączniku nr 15 do ustawy - wyrazy "mechanizm podzielonej płatności", przy czym do przeliczania na złote kwot wyrażonych w walucie obcej stosuje się zasady przeliczania kwot stosowane w celu określenia podstawy opodatkowania; należy podać wartość "1", w przeciwnym przypadku - wartość "2" |
270
+ | `Zwolnienie` | **yes** | — |
271
+ | `NoweSrodkiTransportu` | **yes** | — |
272
+ | `P_23` | **yes** | W przypadku faktur wystawianych w procedurze uproszczonej przez drugiego w kolejności podatnika, o którym mowa w art. 135 ust. 1 pkt 4 lit. b i c oraz ust. 2 ustawy, zawierającej adnotację, o której mowa w art. 136 ust. 1 pkt 1 ustawy i stwierdzenie, o którym mowa w art. 136 ust. 1 pkt 2 ustawy, należy podać wartość "1", w przeciwnym przypadku - wartość "2" |
273
+ | `PMarzy` | **yes** | — |
274
+
275
+
276
+ ## Summary buckets
277
+
278
+ `Totals#buckets` is keyed by element name, and `Invoice#summary_buckets` returns the same shape for every invoice type. Which bucket a row lands in is decided by its `P_12` rate code, and **the map is not invertible** — several codes share one bucket. Three buckets have no rate code at all, which is a limit of this model rather than of FA(3): a document may state them, and this model will read and re-write them only as part of a stated summary.
279
+
280
+ | Element | Rate codes | The Ministry's description (Polish, verbatim) |
281
+ |---|---|---|
282
+ | `P_13_1` | `23`, `22` | Suma wartości sprzedaży netto objętej stawką podstawową - aktualnie 23% albo 22%. W przypadku faktur zaliczkowych - wartość zaliczki netto. W przypadku faktur korygujących - kwota różnicy, o której mowa w art. 106j ust. 2 pkt 5 ustawy |
283
+ | `P_14_1` | `23`, `22` | Kwota podatku od sumy wartości sprzedaży netto objętej stawką podstawową - aktualnie 23% albo 22%. W przypadku faktur zaliczkowych - kwota podatku wyliczona według wzoru, o którym mowa w art. 106f ust. 1 pkt 3 ustawy. W przypadku faktur korygujących - kwota różnicy, o której mowa w art. 106j ust. 2 pkt 5 ustawy |
284
+ | `P_13_2` | `8`, `7` | Suma wartości sprzedaży netto objętej stawką obniżoną pierwszą - aktualnie 8 % albo 7%. W przypadku faktur zaliczkowych - wartość zaliczki netto. W przypadku faktur korygujących - kwota różnicy, o której mowa w art. 106j ust. 2 pkt 5 ustawy |
285
+ | `P_14_2` | `8`, `7` | Kwota podatku od sumy wartości sprzedaży netto objętej stawką obniżoną pierwszą - aktualnie 8% albo 7%. W przypadku faktur zaliczkowych - kwota podatku wyliczona według wzoru, o którym mowa w art. 106f ust. 1 pkt 3 ustawy. W przypadku faktur korygujących - kwota różnicy, o której mowa w art. 106j ust. 2 pkt 5 ustawy |
286
+ | `P_13_3` | `5` | Suma wartości sprzedaży netto objętej stawką obniżoną drugą - aktualnie 5%. W przypadku faktur zaliczkowych - wartość zaliczki netto. W przypadku faktur korygujących - kwota różnicy, o której mowa w art. 106j ust. 2 pkt 5 ustawy |
287
+ | `P_14_3` | `5` | Kwota podatku od sumy wartości sprzedaży netto objętej stawką obniżoną drugą - aktualnie 5%. W przypadku faktur zaliczkowych - kwota podatku wyliczona według wzoru, o którym mowa w art. 106f ust. 1 pkt 3 ustawy. W przypadku faktur korygujących - kwota różnicy, o której mowa w art. 106j ust. 2 pkt 5 ustawy |
288
+ | `P_13_4` | `4`, `3` | Suma wartości sprzedaży netto objętej ryczałtem dla taksówek osobowych. W przypadku faktur zaliczkowych - wartość zaliczki netto. W przypadku faktur korygujących - kwota różnicy, o której mowa w art. 106j ust. 2 pkt 5 ustawy |
289
+ | `P_14_4` | `4`, `3` | Kwota podatku od sumy wartości sprzedaży netto w przypadku ryczałtu dla taksówek osobowych. W przypadku faktur zaliczkowych - kwota podatku wyliczona według wzoru, o którym mowa w art. 106f ust. 1 pkt 3 ustawy. W przypadku faktur korygujących - kwota różnicy, o której mowa w art. 106j ust. 2 pkt 5 ustawy |
290
+ | `P_13_5` | *(none)* | Suma wartości sprzedaży netto w przypadku procedury szczególnej, o której mowa w dziale XII w rozdziale 6a ustawy. W przypadku faktur zaliczkowych - wartość zaliczki netto. W przypadku faktur korygujących - kwota różnicy, o której mowa w art. 106j ust. 2 pkt 5 ustawy |
291
+ | `P_14_5` | *(none)* | Kwota podatku od wartości dodanej w przypadku procedury szczególnej, o której mowa w dziale XII w rozdziale 6a ustawy. W przypadku faktur zaliczkowych - kwota podatku wyliczona według wzoru, o którym mowa w art. 106f ust. 1 pkt 3 ustawy. W przypadku faktur korygujących - kwota różnicy, o której mowa w art. 106j ust. 2 pkt 5 ustawy |
292
+ | `P_13_6_1` | `0 KR` | Suma wartości sprzedaży objętej stawką 0% z wyłączeniem wewnątrzwspólnotowej dostawy towarów i eksportu. W przypadku faktur zaliczkowych - wartość zaliczki. W przypadku faktur korygujących - kwota różnicy, o której mowa w art. 106j ust. 2 pkt 5 ustawy |
293
+ | `P_13_6_2` | `0 WDT` | Suma wartości sprzedaży objętej stawką 0% w przypadku wewnątrzwspólnotowej dostawy towarów. W przypadku faktur korygujących - kwota różnicy, o której mowa w art. 106j ust. 2 pkt 5 ustawy |
294
+ | `P_13_6_3` | `0 EX` | Suma wartości sprzedaży objętej stawką 0% w przypadku eksportu. W przypadku faktur zaliczkowych - wartość zaliczki. W przypadku faktur korygujących - kwota różnicy, o której mowa w art. 106j ust. 2 pkt 5 ustawy |
295
+ | `P_13_7` | `zw` | Suma wartości sprzedaży zwolnionej od podatku. W przypadku faktur zaliczkowych - wartość zaliczki. W przypadku faktur korygujących - kwota różnicy wartości sprzedaży |
296
+ | `P_13_8` | `np I` | Suma wartości sprzedaży w przypadku dostawy towarów oraz świadczenia usług poza terytorium kraju, z wyłączeniem kwot wykazanych w polach P_13_5 i P_13_9. W przypadku faktur zaliczkowych - wartość zaliczki. W przypadku faktur korygujących - kwota różnicy wartości sprzedaży |
297
+ | `P_13_9` | `np II` | Suma wartości świadczenia usług, o których mowa w art. 100 ust. 1 pkt 4 ustawy. W przypadku faktur zaliczkowych - wartość zaliczki. W przypadku faktur korygujących - kwota różnicy wartości sprzedaży |
298
+ | `P_13_10` | `oo` | Suma wartości sprzedaży w procedurze odwrotnego obciążenia, dla której podatnikiem jest nabywca zgodnie z art. 17 ust. 1 pkt 7 i 8 ustawy oraz innych przypadków odwrotnego obciążenia występujących w obrocie krajowym. W przypadku faktur zaliczkowych - wartość zaliczki. W przypadku faktur korygujących - kwota różnicy, o której mowa w art. 106j ust. 2 pkt 5 ustawy |
299
+ | `P_13_11` | *(none)* | Suma wartości sprzedaży w procedurze marży, o której mowa w art. 119 i art. 120 ustawy. W przypadku faktur zaliczkowych - wartość zaliczki. W przypadku faktur korygujących - kwota różnicy wartości sprzedaży |
300
+
301
+
302
+ ## What this model does not carry
303
+
304
+ Listed rather than omitted, because an absent row would otherwise read as "not supported" when it may only mean "not modelled". A document carrying any of these still parses; `Invoice#unmapped_elements` names exactly what `#to_xml` would drop for a given document, and `#raw_document` keeps the original.
305
+
306
+ | Under | Elements |
307
+ |---|---|
308
+ | `Faktura` | `Podmiot3`, `PodmiotUpowazniony`, `Stopka` |
309
+ | `Faktura/Fa` | `P_1M`, `WZ`, `P_6`, `OkresFa`, `P_14_1W`, `P_14_2W`, `P_14_3W`, `P_14_4W`, `KursWalutyZ`, `ZaliczkaCzesciowa`, `FP`, `TP`, `DodatkowyOpis`, `ZwrotAkcyzy`, `Rozliczenie`, `Platnosc`, `WarunkiTransakcji` |
310
+
311
+
312
+ ## Element index
313
+
314
+ The reverse direction: an element from a Polish invoice, and the attribute that carries it. An element listed twice is carried by two different models — `P_9A` on an invoice row and `P_9AZ` on an order position are different elements.
315
+
316
+ | FA(3) element | Attribute |
317
+ |---|---|
318
+ | `Adnotacje` | `Invoice#annotations` |
319
+ | `Adres` | `Buyer#address`, `Seller#address` |
320
+ | `AdresL1` | `Address#line1` |
321
+ | `AdresL2` | `Address#line2` |
322
+ | `Akapit` | `DataBlock#paragraphs` |
323
+ | `BlokDanych` | `Attachment#blocks` |
324
+ | `DaneFaKorygowanej` | `Correction#corrected`, `Invoice#correction` |
325
+ | `DataWystFaKorygowanej` | `CorrectedInvoice#issue_date` |
326
+ | `DataWytworzeniaFa` | `Invoice#issued_at` |
327
+ | `FaWiersz` | `Invoice#lines` |
328
+ | `FakturaZaliczkowa` | `Invoice#advances` |
329
+ | `GV` | `Buyer#vat_group_member` |
330
+ | `IDNabywcy` | `Buyer#buyer_id` |
331
+ | `JST` | `Buyer#local_government_unit` |
332
+ | `KodKraju` | `Address#country` |
333
+ | `KodWaluty` | `Invoice#currency` |
334
+ | `Kol` | `AttachmentTable#columns` |
335
+ | `KursWalutyZK` | `Correction#exchange_rate_before` |
336
+ | `MetaDane` | `DataBlock#metadata` |
337
+ | `NIP` | `Buyer#nip`, `Seller#nip` |
338
+ | `NKom` | `TableColumn#name` |
339
+ | `Nazwa` | `Buyer#name`, `Seller#name` |
340
+ | `NrFaKorygowanej` | `CorrectedInvoice#number` |
341
+ | `NrFaKorygowany` | `Correction#corrected_number` |
342
+ | `NrFaZaliczkowej` | `AdvanceInvoice#number` |
343
+ | `NrKSeFFaKorygowanej` | `CorrectedInvoice#ksef_number` |
344
+ | `NrKSeFFaZaliczkowej` | `AdvanceInvoice#ksef_number` |
345
+ | `NrWierszaFa` | `Line#row_number` |
346
+ | `NrWierszaZam` | `OrderLine#row_number` |
347
+ | `OkresFaKorygowanej` | `Correction#period` |
348
+ | `Opis` | `AttachmentTable#caption` |
349
+ | `P_1` | `Invoice#issue_date` |
350
+ | `P_11` | `Line#net_amount` |
351
+ | `P_11NettoZ` | `OrderLine#net_amount` |
352
+ | `P_11VatZ` | `OrderLine#vat_amount` |
353
+ | `P_12` | `Line#vat_rate` |
354
+ | `P_12Z` | `OrderLine#vat_rate` |
355
+ | `P_15` | `Invoice#totals`, `Totals#gross` |
356
+ | `P_15ZK` | `Correction#paid_before` |
357
+ | `P_2` | `Invoice#number` |
358
+ | `P_7` | `Line#name` |
359
+ | `P_7Z` | `OrderLine#name` |
360
+ | `P_8A` | `Line#unit` |
361
+ | `P_8AZ` | `OrderLine#unit` |
362
+ | `P_8B` | `Line#quantity` |
363
+ | `P_8BZ` | `OrderLine#quantity` |
364
+ | `P_9A` | `Line#net_unit_price` |
365
+ | `P_9AZ` | `OrderLine#net_unit_price` |
366
+ | `Podmiot1` | `Invoice#seller` |
367
+ | `Podmiot1K` | `Correction#previous_seller` |
368
+ | `Podmiot2` | `Invoice#buyer` |
369
+ | `Podmiot2K` | `Correction#previous_buyers` |
370
+ | `PrzyczynaKorekty` | `Correction#reason` |
371
+ | `RodzajFaktury` | `Invoice#invoice_type` |
372
+ | `SKom` | `AttachmentTable#totals` |
373
+ | `StanPrzed` | `Line#state_before` |
374
+ | `StanPrzedZ` | `OrderLine#state_before` |
375
+ | `TMetaDane` | `AttachmentTable#metadata` |
376
+ | `Tabela` | `DataBlock#tables` |
377
+ | `TypKorekty` | `Correction#effect` |
378
+ | `WartoscZamowienia` | `Order#total` |
379
+ | `Wiersz` | `AttachmentTable#rows` |
380
+ | `ZKlucz` | `MetaEntry#key` |
381
+ | `ZNaglowek` | `DataBlock#heading` |
382
+ | `ZWartosc` | `MetaEntry#value` |
383
+ | `Zalacznik` | `Invoice#attachment` |
384
+ | `Zamowienie` | `Invoice#order` |
385
+ | `ZamowienieWiersz` | `Order#lines` |
386
+
387
+ ---
388
+
389
+ *Schema: `lib/ksef/fa3/schema/schemat_FA(3)_v1-0E.xsd`. Regenerate with `rake fa3:field_mapping`; `rake fa3:verify` fails if
390
+ this file is stale.*
@@ -0,0 +1,150 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ksef
4
+ module Auth
5
+ # Holds the redeemed token pair and keeps the access token fresh
6
+ # (DESIGN.md §5.1, §6.3; docs/REFERENCE.md §4.2).
7
+ #
8
+ # ## Expiry comes from the response, never from the JWT
9
+ #
10
+ # The access token is a JWT, and it is tempting to read `exp` out of it. This gem does
11
+ # not, and that is a locked decision: DESIGN.md §4.3 excludes the `jwt` dependency and
12
+ # treats the token as an opaque bearer string, and the contract's `TokenInfo` carries
13
+ # `validUntil` precisely so no decoding is needed. (§6.3 used to say "expiry in `exp`",
14
+ # contradicting §4.3; corrected 2026-08-23.)
15
+ #
16
+ # ## Why refresh early rather than on expiry
17
+ #
18
+ # Refreshing at ~80% of the token's life means a request never carries a credential that
19
+ # expires mid-flight. Waiting for expiry guarantees the opposite: the first request after
20
+ # the deadline fails, and on a non-idempotent call — an invoice submission — a failure
21
+ # that *might* have been delivered is exactly the situation this gem works hardest to
22
+ # avoid (DESIGN.md §6.7).
23
+ #
24
+ # ## Thread safety
25
+ #
26
+ # A `Ksef::Client` is shareable across threads (DESIGN.md §5.2), so this is the piece
27
+ # that has to be safe: one mutex guards the pair, and the staleness check is re-run
28
+ # inside the lock so a burst of threads produces one refresh rather than a stampede.
29
+ #
30
+ # **Not in scope here:** the 401 refresh-and-replay of §6.3. That needs to see the
31
+ # response, so it belongs to the HTTP layer; this class only refreshes on time.
32
+ class AccessToken
33
+ REDACTED = "[REDACTED]"
34
+
35
+ # Fraction of the access token's observed lifetime after which it is considered stale.
36
+ # "~80%" per §6.3 — the docs describe the lifetime only as "kilkanaście minut", so a
37
+ # proportion travels better than a fixed number of seconds.
38
+ REFRESH_THRESHOLD = 0.8
39
+
40
+ # @param tokens [Tokens] the pair from `POST /auth/token/redeem`
41
+ # @param client [Client] used for `POST /auth/token/refresh`
42
+ # @param clock [#call] injected for tests; returns the current {Time}
43
+ # @param threshold [Float] override for {REFRESH_THRESHOLD}
44
+ def initialize(tokens, client:, clock: -> { Time.now }, threshold: REFRESH_THRESHOLD)
45
+ @client = client
46
+ @clock = clock
47
+ @threshold = threshold
48
+ @mutex = Mutex.new
49
+ @access = tokens.access_token
50
+ @refresh = tokens.refresh_token
51
+ # The issue time is not in the response, so the lifetime is measured from when we
52
+ # took delivery. That can only *under*-estimate the remaining life, which errs the
53
+ # safe way: we refresh slightly early rather than slightly late.
54
+ @acquired_at = @clock.call
55
+ end
56
+
57
+ # The bearer string for an `Authorization` header, refreshing first if the token has
58
+ # gone stale.
59
+ #
60
+ # **This may perform a network call** — deliberately named `#bearer` rather than
61
+ # `#token` so that is not a surprise at the call site.
62
+ #
63
+ # @return [String]
64
+ # `@access.nil?` is not a paranoid guard: `TokenInfo.from(nil)` returns nil, so a
65
+ # redeem response missing its `accessToken` produces a pair with no token at all.
66
+ # Without this arm that state reaches `nil.token` and reports `NoMethodError` from
67
+ # deep inside the client, instead of saying the credential is unusable.
68
+ def bearer
69
+ @mutex.synchronize do
70
+ renew! if @access.nil? || stale_unlocked?
71
+ @access.token
72
+ end
73
+ end
74
+
75
+ # Forces a refresh regardless of staleness.
76
+ #
77
+ # @return [self]
78
+ def refresh!
79
+ @mutex.synchronize { renew! }
80
+ self
81
+ end
82
+
83
+ # @return [Time, nil] when the current access token stops being valid
84
+ def valid_until = @access&.valid_until
85
+
86
+ def expired?(now = @clock.call) = @access.nil? || @access.expired?(now)
87
+
88
+ # True once the token is past {REFRESH_THRESHOLD} of its observed lifetime.
89
+ #
90
+ # False when the lifetime cannot be established, which is the conservative answer: a
91
+ # `validUntil` we could not parse is no reason to spend a refresh, and a genuinely
92
+ # expired token still surfaces as a 401 from the API.
93
+ def stale?(now = @clock.call)
94
+ deadline = refresh_deadline
95
+ !deadline.nil? && now >= deadline
96
+ end
97
+
98
+ # The refresh token is valid up to seven days and is reusable (§4.2). Once it lapses
99
+ # there is no way back but a full re-authentication.
100
+ def refresh_token_expired?(now = @clock.call) = @refresh.nil? || @refresh.expired?(now)
101
+
102
+ # Both redacted, `#to_s` included: these are live credentials, and interpolating one
103
+ # into a log line is how they escape (DESIGN.md §4.5).
104
+ def to_s = REDACTED
105
+
106
+ def inspect
107
+ "#<Ksef::Auth::AccessToken token=#{REDACTED} valid_until=#{valid_until.inspect} " \
108
+ "stale=#{stale?} refresh=#{REDACTED}>"
109
+ end
110
+
111
+ private
112
+
113
+ # Callers hold the mutex, and it is the *caller* that decides whether a refresh is
114
+ # wanted: {#bearer} asks only when stale, {#refresh!} always. Guarding staleness in
115
+ # here as well would silently make `refresh!` conditional, which is not what it says.
116
+ #
117
+ # The stampede protection lives in {#bearer} instead, and works because the check
118
+ # happens inside the lock: the first thread through refreshes, `@acquired_at` moves,
119
+ # and every thread behind it sees a fresh token.
120
+ def renew!
121
+ if refresh_token_expired?
122
+ raise AuthenticationError,
123
+ "The refresh token expired at #{@refresh&.valid_until.inspect}, so the access token " \
124
+ "cannot be renewed. Re-authenticate from the challenge (docs/REFERENCE.md §4.2)."
125
+ end
126
+
127
+ @access = @client.refresh(refresh_token: @refresh.token)
128
+ @acquired_at = @clock.call
129
+ end
130
+
131
+ def stale_unlocked?
132
+ deadline = refresh_deadline
133
+ !deadline.nil? && @clock.call >= deadline
134
+ end
135
+
136
+ # `nil` when the lifetime is unknowable — no token, or no parsed `validUntil`.
137
+ def refresh_deadline
138
+ return nil if @access.nil?
139
+
140
+ expiry = @access.valid_until
141
+ return nil if expiry.nil?
142
+
143
+ lifetime = expiry - @acquired_at
144
+ return @acquired_at if lifetime <= 0
145
+
146
+ @acquired_at + (lifetime * @threshold)
147
+ end
148
+ end
149
+ end
150
+ end