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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +1424 -0
- data/CONTRIBUTING.md +45 -6
- data/README.md +377 -25
- data/SECURITY.md +25 -2
- data/docs/REFERENCE.md +3099 -34
- data/docs/errors.md +33 -4
- data/docs/field_mapping.md +390 -0
- data/lib/ksef/auth/access_token.rb +150 -0
- data/lib/ksef/auth/authorization_policy.rb +88 -0
- data/lib/ksef/auth/challenge.rb +53 -0
- data/lib/ksef/auth/client.rb +143 -0
- data/lib/ksef/auth/initiation.rb +22 -0
- data/lib/ksef/auth/operation_status.rb +34 -0
- data/lib/ksef/auth/schema/schemat_auth_v2-0.xsd +109 -0
- data/lib/ksef/auth/schema/schemat_auth_v2-1.xsd +109 -0
- data/lib/ksef/auth/signature_template.rb +120 -0
- data/lib/ksef/auth/signer.rb +132 -0
- data/lib/ksef/auth/status.rb +69 -0
- data/lib/ksef/auth/token.rb +121 -0
- data/lib/ksef/auth/token_info.rb +27 -0
- data/lib/ksef/auth/token_request.rb +142 -0
- data/lib/ksef/auth/tokens.rb +24 -0
- data/lib/ksef/auth/validator.rb +84 -0
- data/lib/ksef/auth/xades.rb +40 -0
- data/lib/ksef/auth.rb +43 -0
- data/lib/ksef/client/receipt.rb +37 -0
- data/lib/ksef/client/session.rb +68 -0
- data/lib/ksef/client.rb +314 -0
- data/lib/ksef/crypto/certificate.rb +78 -0
- data/lib/ksef/crypto/digest.rb +25 -0
- data/lib/ksef/crypto/encryptor.rb +131 -0
- data/lib/ksef/crypto/public_keys.rb +146 -0
- data/lib/ksef/crypto.rb +66 -0
- data/lib/ksef/environments.rb +1 -1
- data/lib/ksef/errors.rb +35 -2
- data/lib/ksef/fa3/address.rb +64 -0
- data/lib/ksef/fa3/advance_checks.rb +55 -0
- data/lib/ksef/fa3/advance_invoice.rb +55 -0
- data/lib/ksef/fa3/advance_reader.rb +59 -0
- data/lib/ksef/fa3/attachment.rb +43 -0
- data/lib/ksef/fa3/attachment_checks.rb +121 -0
- data/lib/ksef/fa3/attachment_reader.rb +99 -0
- data/lib/ksef/fa3/attachment_table.rb +98 -0
- data/lib/ksef/fa3/builder/advances.rb +57 -0
- data/lib/ksef/fa3/builder/corrections.rb +46 -0
- data/lib/ksef/fa3/builder/subjects.rb +71 -0
- data/lib/ksef/fa3/builder.rb +149 -0
- data/lib/ksef/fa3/business_validator.rb +134 -0
- data/lib/ksef/fa3/canonical.rb +44 -0
- data/lib/ksef/fa3/corrected_invoice.rb +53 -0
- data/lib/ksef/fa3/correction.rb +151 -0
- data/lib/ksef/fa3/correction_checks.rb +106 -0
- data/lib/ksef/fa3/correction_reader.rb +88 -0
- data/lib/ksef/fa3/data_block.rb +75 -0
- data/lib/ksef/fa3/document_mapping.rb +142 -0
- data/lib/ksef/fa3/document_validator.rb +174 -0
- data/lib/ksef/fa3/element_tree.rb +49 -0
- data/lib/ksef/fa3/field_checks.rb +151 -0
- data/lib/ksef/fa3/formatting.rb +273 -0
- data/lib/ksef/fa3/generated/enums.rb +652 -0
- data/lib/ksef/fa3/generated/types.rb +3308 -0
- data/lib/ksef/fa3/invoice.rb +259 -0
- data/lib/ksef/fa3/issue.rb +32 -0
- data/lib/ksef/fa3/line.rb +162 -0
- data/lib/ksef/fa3/meta_entry.rb +45 -0
- data/lib/ksef/fa3/model_validator.rb +219 -0
- data/lib/ksef/fa3/nip.rb +59 -0
- data/lib/ksef/fa3/node_reader.rb +46 -0
- data/lib/ksef/fa3/order.rb +55 -0
- data/lib/ksef/fa3/order_line.rb +70 -0
- data/lib/ksef/fa3/parser.rb +208 -0
- data/lib/ksef/fa3/provenance.rb +137 -0
- data/lib/ksef/fa3/rounding_inference.rb +87 -0
- data/lib/ksef/fa3/row_reader.rb +66 -0
- data/lib/ksef/fa3/serializer.rb +140 -0
- data/lib/ksef/fa3/subject.rb +128 -0
- data/lib/ksef/fa3/subject_checks.rb +126 -0
- data/lib/ksef/fa3/subject_reader.rb +76 -0
- data/lib/ksef/fa3/summaries.rb +107 -0
- data/lib/ksef/fa3/summary_checks.rb +75 -0
- data/lib/ksef/fa3/table_column.rb +49 -0
- data/lib/ksef/fa3/totals.rb +119 -0
- data/lib/ksef/fa3/validator.rb +79 -0
- data/lib/ksef/fa3/vat_rate.rb +94 -0
- data/lib/ksef/fa3.rb +59 -0
- data/lib/ksef/http/connection.rb +47 -4
- data/lib/ksef/http/json_decoder.rb +53 -0
- data/lib/ksef/http/retry.rb +105 -0
- data/lib/ksef/invoices/client.rb +72 -0
- data/lib/ksef/ksef_number.rb +150 -0
- data/lib/ksef/sessions/invoice_codes.rb +70 -0
- data/lib/ksef/sessions/invoice_state.rb +68 -0
- data/lib/ksef/sessions/online.rb +169 -0
- data/lib/ksef/sessions/session_codes.rb +73 -0
- data/lib/ksef/sessions/session_state.rb +48 -0
- data/lib/ksef/sessions/status.rb +138 -0
- data/lib/ksef/sessions/upo_page.rb +36 -0
- data/lib/ksef/sessions.rb +91 -0
- data/lib/ksef/upo/client.rb +154 -0
- data/lib/ksef/upo/document.rb +74 -0
- data/lib/ksef/upo/schema/upo-v4-3.xsd +293 -0
- data/lib/ksef/upo/validation.rb +37 -0
- data/lib/ksef/upo/validator.rb +124 -0
- data/lib/ksef/upo.rb +55 -0
- data/lib/ksef/version.rb +1 -1
- data/lib/ksef.rb +18 -0
- 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
|