ksef_client 0.1.0.rc1
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 +7 -0
- data/CHANGELOG.md +59 -0
- data/CONTRIBUTING.md +103 -0
- data/LICENSE +27 -0
- data/README.md +150 -0
- data/SECURITY.md +69 -0
- data/docs/REFERENCE.md +403 -0
- data/docs/errors.md +128 -0
- data/lib/ksef/configuration.rb +122 -0
- data/lib/ksef/environments.rb +85 -0
- data/lib/ksef/errors.rb +92 -0
- data/lib/ksef/fa3/schema/LICENSE.upstream.txt +21 -0
- data/lib/ksef/fa3/schema/bazowe/ElementarneTypyDanych_v10-0E.xsd +1 -0
- data/lib/ksef/fa3/schema/bazowe/KodyKrajow_v10-0E.xsd +1283 -0
- data/lib/ksef/fa3/schema/bazowe/StrukturyDanych_v10-0E.xsd +1 -0
- data/lib/ksef/fa3/schema/schemat_FA(3)_v1-0E.xsd +3950 -0
- data/lib/ksef/http/connection.rb +62 -0
- data/lib/ksef/http/error_handler.rb +83 -0
- data/lib/ksef/http/system_warning.rb +29 -0
- data/lib/ksef/problem_details.rb +143 -0
- data/lib/ksef/retry_policy.rb +77 -0
- data/lib/ksef/version.rb +5 -0
- data/lib/ksef.rb +32 -0
- data/lib/ksef_client.rb +5 -0
- metadata +130 -0
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "uri"
|
|
4
|
+
|
|
5
|
+
module Ksef
|
|
6
|
+
# The three public KSeF API 2.0 environments.
|
|
7
|
+
#
|
|
8
|
+
# Every base URL here was read from that environment's *own* OpenAPI document
|
|
9
|
+
# (`servers[0].url`, served at `https://<host>/docs/v2/openapi.json`) on 2026-08-21,
|
|
10
|
+
# rather than inferred by analogy. See docs/REFERENCE.md §2. Do not edit a URL without
|
|
11
|
+
# re-verifying it against that source and updating the ledger.
|
|
12
|
+
#
|
|
13
|
+
# Note the base URL already carries `/v2`; endpoint paths are appended bare. There is
|
|
14
|
+
# no `/api` segment (docs/REFERENCE.md §7.2).
|
|
15
|
+
module Environments
|
|
16
|
+
Environment = Data.define(:name, :base_url, :supports_test_data) do
|
|
17
|
+
# TEST alone exposes the `/testdata/*` helper API and `/collective-identifiers*`.
|
|
18
|
+
# Code touching those paths must be guarded on this.
|
|
19
|
+
def test_data_api? = supports_test_data
|
|
20
|
+
|
|
21
|
+
def production? = name == :prod
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
TEST = Environment.new(
|
|
25
|
+
name: :test,
|
|
26
|
+
base_url: "https://api-test.ksef.mf.gov.pl/v2",
|
|
27
|
+
supports_test_data: true
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
DEMO = Environment.new(
|
|
31
|
+
name: :demo,
|
|
32
|
+
base_url: "https://api-demo.ksef.mf.gov.pl/v2",
|
|
33
|
+
supports_test_data: false
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
PROD = Environment.new(
|
|
37
|
+
name: :prod,
|
|
38
|
+
base_url: "https://api.ksef.mf.gov.pl/v2",
|
|
39
|
+
supports_test_data: false
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
ALL = { test: TEST, demo: DEMO, prod: PROD }.freeze
|
|
43
|
+
NAMES = ALL.keys.freeze
|
|
44
|
+
|
|
45
|
+
class << self
|
|
46
|
+
# @param env [Symbol, String, Environment]
|
|
47
|
+
# @return [Environment]
|
|
48
|
+
# @raise [Ksef::ConfigurationError] on an unknown environment
|
|
49
|
+
def fetch(env)
|
|
50
|
+
return env if env.is_a?(Environment)
|
|
51
|
+
|
|
52
|
+
key = env.respond_to?(:to_sym) ? env.to_sym : nil
|
|
53
|
+
ALL.fetch(key) do
|
|
54
|
+
raise ConfigurationError,
|
|
55
|
+
"Unknown KSeF environment #{env.inspect}. Known: #{NAMES.map(&:inspect).join(", ")}. " \
|
|
56
|
+
"For a non-public deployment, pass Ksef::Environments.custom(base_url: ...)."
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Escape hatch for deployments not covered by the three published environments
|
|
61
|
+
# (DESIGN.md §6.1).
|
|
62
|
+
#
|
|
63
|
+
# @param base_url [String] must be HTTPS, and should include the `/v2` suffix
|
|
64
|
+
# @return [Environment]
|
|
65
|
+
def custom(base_url:, name: :custom, supports_test_data: false)
|
|
66
|
+
uri = begin
|
|
67
|
+
URI.parse(base_url)
|
|
68
|
+
rescue URI::InvalidURIError => e
|
|
69
|
+
raise ConfigurationError, "Invalid base_url #{base_url.inspect}: #{e.message}"
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
unless uri.is_a?(URI::HTTPS)
|
|
73
|
+
raise ConfigurationError,
|
|
74
|
+
"base_url must be HTTPS, got #{base_url.inspect}. TLS is not optional (DESIGN.md §4.5)."
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
Environment.new(
|
|
78
|
+
name: name.to_sym,
|
|
79
|
+
base_url: base_url.chomp("/"),
|
|
80
|
+
supports_test_data: supports_test_data
|
|
81
|
+
)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
data/lib/ksef/errors.rb
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# The error hierarchy from DESIGN.md §6.7, extended with the 403 and 410 branches the
|
|
4
|
+
# API actually returns (docs/REFERENCE.md §7.4).
|
|
5
|
+
#
|
|
6
|
+
# This file defines many constants and no `Ksef::Errors`, so Zeitwerk cannot manage it —
|
|
7
|
+
# `lib/ksef.rb` ignores it and requires it eagerly.
|
|
8
|
+
module Ksef
|
|
9
|
+
# Base class for everything this gem raises. Rescue this to catch all of it.
|
|
10
|
+
#
|
|
11
|
+
# `#problem` carries the parsed KSeF error body when the error came from an API
|
|
12
|
+
# response, and is nil for locally-raised errors.
|
|
13
|
+
class Error < StandardError
|
|
14
|
+
# @return [Ksef::ProblemDetails, nil]
|
|
15
|
+
attr_reader :problem
|
|
16
|
+
|
|
17
|
+
def initialize(message = nil, problem: nil)
|
|
18
|
+
@problem = problem
|
|
19
|
+
super(message)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Invalid or missing client configuration. Raised locally, before any request.
|
|
24
|
+
class ConfigurationError < Error; end
|
|
25
|
+
|
|
26
|
+
# Challenge, KSeF-token, signature or JWT problems, including HTTP 401.
|
|
27
|
+
class AuthenticationError < Error; end
|
|
28
|
+
|
|
29
|
+
# Raised locally by the FA(3) validator (DESIGN.md §7.7), never by the transport layer.
|
|
30
|
+
class ValidationError < Error; end
|
|
31
|
+
|
|
32
|
+
# Any error response from the KSeF API.
|
|
33
|
+
class ApiError < Error
|
|
34
|
+
# @return [Integer, nil] HTTP status
|
|
35
|
+
def status = problem&.status
|
|
36
|
+
|
|
37
|
+
# @return [Integer, nil] the first KSeF error code, when the body carried one
|
|
38
|
+
def code = problem&.code
|
|
39
|
+
|
|
40
|
+
# @return [Array<String>] flattened detail messages
|
|
41
|
+
def details = problem&.details || []
|
|
42
|
+
|
|
43
|
+
# @return [String, nil] the Ministry's correlation id — quote this in support requests
|
|
44
|
+
def trace_id = problem&.trace_id
|
|
45
|
+
|
|
46
|
+
# @return [Object, nil] the undecoded response body
|
|
47
|
+
def raw = problem&.raw
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# The invoice was rejected by KSeF on schema or business grounds.
|
|
51
|
+
class InvoiceRejectedError < ApiError; end
|
|
52
|
+
|
|
53
|
+
# A session could not be opened, used or closed.
|
|
54
|
+
class SessionError < ApiError; end
|
|
55
|
+
|
|
56
|
+
# HTTP 403. Carries a structured reason (docs/REFERENCE.md §5.3) rather than just prose.
|
|
57
|
+
class AuthorizationError < ApiError
|
|
58
|
+
# @return [String, nil] one of `missing-permissions`, `ip-not-allowed`,
|
|
59
|
+
# `insufficient-resource-access`, `auth-method-not-allowed`,
|
|
60
|
+
# `security-service-blocked`, `context-type-not-allowed`
|
|
61
|
+
def reason_code = problem&.reason_code
|
|
62
|
+
|
|
63
|
+
# @return [Hash] reason-dependent payload, e.g. `requiredAnyOfPermissions`
|
|
64
|
+
def security = problem&.security || {}
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# HTTP 410. The resource existed but is no longer available.
|
|
68
|
+
class ResourceGoneError < ApiError; end
|
|
69
|
+
|
|
70
|
+
# HTTP 429. Retryable, but only for idempotent requests (DESIGN.md §6.7).
|
|
71
|
+
class RateLimitedError < ApiError
|
|
72
|
+
# @return [Integer, nil] seconds to wait, from the `Retry-After` header.
|
|
73
|
+
# Authoritative — the block period is dynamic and lengthens with repeat offences
|
|
74
|
+
# (docs/REFERENCE.md §6). Never work around a 429 by rotating IPs.
|
|
75
|
+
attr_reader :retry_after
|
|
76
|
+
|
|
77
|
+
def initialize(message = nil, problem: nil, retry_after: nil)
|
|
78
|
+
@retry_after = retry_after
|
|
79
|
+
super(message, problem: problem)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# HTTP 5xx. Not declared anywhere in the OpenAPI contract (docs/REFERENCE.md §5.4), so
|
|
84
|
+
# the body shape is unknown and `#problem` may hold only a raw payload.
|
|
85
|
+
class ServerError < ApiError; end
|
|
86
|
+
|
|
87
|
+
# The request exceeded the configured open or read timeout.
|
|
88
|
+
class TimeoutError < Error; end
|
|
89
|
+
|
|
90
|
+
# The connection failed, TLS included.
|
|
91
|
+
class ConnectionError < Error; end
|
|
92
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Ministerstwo Finansów
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|