cuzk-soap 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.
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CUZK
4
+ module SOAP
5
+ class Error < StandardError; end
6
+
7
+ # WSDP service errors
8
+ class WSDPError < Error
9
+ attr_reader :fault_code, :fault_string
10
+
11
+ def initialize(message = nil, fault_code: nil, fault_string: nil)
12
+ @fault_code = fault_code
13
+ @fault_string = fault_string
14
+ super(message || fault_string)
15
+ end
16
+ end
17
+
18
+ class AccountError < WSDPError; end
19
+ class InsufficientFundsError < AccountError; end
20
+ class AccountSuspendedError < AccountError; end
21
+ class ServiceUnavailableError < WSDPError; end
22
+ class InvalidRequestError < WSDPError; end
23
+
24
+ # Document errors
25
+ class DocumentError < Error; end
26
+ class ExtractGenerationError < DocumentError; end
27
+ class InvalidPropertyError < DocumentError; end
28
+ class CertificationError < DocumentError; end
29
+
30
+ # Professional service errors
31
+ class BatchProcessingError < Error; end
32
+ class CostLimitExceededError < Error; end
33
+ class ComplianceViolationError < Error; end
34
+ class IntegrationError < Error; end
35
+
36
+ # Infrastructure errors
37
+ class SOAPError < Error; end
38
+ class AuthenticationError < SOAPError; end
39
+ class CertificateError < SOAPError; end
40
+ class NetworkTimeoutError < SOAPError; end
41
+ class CircuitBreakerOpenError < SOAPError; end
42
+ class MaxRetriesExceededError < SOAPError; end
43
+
44
+ # Configuration errors
45
+ class ConfigurationError < Error; end
46
+ end
47
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CUZK
4
+ module SOAP
5
+ class PropertyReport
6
+ attr_reader :property_id, :property_type, :raw_data, :generated_at
7
+
8
+ def initialize(property_id:, property_type:, raw_data:)
9
+ @property_id = property_id
10
+ @property_type = property_type
11
+ @raw_data = raw_data
12
+ @generated_at = Time.now.utc
13
+ end
14
+
15
+ def self.for_parcel(client, katastr_uzemi_kod:, kmenove_cislo:, format: 'xml')
16
+ raw_data = client.report_service.informace_o_parcele(
17
+ katastr_uzemi_kod: katastr_uzemi_kod,
18
+ kmenove_cislo: kmenove_cislo,
19
+ format: format
20
+ )
21
+
22
+ new(
23
+ property_id: "#{katastr_uzemi_kod}/#{kmenove_cislo}",
24
+ property_type: :parcel,
25
+ raw_data: raw_data
26
+ )
27
+ end
28
+
29
+ def self.for_building(client, katastr_uzemi_kod:, cislo_budovy:, format: 'xml')
30
+ raw_data = client.report_service.informace_o_budove(
31
+ katastr_uzemi_kod: katastr_uzemi_kod,
32
+ cislo_budovy: cislo_budovy,
33
+ format: format
34
+ )
35
+
36
+ new(
37
+ property_id: "#{katastr_uzemi_kod}/#{cislo_budovy}",
38
+ property_type: :building,
39
+ raw_data: raw_data
40
+ )
41
+ end
42
+
43
+ def self.for_unit(client, jednotka_id:, format: 'xml')
44
+ raw_data = client.report_service.informace_o_jednotce(
45
+ jednotka_id: jednotka_id,
46
+ format: format
47
+ )
48
+
49
+ new(
50
+ property_id: jednotka_id.to_s,
51
+ property_type: :unit,
52
+ raw_data: raw_data
53
+ )
54
+ end
55
+
56
+ def basic_info
57
+ extract_section(:zakladni_informace) || extract_section(:basic_info)
58
+ end
59
+
60
+ def location_info
61
+ extract_section(:umisteni) || extract_section(:location)
62
+ end
63
+
64
+ def ownership
65
+ extract_section(:vlastnictvi) || extract_section(:ownership)
66
+ end
67
+
68
+ def encumbrances
69
+ extract_section(:omezeni) || extract_section(:encumbrances) || []
70
+ end
71
+
72
+ def to_h
73
+ {
74
+ property_id: @property_id,
75
+ property_type: @property_type,
76
+ generated_at: @generated_at,
77
+ data: @raw_data
78
+ }
79
+ end
80
+
81
+ private
82
+
83
+ def extract_section(key)
84
+ return nil unless @raw_data.is_a?(Hash)
85
+
86
+ @raw_data[key] || @raw_data[key.to_s]
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CUZK
4
+ module SOAP
5
+ module Services
6
+ class AccountService < BaseService
7
+ def stav_ws
8
+ call_operation(:stav_ws, {})
9
+ end
10
+ alias account_status stav_ws
11
+
12
+ def zmen_heslo(stare_heslo:, nove_heslo:)
13
+ message = {
14
+ stareHeslo: stare_heslo,
15
+ noveHeslo: nove_heslo
16
+ }
17
+
18
+ call_operation(:zmen_heslo, message)
19
+ end
20
+ alias change_password zmen_heslo
21
+
22
+ private
23
+
24
+ def service_name
25
+ :ucet
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CUZK
4
+ module SOAP
5
+ module Services
6
+ class BaseService
7
+ attr_reader :connection
8
+
9
+ def initialize(connection)
10
+ @connection = connection
11
+ end
12
+
13
+ private
14
+
15
+ def call_operation(operation, message = {})
16
+ response = connection.call(service_name, operation, message)
17
+ parse_response(response)
18
+ end
19
+
20
+ def parse_response(response)
21
+ body = response.body
22
+ return body if body.is_a?(Hash)
23
+
24
+ body
25
+ end
26
+
27
+ def extract_document(response, format)
28
+ body = response.body
29
+ case format.to_s
30
+ when 'pdf'
31
+ extract_pdf(body)
32
+ when 'xml'
33
+ extract_xml(body)
34
+ when 'html'
35
+ extract_html(body)
36
+ else
37
+ body
38
+ end
39
+ end
40
+
41
+ def extract_pdf(body)
42
+ # WSDP returns PDF as base64-encoded MTOM attachment or inline
43
+ find_binary_content(body)
44
+ end
45
+
46
+ def extract_xml(body)
47
+ find_content(body)
48
+ end
49
+
50
+ def extract_html(body)
51
+ find_content(body)
52
+ end
53
+
54
+ def find_binary_content(data)
55
+ return Base64.decode64(data) if data.is_a?(String)
56
+
57
+ if data.is_a?(Hash)
58
+ data.each_value do |value|
59
+ result = find_binary_content(value)
60
+ return result if result
61
+ end
62
+ end
63
+
64
+ nil
65
+ end
66
+
67
+ def find_content(data)
68
+ return data if data.is_a?(String)
69
+
70
+ if data.is_a?(Hash)
71
+ data.each_value do |value|
72
+ result = find_content(value)
73
+ return result if result
74
+ end
75
+ end
76
+
77
+ nil
78
+ end
79
+
80
+ def service_name
81
+ raise NotImplementedError, "#{self.class} must implement #service_name"
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CUZK
4
+ module SOAP
5
+ module Services
6
+ class CiselnikService < BaseService
7
+ def seznam_ku
8
+ call_operation(:seznam_ku, {})
9
+ end
10
+ alias list_cadastral_units seznam_ku
11
+
12
+ private
13
+
14
+ def service_name
15
+ :ciselnik
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CUZK
4
+ module SOAP
5
+ module Services
6
+ class InformaceService < BaseService
7
+ def cti_os(opravneny_subjekt_id:)
8
+ call_operation(:cti_os, { opravnenySubjektId: opravneny_subjekt_id })
9
+ end
10
+ alias read_subject cti_os
11
+
12
+ private
13
+
14
+ def service_name
15
+ :informace
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CUZK
4
+ module SOAP
5
+ module Services
6
+ # ReportService wraps the WSDP "sestavy" service.
7
+ # Report generation is ASYNCHRONOUS:
8
+ # 1. Call generuj* to request generation -> returns request metadata
9
+ # 2. Poll seznam_sestav to check if the report is ready
10
+ # 3. Call vrat_sestavu to download the finished report
11
+ # 4. Optionally call smaz_sestavu to clean up
12
+ class ReportService < BaseService
13
+ VALID_FORMATS = %w[pdf xml html zip].freeze
14
+
15
+ # --- Report generation requests ---
16
+
17
+ def generuj_lv(lv_id:, format: 'pdf', **options)
18
+ validate_format!(format)
19
+ message = { lvId: lv_id, format: format }.merge(options)
20
+
21
+ call_operation(:generuj_lv, message)
22
+ end
23
+ alias generate_lv generuj_lv
24
+
25
+ # Find parcel by parcelaId OR katastrUzemiKod + kmenoveCislo
26
+ def generuj_info_o_parcelach(format: 'pdf', parcela_id: nil,
27
+ katastr_uzemi_kod: nil, kmenove_cislo: nil, **options)
28
+ validate_format!(format)
29
+ message = if parcela_id
30
+ { parcelaId: parcela_id }
31
+ else
32
+ { katastrUzemiKod: katastr_uzemi_kod, kmenoveCislo: kmenove_cislo }
33
+ end
34
+ message[:format] = format
35
+ message.merge!(options)
36
+
37
+ call_operation(:generuj_info_o_parcelach, message)
38
+ end
39
+ alias generate_parcel_info generuj_info_o_parcelach
40
+
41
+ def generuj_info_o_stavbach(format: 'pdf', stavba_id: nil,
42
+ cast_obce_kod: nil, typ_stavby_kod: nil, cislo_domovni: nil, **options)
43
+ validate_format!(format)
44
+ message = if stavba_id
45
+ { stavbaId: stavba_id }
46
+ else
47
+ { castObceKod: cast_obce_kod, typStavbyKod: typ_stavby_kod, cisloDomovni: cislo_domovni }
48
+ end
49
+ message[:format] = format
50
+ message.merge!(options)
51
+
52
+ call_operation(:generuj_info_o_stavbach, message)
53
+ end
54
+ alias generate_building_info generuj_info_o_stavbach
55
+
56
+ def generuj_info_o_jednotkach(jednotka_id:, format: 'pdf', **options)
57
+ validate_format!(format)
58
+ message = { jednotkaId: jednotka_id, format: format }.merge(options)
59
+
60
+ call_operation(:generuj_info_o_jednotkach, message)
61
+ end
62
+ alias generate_unit_info generuj_info_o_jednotkach
63
+
64
+ def generuj_prehled_vlastnictvi(opravneny_subjekt_id:, format: 'pdf', **options)
65
+ validate_format!(format)
66
+ message = { opravnenySubjektId: opravneny_subjekt_id, format: format }.merge(options)
67
+
68
+ call_operation(:generuj_prehled_vlastnictvi, message)
69
+ end
70
+ alias generate_ownership_overview generuj_prehled_vlastnictvi
71
+
72
+ def generuj_cenove_udaje_dle_nemovitosti(nemovitost_id:, format: 'pdf', **options)
73
+ validate_format!(format)
74
+ message = { nemovitostId: nemovitost_id, format: format }.merge(options)
75
+
76
+ call_operation(:generuj_cenove_udaje_dle_nemovitosti, message)
77
+ end
78
+ alias generate_price_by_property generuj_cenove_udaje_dle_nemovitosti
79
+
80
+ # --- Report management ---
81
+
82
+ def seznam_sestav(id_sestavy: nil)
83
+ message = {}
84
+ message[:idSestavy] = id_sestavy if id_sestavy
85
+ call_operation(:seznam_sestav, message)
86
+ end
87
+ alias list_reports seznam_sestav
88
+
89
+ def vrat_sestavu(id_sestavy:)
90
+ call_operation(:vrat_sestavu, { idSestavy: id_sestavy })
91
+ end
92
+ alias download_report vrat_sestavu
93
+
94
+ def smaz_sestavu(id_sestavy:)
95
+ call_operation(:smaz_sestavu, { idSestavy: id_sestavy })
96
+ end
97
+ alias delete_report smaz_sestavu
98
+
99
+ private
100
+
101
+ def service_name
102
+ :sestavy
103
+ end
104
+
105
+ def validate_format!(format)
106
+ return if VALID_FORMATS.include?(format.to_s.downcase)
107
+
108
+ raise ArgumentError, "Invalid format: #{format}. Valid formats: #{VALID_FORMATS.join(', ')}"
109
+ end
110
+ end
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CUZK
4
+ module SOAP
5
+ module Services
6
+ class SearchService < BaseService
7
+ # Find parcel by parcelaId OR katastrUzemiKod + kmenoveCislo + optional poddeleni
8
+ def najdi_parcelu(katastr_uzemi_kod: nil, kmenove_cislo: nil, parcela_id: nil, **options)
9
+ message = if parcela_id
10
+ { parcelaId: parcela_id }
11
+ else
12
+ { katastrUzemiKod: katastr_uzemi_kod, kmenoveCislo: kmenove_cislo }
13
+ end
14
+ message.merge!(options)
15
+
16
+ call_operation(:najdi_parcelu, message)
17
+ end
18
+ alias search_parcel najdi_parcelu
19
+
20
+ # Find building by stavbaId OR castObceKod + typStavbyKod + cisloDomovni
21
+ def najdi_stavbu(stavba_id: nil, cast_obce_kod: nil, typ_stavby_kod: nil, cislo_domovni: nil, **options)
22
+ message = if stavba_id
23
+ { stavbaId: stavba_id }
24
+ else
25
+ {
26
+ castObceKod: cast_obce_kod,
27
+ typStavbyKod: typ_stavby_kod,
28
+ cisloDomovni: cislo_domovni
29
+ }
30
+ end
31
+ message.merge!(options)
32
+
33
+ call_operation(:najdi_stavbu, message)
34
+ end
35
+ alias search_building najdi_stavbu
36
+
37
+ def najdi_jednotku(katastr_uzemi_kod: nil, cislo_jednotky: nil, jednotka_id: nil, **options)
38
+ message = if jednotka_id
39
+ { jednotkaId: jednotka_id }
40
+ else
41
+ { katastrUzemiKod: katastr_uzemi_kod, cisloJednotky: cislo_jednotky }
42
+ end
43
+ message.merge!(options)
44
+
45
+ call_operation(:najdi_jednotku, message)
46
+ end
47
+ alias search_unit najdi_jednotku
48
+
49
+ # Find person/entity: by ico/nazev (legal) or jmeno/prijmeni (natural)
50
+ def najdi_os(**params)
51
+ message = {}
52
+ message[:ico] = params[:ico] if params[:ico]
53
+ message[:nazev] = params[:nazev] if params[:nazev]
54
+ message[:jmeno] = params[:jmeno] if params[:jmeno]
55
+ message[:prijmeni] = params[:prijmeni] if params[:prijmeni]
56
+ message[:rc] = params[:rc] if params[:rc]
57
+ message[:datumNarozeni] = params[:datum_narozeni] if params[:datum_narozeni]
58
+ message[:datumK] = params[:datum_k] if params[:datum_k]
59
+
60
+ call_operation(:najdi_os, message)
61
+ end
62
+ alias search_subject najdi_os
63
+
64
+ def najdi_pravo_stavby(**options)
65
+ call_operation(:najdi_pravo_stavby, options)
66
+ end
67
+ alias search_building_right najdi_pravo_stavby
68
+
69
+ def najdi_rizeni(**options)
70
+ call_operation(:najdi_rizeni, options)
71
+ end
72
+ alias search_proceedings najdi_rizeni
73
+
74
+ def stav_ws
75
+ call_operation(:stav_ws, {})
76
+ end
77
+ alias service_status stav_ws
78
+
79
+ private
80
+
81
+ def service_name
82
+ :vyhledat
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CUZK
4
+ module SOAP
5
+ VERSION = '0.1.0'
6
+ end
7
+ end
@@ -0,0 +1,171 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CUZK
4
+ module SOAP
5
+ class WSDLManager
6
+ WSDL_FILES = {
7
+ v31: {
8
+ vyhledat: 'vyhledat_v3_1.wsdl',
9
+ sestavy: 'sestavy_v3_1.wsdl',
10
+ informace: 'informace_v3_1.wsdl',
11
+ ciselnik: 'ciselnik_v3_1.wsdl',
12
+ ucet: 'ucet_v3_1.wsdl'
13
+ },
14
+ v29: {
15
+ vyhledat: 'vyhledat_v29.wsdl',
16
+ sestavy: 'sestavy_v29.wsdl',
17
+ informace: 'informace_v29.wsdl',
18
+ ciselnik: 'ciselnik_v29.wsdl',
19
+ ucet: 'ucet_v29.wsdl'
20
+ }
21
+ }.freeze
22
+
23
+ # v3.1 uses URN-style type namespaces (distinct from service namespaces)
24
+ WSDP_TYPE_NAMESPACES = {
25
+ v31: {
26
+ vyhledat: 'urn:cz:gov:cuzk:iskn:types:wsdp:vyhledat:3.1',
27
+ sestavy: 'urn:cz:gov:cuzk:iskn:types:wsdp:sestavy:3.1',
28
+ informace: 'urn:cz:gov:cuzk:iskn:types:wsdp:informace:3.1',
29
+ ciselnik: 'urn:cz:gov:cuzk:iskn:types:wsdp:ciselnik:3.1',
30
+ ucet: 'urn:cz:gov:cuzk:iskn:types:wsdp:ucet:3.1'
31
+ },
32
+ v29: {
33
+ vyhledat: 'http://katastr.cuzk.cz/vyhledat/types/v2.9',
34
+ sestavy: 'http://katastr.cuzk.cz/sestavy/types/v2.9',
35
+ informace: 'http://katastr.cuzk.cz/informace/types/v2.9',
36
+ ciselnik: 'http://katastr.cuzk.cz/ciselnik/types/v2.9',
37
+ ucet: 'http://katastr.cuzk.cz/ucet/types/v2.9'
38
+ }
39
+ }.freeze
40
+
41
+ # v3.1 uses URN-style namespaces per service
42
+ WSDP_SERVICE_NAMESPACES = {
43
+ v31: {
44
+ vyhledat: 'urn:cz:gov:cuzk:iskn:services:wsdp:vyhledat:3.1',
45
+ sestavy: 'urn:cz:gov:cuzk:iskn:services:wsdp:sestavy:3.1',
46
+ informace: 'urn:cz:gov:cuzk:iskn:services:wsdp:informace:3.1',
47
+ ciselnik: 'urn:cz:gov:cuzk:iskn:services:wsdp:ciselnik:3.1',
48
+ ucet: 'urn:cz:gov:cuzk:iskn:services:wsdp:ucet:3.1'
49
+ },
50
+ v29: {
51
+ vyhledat: 'http://katastr.cuzk.cz/vyhledat/v2.9',
52
+ sestavy: 'http://katastr.cuzk.cz/sestavy/v2.9',
53
+ informace: 'http://katastr.cuzk.cz/informace/v2.9',
54
+ ciselnik: 'http://katastr.cuzk.cz/ciselnik/v2.9',
55
+ ucet: 'http://katastr.cuzk.cz/ucet/v2.9'
56
+ }
57
+ }.freeze
58
+
59
+ # WSDL directory paths differ between versions
60
+ WSDL_DIR_PATHS = {
61
+ v31: 'ws3_1/wsdp/',
62
+ v29: 'ws29/wsdp/'
63
+ }.freeze
64
+
65
+ # SOAP endpoints follow a different path pattern
66
+ SOAP_ENDPOINT_PATHS = {
67
+ v31: 'ws/wsdp/3.1/',
68
+ v29: 'ws/wsdp/2.9/'
69
+ }.freeze
70
+
71
+ TRIAL_BASE_URL = 'https://wsdptrial.cuzk.gov.cz/trial/'
72
+ PRODUCTION_BASE_URL = 'https://katastr.cuzk.gov.cz/'
73
+
74
+ attr_reader :version, :environment
75
+
76
+ def initialize(version: :v31, environment: :production)
77
+ @version = version
78
+ @environment = environment
79
+ end
80
+
81
+ def wsdl_url(service)
82
+ filename = wsdl_files.fetch(service) do
83
+ raise ArgumentError, "Unknown service: #{service}. Available: #{available_services.join(', ')}"
84
+ end
85
+
86
+ "#{documentation_base_url}#{wsdl_dir_path}#{filename}"
87
+ end
88
+
89
+ def soap_endpoint_url(service)
90
+ unless wsdl_files.key?(service)
91
+ raise ArgumentError, "Unknown service: #{service}. Available: #{available_services.join(', ')}"
92
+ end
93
+
94
+ "#{base_url}#{soap_endpoint_path}#{service}"
95
+ end
96
+
97
+ def namespace(service)
98
+ namespaces = WSDP_SERVICE_NAMESPACES.fetch(@version) do
99
+ raise ConfigurationError, "Unknown WSDP version: #{@version}"
100
+ end
101
+
102
+ namespaces.fetch(service) do
103
+ raise ArgumentError, "Unknown service: #{service}"
104
+ end
105
+ end
106
+
107
+ def types_namespace(service)
108
+ type_namespaces = WSDP_TYPE_NAMESPACES.fetch(@version) do
109
+ raise ConfigurationError, "Unknown WSDP version: #{@version}"
110
+ end
111
+
112
+ type_namespaces.fetch(service) do
113
+ raise ArgumentError, "Unknown service: #{service}"
114
+ end
115
+ end
116
+
117
+ def soap_action(service, operation)
118
+ ns = namespace(service)
119
+ if @version == :v31
120
+ # v3.1 uses urn-style: urn:cz:gov:cuzk:iskn:services:wsdp:{service}:{operation}
121
+ base = ns.sub(/:[\d.]+\z/, '')
122
+ "#{base}:#{operation}"
123
+ else
124
+ # v2.9 uses http-style: http://katastr.cuzk.cz/{service}/{operation}
125
+ base = ns.sub(%r{/v[\d.]+\z}, '')
126
+ "#{base}/#{operation}"
127
+ end
128
+ end
129
+
130
+ def available_services
131
+ wsdl_files.keys
132
+ end
133
+
134
+ def service_available?(service)
135
+ wsdl_files.key?(service)
136
+ end
137
+
138
+ def all_wsdl_urls
139
+ wsdl_files.transform_values { |filename| "#{documentation_base_url}#{wsdl_dir_path}#{filename}" }
140
+ end
141
+
142
+ def all_soap_endpoints
143
+ wsdl_files.keys.to_h { |service| [service, soap_endpoint_url(service)] }
144
+ end
145
+
146
+ private
147
+
148
+ def wsdl_files
149
+ WSDL_FILES.fetch(@version) do
150
+ raise ConfigurationError, "Unknown WSDP version: #{@version}. Available: #{WSDL_FILES.keys.join(', ')}"
151
+ end
152
+ end
153
+
154
+ def wsdl_dir_path
155
+ WSDL_DIR_PATHS.fetch(@version)
156
+ end
157
+
158
+ def soap_endpoint_path
159
+ SOAP_ENDPOINT_PATHS.fetch(@version)
160
+ end
161
+
162
+ def base_url
163
+ @environment == :testing ? TRIAL_BASE_URL : PRODUCTION_BASE_URL
164
+ end
165
+
166
+ def documentation_base_url
167
+ "#{base_url}dokumentace/"
168
+ end
169
+ end
170
+ end
171
+ end