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
@@ -0,0 +1,154 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uri"
4
+
5
+ module Ksef
6
+ module UPO
7
+ # Retrieving UPO documents, by either of the two available routes
8
+ # (docs/REFERENCE.md §12, §14.2).
9
+ #
10
+ # ## Two routes, and which to prefer
11
+ #
12
+ # The **pre-signed link** in a session's `upo.pages[]` or an invoice's `upoDownloadUrl`
13
+ # is unmetered, carries an integrity hash, and expires. The **API route** is metered
14
+ # against a budget where `GET /sessions` already allows only 10 requests a minute (§6.1),
15
+ # and publishes no hash.
16
+ #
17
+ # So the link is preferred and the API route is the fallback, which is the resolution
18
+ # §14.2 arrived at — after an earlier revision of the ledger got it backwards and
19
+ # concluded the link should be ignored. {#fetch} implements that preference.
20
+ #
21
+ # ## The connection split is the safety mechanism
22
+ #
23
+ # Requests to a pre-signed link go over `storage`, a connection built by
24
+ # {Ksef::HTTP::Connection.storage} with **no credential and no base URL**. Sending the
25
+ # access token to third-party storage would leak it, and the contract says not to; using
26
+ # a separate connection means no code path here *can*.
27
+ class Client
28
+ # @param connection [Faraday::Connection] the authenticated API connection, for the
29
+ # metered routes
30
+ # @param credential [Ksef::Auth::AccessToken, String] anything with `#bearer`
31
+ # @param storage [Faraday::Connection] from {Ksef::HTTP::Connection.storage} — must
32
+ # not carry a credential
33
+ # @param clock [#call] returns the current {Time}. {#fetch} is the only route decision
34
+ # in this library that depends on the clock, so it is the only reason this exists —
35
+ # see there for what reading the wall clock instead cost.
36
+ def initialize(connection, credential, storage:, clock: -> { Time.now })
37
+ @connection = connection
38
+ @credential = credential
39
+ @storage = storage
40
+ @clock = clock
41
+ end
42
+
43
+ # Follows a pre-signed link, verifying the published hash.
44
+ #
45
+ # @param page [Ksef::Sessions::UpoPage, String] a page, or a bare URL
46
+ # @return [Document]
47
+ # @raise [Ksef::IntegrityError] when the bytes do not match `x-ms-meta-hash`
48
+ def download(page)
49
+ url = page.respond_to?(:download_url) ? page.download_url : page.to_s
50
+ raise ValidationError, "No download URL to follow" if url.to_s.empty?
51
+
52
+ # No Authorization header is set here, and `@storage` has none by construction.
53
+ response = @storage.get(absolute(url))
54
+ Document.new(
55
+ xml: response.body.to_s,
56
+ published_hash: response.headers[HASH_HEADER],
57
+ source: :storage
58
+ ).verify!
59
+ end
60
+
61
+ # The collective UPO for a whole session, over the metered API route.
62
+ def collective(session_reference, upo_reference)
63
+ session = Sessions.reference_number!(session_reference)
64
+ upo = Sessions.reference_number!(upo_reference)
65
+
66
+ via_api("sessions/#{session}/upo/#{upo}")
67
+ end
68
+
69
+ # One invoice's UPO, addressed by its submission reference.
70
+ def for_invoice(session_reference, invoice_reference)
71
+ session = Sessions.reference_number!(session_reference)
72
+ invoice = Sessions.reference_number!(invoice_reference)
73
+
74
+ via_api("sessions/#{session}/invoices/#{invoice}/upo")
75
+ end
76
+
77
+ # One invoice's UPO, addressed by its KSeF number.
78
+ #
79
+ # The number is parsed before use, so a mistyped one fails here on its checksum rather
80
+ # than as an opaque 404 (§13).
81
+ def for_ksef_number(session_reference, ksef_number)
82
+ session = Sessions.reference_number!(session_reference)
83
+ number = KsefNumber.parse(ksef_number)
84
+
85
+ via_api("sessions/#{session}/invoices/ksef/#{number}/upo")
86
+ end
87
+
88
+ # Prefers the unmetered link and falls back to the metered route when it has expired
89
+ # or is missing — the resolution of §14.2, in one call.
90
+ #
91
+ # **The expiry is judged against the injected clock, not `Time.now`.** This read the wall
92
+ # clock until 2026-09-03, which made it the one route decision in this library the
93
+ # recorded tier's pinned clock could not reach. KSeF signs the link for about three days,
94
+ # so `spec/recorded/invoice_download_spec.rb` replayed correctly for three and then
95
+ # started taking the metered fallback — a request its cassette had never recorded, since
96
+ # the recording itself took the link. The tier went red on 2026-08-29 with nothing
97
+ # changed, and nothing noticed until 2026-09-03 because no push ran the suite in between.
98
+ #
99
+ # Same failure as the fifteen-minute access token ({Ksef::Client#initialize}), one layer
100
+ # down: a recorded response is not a fixture, it decays, and every decision made against
101
+ # it has to be made against the moment it was recorded.
102
+ #
103
+ # @param page [Ksef::Sessions::UpoPage]
104
+ # @param session_reference [String] needed for the fallback
105
+ # @return [Document]
106
+ def fetch(page, session_reference:)
107
+ return download(page) unless page.download_url.to_s.empty? || page.expired?(@clock.call)
108
+
109
+ collective(session_reference, page.reference_number)
110
+ end
111
+
112
+ private
113
+
114
+ # **The metered routes publish `x-ms-meta-hash` too**, and an earlier version of this
115
+ # method threw it away with a comment claiming they did not. The pinned contract
116
+ # declares the header on all three UPO routes *and* on the invoice download — four
117
+ # `200` responses in total — which §5.5 of the ledger had recorded correctly all along.
118
+ # Discarding an available integrity check on legal proof of receipt was the wrong
119
+ # trade at any price. Corrected 2026-08-23.
120
+ #
121
+ # It is still read defensively rather than required: if the header is absent the
122
+ # document comes back unverifiable rather than failing, which is what {Document}'s
123
+ # `#verifiable?`/`#verified?` split is for.
124
+ def via_api(endpoint)
125
+ response = @connection.get(endpoint) do |request|
126
+ request.headers["Authorization"] = "Bearer #{bearer}"
127
+ end
128
+
129
+ Document.new(
130
+ xml: response.body.to_s,
131
+ published_hash: response.headers[HASH_HEADER],
132
+ source: :api
133
+ ).verify!
134
+ end
135
+
136
+ # §9 carries this as unverified: whether the live API returns `downloadUrl` absolute or
137
+ # host-relative is unsettled, and `srodowiska.md` says only that a returned URL's
138
+ # *host* matches the environment called. So an absolute URL is used untouched, and a
139
+ # relative one is resolved against the API host rather than guessed at.
140
+ def absolute(url)
141
+ uri = URI.parse(url)
142
+ return url if uri.absolute?
143
+
144
+ URI.join(@connection.url_prefix, url).to_s
145
+ rescue URI::InvalidURIError => e
146
+ raise ValidationError, "Malformed UPO download URL: #{e.message}"
147
+ end
148
+
149
+ def bearer
150
+ @credential.respond_to?(:bearer) ? @credential.bearer : @credential.to_s
151
+ end
152
+ end
153
+ end
154
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ksef
4
+ module UPO
5
+ # A retrieved UPO, held as the exact bytes received (docs/REFERENCE.md §12).
6
+ #
7
+ # `xml` is deliberately a raw `String` and not a parsed document. The Ministry's XAdES
8
+ # signature covers the octets, so anything that re-serialises them — even a lossless
9
+ # round-trip through an XML library — risks producing a document that no longer
10
+ # verifies. Parse a *copy* if you need to read it; archive this.
11
+ #
12
+ # `source` records where the bytes came from, because the two routes have different
13
+ # properties worth knowing after the fact: `:storage` is the unmetered pre-signed link
14
+ # (§14.2), `:api` the metered fallback.
15
+ Document = Data.define(:xml, :published_hash, :source) do
16
+ # SHA-256 of the bytes held, Base64 — the same form `x-ms-meta-hash` uses.
17
+ def sha256 = Ksef::Crypto::Digest.of(xml).base64
18
+
19
+ def size = xml.bytesize
20
+
21
+ # Whether there is anything to check against at all. The pre-signed storage link
22
+ # publishes `x-ms-meta-hash`; the metered API route publishes nothing.
23
+ #
24
+ # Split from {#verified?} rather than folded into it as a third state: "no hash was
25
+ # published" and "the hash did not match" call for completely different responses, and
26
+ # a predicate that answers both with one value invites treating an unverifiable
27
+ # document as a corrupt one.
28
+ def verifiable? = !published_hash.nil?
29
+
30
+ # True only when a hash was published **and** the bytes match it. False for an
31
+ # unverifiable document too — ask {#verifiable?} to tell the two apart.
32
+ def verified? = verifiable? && sha256 == published_hash
33
+
34
+ # @raise [Ksef::IntegrityError] when a published hash does not match the bytes
35
+ # @return [self]
36
+ def verify!
37
+ return self unless verifiable? && !verified?
38
+
39
+ raise IntegrityError,
40
+ "The UPO fetched from #{source} does not match the hash the server published: " \
41
+ "expected #{published_hash}, got #{sha256} over #{size} bytes. This is a corrupted " \
42
+ "transfer, not a bad request — fetch it again. Do not archive these bytes as proof " \
43
+ "of receipt (docs/REFERENCE.md §14.2)."
44
+ end
45
+
46
+ # Checks the document against the bundled schema — **a diagnostic, never a gate**
47
+ # (§14.3). Archive the bytes regardless of what this says; see {Validator} for why
48
+ # there is no raising counterpart.
49
+ #
50
+ # @return [Validation]
51
+ def validate = Validator.validate(xml)
52
+
53
+ # Writes the bytes untouched. Binary mode on purpose: this must not pick up a newline
54
+ # translation or a re-encode on the way to disk, or the archived file stops matching
55
+ # what the Ministry signed.
56
+ #
57
+ # @param path [String]
58
+ # @return [Integer] bytes written
59
+ def write(path)
60
+ File.binwrite(path, xml)
61
+ end
62
+
63
+ # Deliberately not the document: a UPO is a few kilobytes of XML, and putting it in a
64
+ # log line or an exception message helps nobody. DESIGN.md §4.5 forbids full invoice
65
+ # payloads at default log level, and the same reasoning applies here.
66
+ def to_s = "#<Ksef::UPO::Document #{size} bytes from #{source}>"
67
+
68
+ def inspect
69
+ state = verifiable? ? verified?.to_s : "unverifiable"
70
+ "#<data Ksef::UPO::Document size=#{size} source=#{source.inspect} verified=#{state}>"
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,293 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"
3
+ xmlns:upo="http://upo.schematy.mf.gov.pl/KSeF/v4-3"
4
+ targetNamespace="http://upo.schematy.mf.gov.pl/KSeF/v4-3"
5
+ elementFormDefault="qualified" attributeFormDefault="unqualified"
6
+ version="1.0" xml:lang="PL">
7
+ <xsd:element name="Potwierdzenie">
8
+ <xsd:annotation>
9
+ <xsd:documentation>Element główny potwierdzenia</xsd:documentation>
10
+ </xsd:annotation>
11
+ <xsd:complexType>
12
+ <xsd:sequence>
13
+ <xsd:element name="NazwaPodmiotuPrzyjmujacego" fixed="Ministerstwo Finansów">
14
+ <xsd:annotation>
15
+ <xsd:documentation>Nazwa podmiotu przyjmującego dokument elektroniczny</xsd:documentation>
16
+ </xsd:annotation>
17
+ <xsd:simpleType>
18
+ <xsd:restriction base="xsd:token">
19
+ <xsd:minLength value="1"/>
20
+ <xsd:maxLength value="240"/>
21
+ </xsd:restriction>
22
+ </xsd:simpleType>
23
+ </xsd:element>
24
+ <xsd:element name="NumerReferencyjnySesji" type="upo:TNumerReferencyjny">
25
+ <xsd:annotation>
26
+ <xsd:documentation>Wartość numeru referencyjnego sesji</xsd:documentation>
27
+ </xsd:annotation>
28
+ </xsd:element>
29
+
30
+ <xsd:element name="Uwierzytelnienie">
31
+ <xsd:complexType>
32
+ <xsd:sequence>
33
+ <xsd:element name="IdKontekstu" >
34
+ <xsd:annotation>
35
+ <xsd:documentation>Identyfikator kontekstu uwierzytelnienia w systemie KSeF.</xsd:documentation>
36
+ </xsd:annotation>
37
+ <xsd:complexType>
38
+ <xsd:choice>
39
+ <xsd:element name="Nip" type="upo:TNip">
40
+ <xsd:annotation>
41
+ <xsd:documentation>Numer identyfikacyjny NIP</xsd:documentation>
42
+ </xsd:annotation>
43
+ </xsd:element>
44
+
45
+ <xsd:element name="IdWewnetrzny" type="upo:TIdWewnetrzny">
46
+ <xsd:annotation>
47
+ <xsd:documentation>Identyfikator wewnętrzny</xsd:documentation>
48
+ </xsd:annotation>
49
+ </xsd:element>
50
+
51
+ <xsd:element name="IdZlozonyVatUE" type="upo:TIdZlozonyVatUE">
52
+ <xsd:annotation>
53
+ <xsd:documentation>Kontekst złożony: NIP + numer VAT UE</xsd:documentation>
54
+ </xsd:annotation>
55
+ </xsd:element>
56
+
57
+ <xsd:element name="IdDostawcyUslugPeppol" type="upo:TIdDostawcyUslugPeppol">
58
+ <xsd:annotation>
59
+ <xsd:documentation>Identyfikator dostawcy usług Peppol</xsd:documentation>
60
+ </xsd:annotation>
61
+ </xsd:element>
62
+ </xsd:choice>
63
+ </xsd:complexType>
64
+ </xsd:element>
65
+ <xsd:choice>
66
+
67
+ <xsd:element name="NumerReferencyjnyTokenaKSeF" type="upo:TNumerReferencyjny">
68
+ <xsd:annotation>
69
+ <xsd:documentation>Identyfikator tokenu uwierzytelniającego w systemie KSeF</xsd:documentation>
70
+ </xsd:annotation>
71
+ </xsd:element>
72
+
73
+ <xsd:element name="SkrotDokumentuUwierzytelniajacego" type="upo:TSkrotDokumentu">
74
+ <xsd:annotation>
75
+ <xsd:documentation>Wartość funkcji skrótu dokumentu uwierzytelniającego w postaci otrzymanej przez system (łącznie z podpisem elektronicznym)</xsd:documentation>
76
+ </xsd:annotation>
77
+ </xsd:element>
78
+ </xsd:choice>
79
+ </xsd:sequence>
80
+ </xsd:complexType>
81
+ </xsd:element>
82
+
83
+ <xsd:element name="OpisPotwierdzenia" minOccurs="0" maxOccurs="1">
84
+ <xsd:complexType>
85
+ <xsd:sequence>
86
+ <xsd:element name="Strona">
87
+ <xsd:annotation>
88
+ <xsd:documentation>Numer strony dokumentu UPO.</xsd:documentation>
89
+ </xsd:annotation>
90
+ <xsd:simpleType>
91
+ <xsd:restriction base="xsd:positiveInteger">
92
+ <xsd:whiteSpace value="collapse" />
93
+ </xsd:restriction>
94
+ </xsd:simpleType>
95
+ </xsd:element>
96
+ <xsd:element name="LiczbaStron">
97
+ <xsd:annotation>
98
+ <xsd:documentation>Całkowita liczba stron dokumentu UPO.</xsd:documentation>
99
+ </xsd:annotation>
100
+ <xsd:simpleType>
101
+ <xsd:restriction base="xsd:positiveInteger">
102
+ <xsd:whiteSpace value="collapse" />
103
+ </xsd:restriction>
104
+ </xsd:simpleType>
105
+ </xsd:element>
106
+ <xsd:element name="ZakresDokumentowOd">
107
+ <xsd:annotation>
108
+ <xsd:documentation>Początek zakresu dokumentów na stronie UPO (inclusive).</xsd:documentation>
109
+ </xsd:annotation>
110
+ <xsd:simpleType>
111
+ <xsd:restriction base="xsd:positiveInteger">
112
+ <xsd:whiteSpace value="collapse" />
113
+ </xsd:restriction>
114
+ </xsd:simpleType>
115
+ </xsd:element>
116
+ <xsd:element name="ZakresDokumentowDo">
117
+ <xsd:annotation>
118
+ <xsd:documentation>Koniec zakresu dokumentów na stronie UPO (exclusive).</xsd:documentation>
119
+ </xsd:annotation>
120
+ <xsd:simpleType>
121
+ <xsd:restriction base="xsd:positiveInteger">
122
+ <xsd:whiteSpace value="collapse" />
123
+ </xsd:restriction>
124
+ </xsd:simpleType>
125
+ </xsd:element>
126
+ <xsd:element name="CalkowitaLiczbaDokumentow">
127
+ <xsd:annotation>
128
+ <xsd:documentation>Całkowita liczba dokumentów na wszystkich stronach dokumentu UPO.</xsd:documentation>
129
+ </xsd:annotation>
130
+ <xsd:simpleType>
131
+ <xsd:restriction base="xsd:positiveInteger">
132
+ <xsd:whiteSpace value="collapse" />
133
+ </xsd:restriction>
134
+ </xsd:simpleType>
135
+ </xsd:element>
136
+ </xsd:sequence>
137
+ </xsd:complexType>
138
+ </xsd:element>
139
+
140
+ <xsd:element name="NazwaStrukturyLogicznej">
141
+ <xsd:annotation>
142
+ <xsd:documentation>Nazwa pliku XSD struktury logicznej dotycząca przesłanego dokumentu</xsd:documentation>
143
+ </xsd:annotation>
144
+ <xsd:simpleType>
145
+ <xsd:restriction base="xsd:token">
146
+ <xsd:minLength value="1"/>
147
+ <xsd:maxLength value="240"/>
148
+ </xsd:restriction>
149
+ </xsd:simpleType>
150
+ </xsd:element>
151
+ <xsd:element name="KodFormularza">
152
+ <xsd:annotation>
153
+ <xsd:documentation>Kod formularza przedłożonego dokumentu elektronicznego</xsd:documentation>
154
+ </xsd:annotation>
155
+ <xsd:simpleType>
156
+ <xsd:restriction base="xsd:token">
157
+ <xsd:minLength value="1"/>
158
+ <xsd:maxLength value="240"/>
159
+ </xsd:restriction>
160
+ </xsd:simpleType>
161
+ </xsd:element>
162
+ <xsd:element name="Dokument" maxOccurs="10000">
163
+ <xsd:complexType>
164
+ <xsd:sequence>
165
+ <xsd:element name="NipSprzedawcy" type="upo:TNip" >
166
+ <xsd:annotation>
167
+ <xsd:documentation>Nip sprzedawcy z faktury (Podmiot 1)</xsd:documentation>
168
+ </xsd:annotation>
169
+ </xsd:element>
170
+ <xsd:element name="NumerKSeFDokumentu">
171
+ <xsd:annotation>
172
+ <xsd:documentation>Numer identyfikujący fakturę w Krajowym Systemie e-Faktur (KSeF)</xsd:documentation>
173
+ </xsd:annotation>
174
+ <xsd:simpleType>
175
+ <xsd:restriction base="xsd:token">
176
+ <xsd:pattern value="([1-9](\d[1-9]|[1-9]\d)\d{7})-(20[2-9][0-9]|2[1-9]\d{2}|[3-9]\d{3})(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])-([0-9A-F]{6})-?([0-9A-F]{6})-([0-9A-F]{2})"/>
177
+ </xsd:restriction>
178
+ </xsd:simpleType>
179
+ </xsd:element>
180
+ <xsd:element name="NumerFaktury">
181
+ <xsd:annotation>
182
+ <xsd:documentation>Numer faktury</xsd:documentation>
183
+ </xsd:annotation>
184
+ <xsd:simpleType>
185
+ <xsd:restriction base="xsd:token">
186
+ <xsd:minLength value="1"/>
187
+ <xsd:maxLength value="256"/>
188
+ </xsd:restriction>
189
+ </xsd:simpleType>
190
+ </xsd:element>
191
+ <xsd:element name="DataWystawieniaFaktury">
192
+ <xsd:annotation>
193
+ <xsd:documentation>Data wystawienia faktury</xsd:documentation>
194
+ </xsd:annotation>
195
+ <xsd:simpleType>
196
+ <xsd:restriction base="xsd:date">
197
+ <xsd:minInclusive value="2006-01-01"/>
198
+ </xsd:restriction>
199
+ </xsd:simpleType>
200
+ </xsd:element>
201
+ <xsd:element name="DataPrzeslaniaDokumentu">
202
+ <xsd:annotation>
203
+ <xsd:documentation>Data przesłania dokumentu do systemu KSeF</xsd:documentation>
204
+ </xsd:annotation>
205
+ <xsd:simpleType>
206
+ <xsd:restriction base="xsd:dateTime">
207
+ <xsd:whiteSpace value="collapse"/>
208
+ <xsd:minInclusive value="2021-10-01T00:00:00Z"/>
209
+ </xsd:restriction>
210
+ </xsd:simpleType>
211
+ </xsd:element>
212
+ <xsd:element name="DataNadaniaNumeruKSeF">
213
+ <xsd:annotation>
214
+ <xsd:documentation>Czas i data wygenerowania numeru KSeF</xsd:documentation>
215
+ </xsd:annotation>
216
+ <xsd:simpleType>
217
+ <xsd:restriction base="xsd:dateTime">
218
+ <xsd:whiteSpace value="collapse"/>
219
+ <xsd:minInclusive value="2021-10-01T00:00:00Z"/>
220
+ </xsd:restriction>
221
+ </xsd:simpleType>
222
+ </xsd:element>
223
+ <xsd:element name="SkrotDokumentu" type="upo:TSkrotDokumentu">
224
+ <xsd:annotation>
225
+ <xsd:documentation>Wartość funkcji skrótu złożonego dokumentu</xsd:documentation>
226
+ </xsd:annotation>
227
+ </xsd:element>
228
+ <xsd:element name="TrybWysylki">
229
+ <xsd:annotation>
230
+ <xsd:documentation>Tryb wysyłki dokumentu do KSeF: Online lub Offline</xsd:documentation>
231
+ </xsd:annotation>
232
+ <xsd:simpleType>
233
+ <xsd:restriction base="xsd:string">
234
+ <xsd:enumeration value="Online"/>
235
+ <xsd:enumeration value="Offline"/>
236
+ </xsd:restriction>
237
+ </xsd:simpleType>
238
+ </xsd:element>
239
+
240
+ </xsd:sequence>
241
+ </xsd:complexType>
242
+ </xsd:element>
243
+ </xsd:sequence>
244
+ <xsd:attribute name="wersjaSchemy" type="xsd:string" use="optional">
245
+ <xsd:annotation>
246
+ <xsd:documentation>Wersja struktury logicznej UPO KSeF 2.0</xsd:documentation>
247
+ </xsd:annotation>
248
+ </xsd:attribute>
249
+ </xsd:complexType>
250
+ <xsd:unique name="NumerReferencyjnyDokumentuUnique">
251
+ <xsd:selector xpath="upo:Dokument"/>
252
+ <xsd:field xpath="upo:NumerKSeFDokumentu"/>
253
+ </xsd:unique>
254
+ </xsd:element>
255
+
256
+ <xsd:simpleType name="TNip">
257
+ <xsd:restriction base="xsd:token">
258
+ <xsd:pattern value="[1-9]((\d[1-9])|([1-9]\d))\d{7}"/>
259
+ </xsd:restriction>
260
+ </xsd:simpleType>
261
+
262
+ <xsd:simpleType name="TIdWewnetrzny">
263
+ <xsd:restriction base="xsd:token">
264
+ <xsd:pattern value="[1-9]((\d[1-9])|([1-9]\d))\d{7}-\d{5}"/>
265
+ </xsd:restriction>
266
+ </xsd:simpleType>
267
+
268
+ <xsd:simpleType name="TIdZlozonyVatUE">
269
+ <xsd:restriction base="xsd:token">
270
+ <xsd:pattern value="([1-9]((\d[1-9])|([1-9]\d))\d{7}-((AT)(U\d{8})|(BE)([01]{1}\d{9})|(BG)(\d{9,10})|(CY)(\d{8}[A-Z])|(CZ)(\d{8,10})|(DE)(\d{9})|(DK)(\d{8})|(EE)(\d{9})|(EL)(\d{9})|(ES)([A-Z]\d{8}|\d{8}[A-Z]|[A-Z]\d{7}[A-Z])|(FI)(\d{8})|(FR)[A-Z0-9]{2}\d{9}|(HR)(\d{11})|(HU)(\d{8})|(IE)(\d{7}[A-Z]{2}|\d[A-Z0-9+*]\d{5}[A-Z])|(IT)(\d{11})|(LT)(\d{9}|\d{12})|(LU)(\d{8})|(LV)(\d{11})|(MT)(\d{8})|(NL)([A-Z0-9+*]{12})|(PT)(\d{9})|(RO)(\d{2,10})|(SE)(\d{12})|(SI)(\d{8})|(SK)(\d{10})|(XI)((\d{9}|(\d{12}))|(GD|HA)(\d{3}))))"/>
271
+ </xsd:restriction>
272
+ </xsd:simpleType>
273
+
274
+ <xsd:simpleType name="TIdDostawcyUslugPeppol">
275
+ <xsd:restriction base="xsd:token">
276
+ <xsd:pattern value="P[A-Z]{2}[0-9]{6}"/>
277
+ </xsd:restriction>
278
+ </xsd:simpleType>
279
+
280
+ <xsd:simpleType name="TSkrotDokumentu">
281
+ <xsd:restriction base="xsd:token">
282
+ <xsd:minLength value="44"/>
283
+ <xsd:maxLength value="44"/>
284
+ </xsd:restriction>
285
+ </xsd:simpleType>
286
+
287
+ <xsd:simpleType name="TNumerReferencyjny">
288
+ <xsd:restriction base="xsd:token">
289
+ <xsd:pattern value="(20[2-9][0-9]|2[1-9][0-9]{2}|[3-9][0-9]{3})(0[1-9]|1[0-2])(0[1-9]|[1-2][0-9]|3[0-1])-([0-9A-Z]{2})-([0-9A-F]{10})-([0-9A-F]{10})-([0-9A-F]{2})"/>
290
+ </xsd:restriction>
291
+ </xsd:simpleType>
292
+
293
+ </xsd:schema>
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ksef
4
+ module UPO
5
+ # The outcome of checking a UPO against the bundled schema (docs/REFERENCE.md §14.3).
6
+ #
7
+ # Two lists rather than one, because a UPO has a category of schema violation that is
8
+ # **upstream's fault and expected**: `upo-v4-3.xsd` fixes the receiving party's name to
9
+ # `"Ministerstwo Finansów"`, while every non-production environment appends a marker. All
10
+ # six of upstream's own worked examples fail upstream's own schema on exactly that
11
+ # element, and nothing else.
12
+ #
13
+ # So those go in {#warnings} and everything else in {#errors}, and {#valid?} ignores the
14
+ # warnings. A validator that reported them as errors would reject every UPO that TEST
15
+ # and DEMO issue — which is the trap this gem would otherwise have walked into, having
16
+ # already built strict XSD validation for FA(3).
17
+ Validation = Data.define(:errors, :warnings, :receiving_party) do
18
+ # True when nothing but the expected environment-marker discrepancy was found.
19
+ def valid? = errors.empty?
20
+
21
+ # True when the document also matched the `fixed` value — which in practice means it
22
+ # came from production, or from an environment that has stopped appending a marker.
23
+ def clean? = errors.empty? && warnings.empty?
24
+
25
+ # True when the only complaint was the known upstream defect.
26
+ def environment_marked? = errors.empty? && !warnings.empty?
27
+
28
+ def messages = errors + warnings
29
+
30
+ def to_s
31
+ return "valid" if clean?
32
+
33
+ "#{errors.size} error(s), #{warnings.size} warning(s)"
34
+ end
35
+ end
36
+ end
37
+ end