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.
- checksums.yaml +7 -0
- data/README.md +311 -0
- data/lib/cuzk/soap/authentication/test_credentials.rb +32 -0
- data/lib/cuzk/soap/authentication/wsdp_credentials.rb +57 -0
- data/lib/cuzk/soap/batch_processor.rb +112 -0
- data/lib/cuzk/soap/change_tracker.rb +110 -0
- data/lib/cuzk/soap/client.rb +196 -0
- data/lib/cuzk/soap/configuration.rb +117 -0
- data/lib/cuzk/soap/connection.rb +138 -0
- data/lib/cuzk/soap/cost_manager.rb +136 -0
- data/lib/cuzk/soap/document_manager.rb +106 -0
- data/lib/cuzk/soap/errors.rb +47 -0
- data/lib/cuzk/soap/property_report.rb +90 -0
- data/lib/cuzk/soap/services/account_service.rb +30 -0
- data/lib/cuzk/soap/services/base_service.rb +86 -0
- data/lib/cuzk/soap/services/ciselnik_service.rb +20 -0
- data/lib/cuzk/soap/services/informace_service.rb +20 -0
- data/lib/cuzk/soap/services/report_service.rb +113 -0
- data/lib/cuzk/soap/services/search_service.rb +87 -0
- data/lib/cuzk/soap/version.rb +7 -0
- data/lib/cuzk/soap/wsdl_manager.rb +171 -0
- data/lib/cuzk/soap.rb +39 -0
- metadata +250 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 854f7991c5000a7bf1e10e0c83e117cd945be2962da139dad760fe9f408dd612
|
|
4
|
+
data.tar.gz: d71c566236109548a142e35e456a5bd7a4443ceed4f3b1e1b8f09e81bd756124
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: d473c1d48115563b9f53917c52976878f2581dfe591b7d3f9b304bde01bf25cf65d2d04f2740143fd84aecfb95d04cf9736bbb3ab278b09d233f77c709d5f143
|
|
7
|
+
data.tar.gz: f4e484e612eb98dcb7d4b5e37027ff01c06e21da225ad96066e3cb59113a1ec658ea17edb2a2367b47c063750389b15b1313c6214775e562e56bacde35f1768f
|
data/README.md
ADDED
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
# CUZK::SOAP
|
|
2
|
+
|
|
3
|
+
Ruby client for the ČÚZK WSDP SOAP API (Czech Cadastral Registry / Český úřad zeměměřický a katastrální).
|
|
4
|
+
|
|
5
|
+
Wraps the [WSDP (Webové služby dálkového přístupu)](https://www.cuzk.gov.cz/Katastr-nemovitosti/Poskytovani-udaju-z-KN/Dalkovypristup/Webove-sluzby-dalkoveho-pristupu.aspx) v3.1 and v2.9 services with a Ruby-friendly interface, including cost tracking, batch processing, and circuit breaker resilience.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
Add to your Gemfile:
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
gem 'cuzk-soap'
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Then run `bundle install`. Requires Ruby >= 3.2.0.
|
|
16
|
+
|
|
17
|
+
## Configuration
|
|
18
|
+
|
|
19
|
+
### Global configuration
|
|
20
|
+
|
|
21
|
+
```ruby
|
|
22
|
+
CUZK::SOAP.configure do |c|
|
|
23
|
+
c.username = 'YOUR_WSDP_USERNAME'
|
|
24
|
+
c.password = 'YOUR_WSDP_PASSWORD'
|
|
25
|
+
c.environment = :production # :production or :testing
|
|
26
|
+
c.wsdp_version = :v31 # :v31 (default) or :v29
|
|
27
|
+
|
|
28
|
+
# Optional: cost management (CZK)
|
|
29
|
+
c.daily_budget_limit = 1000.0
|
|
30
|
+
c.monthly_budget_limit = 15_000.0
|
|
31
|
+
c.auto_pause_on_budget_exceeded = true
|
|
32
|
+
|
|
33
|
+
# Optional: timeouts
|
|
34
|
+
c.open_timeout = 30
|
|
35
|
+
c.read_timeout = 120
|
|
36
|
+
|
|
37
|
+
# Optional: SSL verification (set :none for trial environment)
|
|
38
|
+
c.ssl_verify_mode = :peer
|
|
39
|
+
end
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Per-client configuration
|
|
43
|
+
|
|
44
|
+
You can also pass credentials and options directly when creating a client. These override the global configuration:
|
|
45
|
+
|
|
46
|
+
```ruby
|
|
47
|
+
client = CUZK::SOAP::Client.new(
|
|
48
|
+
username: 'YOUR_WSDP_USERNAME',
|
|
49
|
+
password: 'YOUR_WSDP_PASSWORD',
|
|
50
|
+
environment: :testing,
|
|
51
|
+
daily_budget_limit: 500.0
|
|
52
|
+
)
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Usage
|
|
56
|
+
|
|
57
|
+
### Search operations (`vyhledat` service)
|
|
58
|
+
|
|
59
|
+
Search for parcels, buildings, units, and subjects in the cadastral registry.
|
|
60
|
+
|
|
61
|
+
```ruby
|
|
62
|
+
client = CUZK::SOAP::Client.new(
|
|
63
|
+
username: 'WSTEST', password: 'WSHESLO', environment: :testing
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
# Search parcel by cadastral unit code + parcel number
|
|
67
|
+
result = client.search_parcel(
|
|
68
|
+
cadastral_unit_code: '691232',
|
|
69
|
+
parcel_number: '68'
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
# Search parcel by internal ID
|
|
73
|
+
result = client.search_parcel(parcel_id: '12345')
|
|
74
|
+
|
|
75
|
+
# Search building by composite key
|
|
76
|
+
result = client.search_building(
|
|
77
|
+
district_part_code: '691232',
|
|
78
|
+
building_type_code: '1',
|
|
79
|
+
building_number: '50'
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
# Search building by internal ID
|
|
83
|
+
result = client.search_building(building_id: '999')
|
|
84
|
+
|
|
85
|
+
# Search unit
|
|
86
|
+
result = client.search_unit(
|
|
87
|
+
cadastral_unit_code: '691232',
|
|
88
|
+
unit_number: '10'
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
# Search subject (natural person)
|
|
92
|
+
result = client.search_subject(jmeno: 'Jan', prijmeni: 'Novak')
|
|
93
|
+
|
|
94
|
+
# Search subject (legal entity by IČO)
|
|
95
|
+
result = client.search_subject(ico: '12345678')
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Report generation (`sestavy` service)
|
|
99
|
+
|
|
100
|
+
Report generation is **asynchronous**. You request generation, poll for completion, then download.
|
|
101
|
+
|
|
102
|
+
```ruby
|
|
103
|
+
# 1. Request LV (list vlastnictví) generation
|
|
104
|
+
result = client.generate_lv(lv_id: '1234', format: 'pdf')
|
|
105
|
+
|
|
106
|
+
# 2. Poll for status
|
|
107
|
+
reports = client.list_reports
|
|
108
|
+
|
|
109
|
+
# 3. Download when ready
|
|
110
|
+
report = client.download_report(id_sestavy: '42')
|
|
111
|
+
|
|
112
|
+
# 4. Clean up
|
|
113
|
+
client.delete_report(id_sestavy: '42')
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Other report types:
|
|
117
|
+
|
|
118
|
+
```ruby
|
|
119
|
+
# Parcel info report
|
|
120
|
+
client.generate_parcel_info(
|
|
121
|
+
katastr_uzemi_kod: '691232',
|
|
122
|
+
kmenove_cislo: '68',
|
|
123
|
+
format: 'pdf'
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
# Building info report
|
|
127
|
+
client.generate_building_info(
|
|
128
|
+
stavba_id: '999',
|
|
129
|
+
format: 'pdf'
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
# Unit info report
|
|
133
|
+
client.generate_unit_info(jednotka_id: '555', format: 'pdf')
|
|
134
|
+
|
|
135
|
+
# Ownership overview
|
|
136
|
+
client.report_service.generuj_prehled_vlastnictvi(
|
|
137
|
+
opravneny_subjekt_id: '999',
|
|
138
|
+
format: 'pdf'
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
# Price data by property
|
|
142
|
+
client.report_service.generuj_cenove_udaje_dle_nemovitosti(
|
|
143
|
+
nemovitost_id: '123',
|
|
144
|
+
format: 'pdf'
|
|
145
|
+
)
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Supported formats: `pdf`, `xml`, `html`, `zip`.
|
|
149
|
+
|
|
150
|
+
### Codelist operations (`ciselnik` service)
|
|
151
|
+
|
|
152
|
+
```ruby
|
|
153
|
+
# List all cadastral units
|
|
154
|
+
units = client.list_cadastral_units
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### Subject info (`informace` service)
|
|
158
|
+
|
|
159
|
+
```ruby
|
|
160
|
+
# Read subject details by ID
|
|
161
|
+
subject = client.informace_service.cti_os(opravneny_subjekt_id: '999')
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### Account management (`ucet` service)
|
|
165
|
+
|
|
166
|
+
```ruby
|
|
167
|
+
# Check account status / connectivity
|
|
168
|
+
status = client.account_status
|
|
169
|
+
|
|
170
|
+
# Change password
|
|
171
|
+
client.change_password(
|
|
172
|
+
old_password: 'OLD_PASSWORD',
|
|
173
|
+
new_password: 'NEW_PASSWORD'
|
|
174
|
+
)
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### Batch processing
|
|
178
|
+
|
|
179
|
+
Execute multiple operations in sequence with progress tracking:
|
|
180
|
+
|
|
181
|
+
```ruby
|
|
182
|
+
operations = [
|
|
183
|
+
{ method: :search_parcel, params: { cadastral_unit_code: '691232', parcel_number: '68' } },
|
|
184
|
+
{ method: :search_parcel, params: { cadastral_unit_code: '691232', parcel_number: '69' } },
|
|
185
|
+
{ method: :generate_lv, params: { lv_id: '1234' } }
|
|
186
|
+
]
|
|
187
|
+
|
|
188
|
+
processor = client.batch_generate(operations) do |current, total, job_id|
|
|
189
|
+
puts "#{job_id}: #{current}/#{total}"
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
puts processor.summary
|
|
193
|
+
# => { job_id: "batch_...", status: :completed, total_operations: 3, successful: 3, failed: 0, progress: 100.0 }
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### Cost management
|
|
197
|
+
|
|
198
|
+
The built-in cost manager tracks spending against ČÚZK fee schedules:
|
|
199
|
+
|
|
200
|
+
```ruby
|
|
201
|
+
# Get cost estimate before running operations
|
|
202
|
+
estimate = client.cost_estimate(lv_extract_pdf: 5, property_info: 10)
|
|
203
|
+
# => 550.0 (CZK)
|
|
204
|
+
|
|
205
|
+
# Check remaining budget
|
|
206
|
+
status = client.service_status
|
|
207
|
+
status[:cost_manager][:daily_remaining] # => 450.0
|
|
208
|
+
status[:cost_manager][:monthly_remaining] # => 14450.0
|
|
209
|
+
status[:cost_manager][:budget_alert] # => false
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
When `auto_pause_on_budget_exceeded` is enabled, paid operations raise `CUZK::SOAP::CostLimitExceededError` if the estimated cost would exceed the daily or monthly budget.
|
|
213
|
+
|
|
214
|
+
### Direct service access
|
|
215
|
+
|
|
216
|
+
For operations not exposed through the `Client` convenience methods, access services directly. All Czech method names are available alongside English aliases:
|
|
217
|
+
|
|
218
|
+
```ruby
|
|
219
|
+
# Czech names
|
|
220
|
+
client.search_service.najdi_parcelu(katastr_uzemi_kod: '691232', kmenove_cislo: '68')
|
|
221
|
+
client.search_service.najdi_stavbu(stavba_id: '999')
|
|
222
|
+
client.search_service.najdi_os(ico: '12345678')
|
|
223
|
+
client.search_service.najdi_rizeni(...)
|
|
224
|
+
client.report_service.generuj_lv(lv_id: '1234')
|
|
225
|
+
|
|
226
|
+
# English aliases
|
|
227
|
+
client.search_service.search_parcel(...)
|
|
228
|
+
client.search_service.search_building(...)
|
|
229
|
+
client.search_service.search_subject(...)
|
|
230
|
+
client.search_service.search_proceedings(...)
|
|
231
|
+
client.report_service.generate_lv(...)
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
### Monitoring
|
|
235
|
+
|
|
236
|
+
```ruby
|
|
237
|
+
# Full service status with cost tracking
|
|
238
|
+
client.service_status
|
|
239
|
+
# => { environment: :testing, wsdp_version: :v31, connected: true, cost_manager: { ... } }
|
|
240
|
+
|
|
241
|
+
# WSDP version info with available services and endpoints
|
|
242
|
+
client.wsdp_version_info
|
|
243
|
+
# => { version: :v31, available_services: [:vyhledat, :sestavy, ...], wsdl_urls: { ... }, soap_endpoints: { ... } }
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
## WSDP Services
|
|
247
|
+
|
|
248
|
+
| Service | Description | Client accessor |
|
|
249
|
+
|-------------|------------------------------------|-------------------------|
|
|
250
|
+
| `vyhledat` | Search parcels, buildings, units, subjects | `client.search_service` |
|
|
251
|
+
| `sestavy` | Generate & manage reports (async) | `client.report_service` |
|
|
252
|
+
| `informace` | Read subject detail data | `client.informace_service` |
|
|
253
|
+
| `ciselnik` | Codelists (cadastral units, etc.) | `client.ciselnik_service` |
|
|
254
|
+
| `ucet` | Account status & password change | `client.account_service` |
|
|
255
|
+
|
|
256
|
+
## Error handling
|
|
257
|
+
|
|
258
|
+
```ruby
|
|
259
|
+
begin
|
|
260
|
+
client.search_parcel(cadastral_unit_code: '691232', parcel_number: '68')
|
|
261
|
+
rescue CUZK::SOAP::AuthenticationError => e
|
|
262
|
+
# Invalid credentials or expired session
|
|
263
|
+
rescue CUZK::SOAP::NetworkTimeoutError => e
|
|
264
|
+
# Connection timeout
|
|
265
|
+
rescue CUZK::SOAP::CostLimitExceededError => e
|
|
266
|
+
# Budget limit would be exceeded
|
|
267
|
+
rescue CUZK::SOAP::WSDPError => e
|
|
268
|
+
# WSDP service fault
|
|
269
|
+
e.fault_code # SOAP fault code
|
|
270
|
+
e.fault_string # SOAP fault description
|
|
271
|
+
rescue CUZK::SOAP::Error => e
|
|
272
|
+
# Base error class for all gem errors
|
|
273
|
+
end
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
## Trial environment
|
|
277
|
+
|
|
278
|
+
ČÚZK provides a trial environment for development and testing with pre-loaded test data (Prachatice district):
|
|
279
|
+
|
|
280
|
+
| Account | Username | Password |
|
|
281
|
+
|---------------|------------|------------|
|
|
282
|
+
| Standard | `WSTEST` | `WSHESLO` |
|
|
283
|
+
| Free | `WSTESTB` | `WSHESLOB` |
|
|
284
|
+
| Verification | `WSTESTO` | `WSHESLOO` |
|
|
285
|
+
| CzechPoint | `WSTESTC` | `WSHESLOC` |
|
|
286
|
+
|
|
287
|
+
```ruby
|
|
288
|
+
client = CUZK::SOAP::Client.new(
|
|
289
|
+
username: 'WSTEST',
|
|
290
|
+
password: 'WSHESLO',
|
|
291
|
+
environment: :testing,
|
|
292
|
+
ssl_verify_mode: :none # trial environment may have CRL issues
|
|
293
|
+
)
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
## Development
|
|
297
|
+
|
|
298
|
+
```bash
|
|
299
|
+
bin/setup # Install dependencies
|
|
300
|
+
bundle exec rspec spec/cuzk # Run unit tests (178 examples)
|
|
301
|
+
bundle exec rspec spec/integration # Run integration tests against VCR cassettes
|
|
302
|
+
bundle exec rspec spec/ # Run all tests (194 examples)
|
|
303
|
+
bundle exec rubocop # Run linter
|
|
304
|
+
|
|
305
|
+
# Re-record VCR cassettes against the live trial environment
|
|
306
|
+
VCR_RECORD=new_episodes bundle exec rspec spec/integration/
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
## License
|
|
310
|
+
|
|
311
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CUZK
|
|
4
|
+
module SOAP
|
|
5
|
+
module Authentication
|
|
6
|
+
class TestCredentials < WSDPCredentials
|
|
7
|
+
TEST_USERNAME = 'WSTEST'
|
|
8
|
+
TEST_PASSWORD = 'WSHESLO'
|
|
9
|
+
|
|
10
|
+
# Additional trial accounts
|
|
11
|
+
ACCOUNTS = {
|
|
12
|
+
standard: { username: 'WSTEST', password: 'WSHESLO' },
|
|
13
|
+
free: { username: 'WSTESTB', password: 'WSHESLOB' },
|
|
14
|
+
verification: { username: 'WSTESTO', password: 'WSHESLOO' },
|
|
15
|
+
czechpoint: { username: 'WSTESTC', password: 'WSHESLOC' }
|
|
16
|
+
}.freeze
|
|
17
|
+
|
|
18
|
+
def initialize(account: :standard)
|
|
19
|
+
creds = ACCOUNTS.fetch(account) do
|
|
20
|
+
raise ArgumentError, "Unknown test account: #{account}. Available: #{ACCOUNTS.keys.join(', ')}"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
super(
|
|
24
|
+
username: creds[:username],
|
|
25
|
+
password: creds[:password],
|
|
26
|
+
environment: :testing
|
|
27
|
+
)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'securerandom'
|
|
4
|
+
require 'digest'
|
|
5
|
+
require 'base64'
|
|
6
|
+
require 'time'
|
|
7
|
+
|
|
8
|
+
module CUZK
|
|
9
|
+
module SOAP
|
|
10
|
+
module Authentication
|
|
11
|
+
class WSDPCredentials
|
|
12
|
+
WSSE_NAMESPACE = 'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd'
|
|
13
|
+
WSU_NAMESPACE = 'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd'
|
|
14
|
+
PASSWORD_TEXT_TYPE =
|
|
15
|
+
'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordText'
|
|
16
|
+
|
|
17
|
+
attr_reader :username, :environment
|
|
18
|
+
|
|
19
|
+
def initialize(username:, password:, environment: :production)
|
|
20
|
+
@username = username
|
|
21
|
+
@password = password
|
|
22
|
+
@environment = environment
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Build the WS-Security header hash for Savon's :soap_header option.
|
|
26
|
+
# WSDP requires PasswordText over HTTPS (not digest).
|
|
27
|
+
def wsse_security_header
|
|
28
|
+
{
|
|
29
|
+
'wsse:Security' => {
|
|
30
|
+
'@xmlns:wsse' => WSSE_NAMESPACE,
|
|
31
|
+
'@xmlns:wsu' => WSU_NAMESPACE,
|
|
32
|
+
'@SOAP-ENV:mustUnderstand' => '1',
|
|
33
|
+
'wsse:UsernameToken' => {
|
|
34
|
+
'wsse:Username' => @username,
|
|
35
|
+
'wsse:Password' => {
|
|
36
|
+
'@Type' => PASSWORD_TEXT_TYPE,
|
|
37
|
+
:content! => @password
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Savon wsse_auth array: [username, password] for PasswordText
|
|
45
|
+
# Savon uses plaintext by default, :digest flag enables digest mode.
|
|
46
|
+
def savon_wsse_auth
|
|
47
|
+
[@username, @password]
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def validate_credentials
|
|
51
|
+
!@username.nil? && !@username.empty? &&
|
|
52
|
+
!@password.nil? && !@password.empty?
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CUZK
|
|
4
|
+
module SOAP
|
|
5
|
+
class BatchProcessor
|
|
6
|
+
attr_reader :job_id, :status, :operations, :results, :errors
|
|
7
|
+
|
|
8
|
+
def initialize(client, operations)
|
|
9
|
+
@client = client
|
|
10
|
+
@operations = operations
|
|
11
|
+
@job_id = generate_job_id
|
|
12
|
+
@status = :pending
|
|
13
|
+
@results = []
|
|
14
|
+
@errors = []
|
|
15
|
+
@progress_callbacks = []
|
|
16
|
+
@completion_callbacks = []
|
|
17
|
+
@error_callbacks = []
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def start
|
|
21
|
+
@status = :running
|
|
22
|
+
total = @operations.size
|
|
23
|
+
|
|
24
|
+
@operations.each_with_index do |operation, index|
|
|
25
|
+
break if @status == :cancelled
|
|
26
|
+
|
|
27
|
+
execute_operation(operation, index)
|
|
28
|
+
notify_progress(index + 1, total)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
@status = @errors.empty? ? :completed : :completed_with_errors
|
|
32
|
+
notify_completion
|
|
33
|
+
self
|
|
34
|
+
rescue StandardError => e
|
|
35
|
+
@status = :failed
|
|
36
|
+
raise BatchProcessingError, "Batch job #{@job_id} failed: #{e.message}"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def cancel
|
|
40
|
+
@status = :cancelled
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def progress_percentage
|
|
44
|
+
return 0 if @operations.empty?
|
|
45
|
+
|
|
46
|
+
completed = @results.size + @errors.size
|
|
47
|
+
(completed.to_f / @operations.size * 100).round(1)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def successful_count
|
|
51
|
+
@results.size
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def failed_count
|
|
55
|
+
@errors.size
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def on_progress(&callback)
|
|
59
|
+
@progress_callbacks << callback
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def on_completion(&callback)
|
|
63
|
+
@completion_callbacks << callback
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def on_error(&callback)
|
|
67
|
+
@error_callbacks << callback
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def summary
|
|
71
|
+
{
|
|
72
|
+
job_id: @job_id,
|
|
73
|
+
status: @status,
|
|
74
|
+
total_operations: @operations.size,
|
|
75
|
+
successful: successful_count,
|
|
76
|
+
failed: failed_count,
|
|
77
|
+
progress: progress_percentage
|
|
78
|
+
}
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
private
|
|
82
|
+
|
|
83
|
+
def execute_operation(operation, index)
|
|
84
|
+
method_name = operation[:method]
|
|
85
|
+
params = operation[:params] || {}
|
|
86
|
+
|
|
87
|
+
result = @client.public_send(method_name, **params)
|
|
88
|
+
@results << { index: index, operation: operation, result: result }
|
|
89
|
+
rescue Error => e
|
|
90
|
+
error_entry = { index: index, operation: operation, error: e }
|
|
91
|
+
@errors << error_entry
|
|
92
|
+
notify_error(error_entry)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def notify_progress(current, total)
|
|
96
|
+
@progress_callbacks.each { |cb| cb.call(current, total, @job_id) }
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def notify_completion
|
|
100
|
+
@completion_callbacks.each { |cb| cb.call(summary) }
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def notify_error(error_entry)
|
|
104
|
+
@error_callbacks.each { |cb| cb.call(error_entry) }
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def generate_job_id
|
|
108
|
+
"batch_#{Time.now.utc.strftime('%Y%m%d%H%M%S')}_#{SecureRandom.hex(4)}"
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CUZK
|
|
4
|
+
module SOAP
|
|
5
|
+
class ChangeTracker
|
|
6
|
+
attr_reader :tracked_properties, :last_check, :changes_detected
|
|
7
|
+
|
|
8
|
+
def initialize(client)
|
|
9
|
+
@client = client
|
|
10
|
+
@tracked_properties = {}
|
|
11
|
+
@last_check = nil
|
|
12
|
+
@changes_detected = []
|
|
13
|
+
@alert_callbacks = []
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def add_property(property_id, property_type:, monitoring_level: :standard, **metadata)
|
|
17
|
+
@tracked_properties[property_id] = {
|
|
18
|
+
property_type: property_type,
|
|
19
|
+
monitoring_level: monitoring_level,
|
|
20
|
+
added_at: Time.now.utc,
|
|
21
|
+
last_snapshot: nil,
|
|
22
|
+
metadata: metadata
|
|
23
|
+
}
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def remove_property(property_id)
|
|
27
|
+
@tracked_properties.delete(property_id)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def check_for_changes
|
|
31
|
+
changes = []
|
|
32
|
+
|
|
33
|
+
@tracked_properties.each do |property_id, tracking_info|
|
|
34
|
+
current_data = fetch_current_data(property_id, tracking_info)
|
|
35
|
+
previous_data = tracking_info[:last_snapshot]
|
|
36
|
+
|
|
37
|
+
if previous_data && current_data != previous_data
|
|
38
|
+
change = {
|
|
39
|
+
property_id: property_id,
|
|
40
|
+
property_type: tracking_info[:property_type],
|
|
41
|
+
detected_at: Time.now.utc,
|
|
42
|
+
previous_data: previous_data,
|
|
43
|
+
current_data: current_data
|
|
44
|
+
}
|
|
45
|
+
changes << change
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
tracking_info[:last_snapshot] = current_data
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
@last_check = Time.now.utc
|
|
52
|
+
@changes_detected.concat(changes)
|
|
53
|
+
trigger_alerts(changes) unless changes.empty?
|
|
54
|
+
changes
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def changes_since(date)
|
|
58
|
+
@changes_detected.select { |c| c[:detected_at] >= date }
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def change_history(property_id)
|
|
62
|
+
@changes_detected.select { |c| c[:property_id] == property_id }
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def on_change(&callback)
|
|
66
|
+
@alert_callbacks << callback
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def summary
|
|
70
|
+
{
|
|
71
|
+
tracked_count: @tracked_properties.size,
|
|
72
|
+
total_changes: @changes_detected.size,
|
|
73
|
+
last_check: @last_check
|
|
74
|
+
}
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
private
|
|
78
|
+
|
|
79
|
+
def fetch_current_data(property_id, tracking_info)
|
|
80
|
+
case tracking_info[:property_type]
|
|
81
|
+
when :parcel
|
|
82
|
+
parts = property_id.split('/')
|
|
83
|
+
@client.report_service.informace_o_parcele(
|
|
84
|
+
katastr_uzemi_kod: parts[0],
|
|
85
|
+
kmenove_cislo: parts[1],
|
|
86
|
+
format: 'xml'
|
|
87
|
+
)
|
|
88
|
+
when :building
|
|
89
|
+
parts = property_id.split('/')
|
|
90
|
+
@client.report_service.informace_o_budove(
|
|
91
|
+
katastr_uzemi_kod: parts[0],
|
|
92
|
+
cislo_budovy: parts[1],
|
|
93
|
+
format: 'xml'
|
|
94
|
+
)
|
|
95
|
+
when :unit
|
|
96
|
+
@client.report_service.informace_o_jednotce(
|
|
97
|
+
jednotka_id: property_id,
|
|
98
|
+
format: 'xml'
|
|
99
|
+
)
|
|
100
|
+
else
|
|
101
|
+
raise ArgumentError, "Unknown property type: #{tracking_info[:property_type]}"
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def trigger_alerts(changes)
|
|
106
|
+
@alert_callbacks.each { |cb| cb.call(changes) }
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|