ee_e_business_register 0.3.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/.ee_business_register_credentials.yml.example +8 -0
- data/.rubocop.yml +8 -0
- data/CHANGELOG.md +93 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/LICENSE.txt +21 -0
- data/Makefile +392 -0
- data/README.md +1294 -0
- data/Rakefile +12 -0
- data/lib/ee_e_business_register/client.rb +224 -0
- data/lib/ee_e_business_register/configuration.rb +160 -0
- data/lib/ee_e_business_register/errors.rb +98 -0
- data/lib/ee_e_business_register/models/classifier.rb +28 -0
- data/lib/ee_e_business_register/models/company.rb +2363 -0
- data/lib/ee_e_business_register/models/trust.rb +47 -0
- data/lib/ee_e_business_register/services/classifier_service.rb +176 -0
- data/lib/ee_e_business_register/services/company_service.rb +400 -0
- data/lib/ee_e_business_register/services/trusts_service.rb +136 -0
- data/lib/ee_e_business_register/types.rb +24 -0
- data/lib/ee_e_business_register/validation.rb +367 -0
- data/lib/ee_e_business_register/version.rb +5 -0
- data/lib/ee_e_business_register.rb +481 -0
- data/sig/ee_e_business_register.rbs +4 -0
- metadata +212 -0
|
@@ -0,0 +1,2363 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "dry-struct"
|
|
4
|
+
|
|
5
|
+
module EeEBusinessRegister
|
|
6
|
+
module Models
|
|
7
|
+
# Estonian business address information
|
|
8
|
+
#
|
|
9
|
+
# Represents a company address as stored in the Estonian e-Business Register.
|
|
10
|
+
# Estonian addresses use a structured format with counties, municipalities,
|
|
11
|
+
# and specific Estonian address system identifiers (ADS codes).
|
|
12
|
+
#
|
|
13
|
+
# The address model supports both human-readable address components and
|
|
14
|
+
# technical identifiers used by the Estonian address system for precise
|
|
15
|
+
# location identification.
|
|
16
|
+
#
|
|
17
|
+
# @example
|
|
18
|
+
# address = Address.new(
|
|
19
|
+
# street: "Narva mnt 7",
|
|
20
|
+
# city: "Tallinn",
|
|
21
|
+
# postal_code: "10117",
|
|
22
|
+
# county: "Harju maakond",
|
|
23
|
+
# full_address: "Narva mnt 7, 10117 Tallinn, Harju maakond"
|
|
24
|
+
# )
|
|
25
|
+
#
|
|
26
|
+
class Address < Dry::Struct
|
|
27
|
+
# @!attribute [r] street
|
|
28
|
+
# @return [String, nil] Street name and building number
|
|
29
|
+
attribute :street, Types::String.optional.default(nil)
|
|
30
|
+
|
|
31
|
+
# @!attribute [r] city
|
|
32
|
+
# @return [String, nil] City or settlement name
|
|
33
|
+
attribute :city, Types::String.optional.default(nil)
|
|
34
|
+
|
|
35
|
+
# @!attribute [r] postal_code
|
|
36
|
+
# @return [String, nil] Estonian postal code (5 digits)
|
|
37
|
+
attribute :postal_code, Types::String.optional.default(nil)
|
|
38
|
+
|
|
39
|
+
# @!attribute [r] county
|
|
40
|
+
# @return [String, nil] Estonian county (maakond) name
|
|
41
|
+
attribute :county, Types::String.optional.default(nil)
|
|
42
|
+
|
|
43
|
+
# @!attribute [r] country
|
|
44
|
+
# @return [String] Country name (defaults to "Estonia")
|
|
45
|
+
attribute :country, Types::String.default("Estonia")
|
|
46
|
+
|
|
47
|
+
# @!attribute [r] ads_oid
|
|
48
|
+
# @return [String, nil] Estonian Address Data System object identifier
|
|
49
|
+
attribute :ads_oid, Types::String.optional.default(nil)
|
|
50
|
+
|
|
51
|
+
# @!attribute [r] ads_adr_id
|
|
52
|
+
# @return [String, nil] Estonian Address Data System address identifier
|
|
53
|
+
attribute :ads_adr_id, Types::String.optional.default(nil)
|
|
54
|
+
|
|
55
|
+
# @!attribute [r] full_address
|
|
56
|
+
# @return [String, nil] Complete formatted address string
|
|
57
|
+
attribute :full_address, Types::String.optional.default(nil)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Estonian company information model
|
|
61
|
+
#
|
|
62
|
+
# Represents a company as registered in the Estonian e-Business Register.
|
|
63
|
+
# Contains all basic company information including identification, legal status,
|
|
64
|
+
# contact details, and business classification data.
|
|
65
|
+
#
|
|
66
|
+
# Estonian companies have unique 8-digit registry codes and follow specific
|
|
67
|
+
# legal forms (OÜ for private limited companies, AS for public companies, etc.).
|
|
68
|
+
# The status field uses single-letter codes: 'R' for active, 'K' for deleted,
|
|
69
|
+
# 'L' and 'N' for liquidation states, 'S' for reorganization.
|
|
70
|
+
#
|
|
71
|
+
# @example
|
|
72
|
+
# company = Company.new(
|
|
73
|
+
# name: "Sorbeet Payments OÜ",
|
|
74
|
+
# registry_code: "16863232",
|
|
75
|
+
# status: "R",
|
|
76
|
+
# legal_form: "OUE",
|
|
77
|
+
# legal_form_text: "Osaühing"
|
|
78
|
+
# )
|
|
79
|
+
# puts company.active? # => true
|
|
80
|
+
#
|
|
81
|
+
class Company < Dry::Struct
|
|
82
|
+
# @!attribute [r] name
|
|
83
|
+
# @return [String] Official company name as registered
|
|
84
|
+
attribute :name, Types::String
|
|
85
|
+
|
|
86
|
+
# @!attribute [r] registry_code
|
|
87
|
+
# @return [String] 8-digit Estonian company registry code (unique identifier)
|
|
88
|
+
attribute :registry_code, Types::String
|
|
89
|
+
|
|
90
|
+
# @!attribute [r] status
|
|
91
|
+
# @return [String] Single-letter status code (R=Active, K=Deleted, L/N=Liquidation, S=Reorganization)
|
|
92
|
+
attribute :status, Types::String
|
|
93
|
+
|
|
94
|
+
# @!attribute [r] status_text
|
|
95
|
+
# @return [String, nil] Human-readable status description
|
|
96
|
+
attribute :status_text, Types::String.optional.default(nil)
|
|
97
|
+
|
|
98
|
+
# @!attribute [r] legal_form
|
|
99
|
+
# @return [String, nil] Legal form code (OUE=OÜ, ASE=AS, MTU=MTÜ, etc.)
|
|
100
|
+
attribute :legal_form, Types::String.optional.default(nil)
|
|
101
|
+
|
|
102
|
+
# @!attribute [r] legal_form_text
|
|
103
|
+
# @return [String, nil] Human-readable legal form description
|
|
104
|
+
attribute :legal_form_text, Types::String.optional.default(nil)
|
|
105
|
+
|
|
106
|
+
# @!attribute [r] sub_type
|
|
107
|
+
# @return [String, nil] Company sub-type classification code
|
|
108
|
+
attribute :sub_type, Types::String.optional.default(nil)
|
|
109
|
+
|
|
110
|
+
# @!attribute [r] sub_type_text
|
|
111
|
+
# @return [String, nil] Human-readable sub-type description
|
|
112
|
+
attribute :sub_type_text, Types::String.optional.default(nil)
|
|
113
|
+
|
|
114
|
+
# @!attribute [r] registration_date
|
|
115
|
+
# @return [String, nil] Company registration date (YYYY-MM-DD format)
|
|
116
|
+
attribute :registration_date, Types::String.optional.default(nil)
|
|
117
|
+
|
|
118
|
+
# @!attribute [r] deletion_date
|
|
119
|
+
# @return [String, nil] Company deletion date if deleted (YYYY-MM-DD format)
|
|
120
|
+
attribute :deletion_date, Types::String.optional.default(nil)
|
|
121
|
+
|
|
122
|
+
# @!attribute [r] address
|
|
123
|
+
# @return [Address, nil] Company's registered address
|
|
124
|
+
attribute :address, Address.optional.default(nil)
|
|
125
|
+
|
|
126
|
+
# @!attribute [r] email
|
|
127
|
+
# @return [String, nil] Company's registered email address
|
|
128
|
+
attribute :email, Types::String.optional.default(nil)
|
|
129
|
+
|
|
130
|
+
# @!attribute [r] capital
|
|
131
|
+
# @return [Float, nil] Company's share capital amount
|
|
132
|
+
attribute :capital, Types::Coercible::Float.optional.default(nil)
|
|
133
|
+
|
|
134
|
+
# @!attribute [r] capital_currency
|
|
135
|
+
# @return [String, nil] Currency of the share capital (usually EUR)
|
|
136
|
+
attribute :capital_currency, Types::String.optional.default(nil)
|
|
137
|
+
|
|
138
|
+
# @!attribute [r] region
|
|
139
|
+
# @return [String, nil] Estonian region/county code
|
|
140
|
+
attribute :region, Types::String.optional.default(nil)
|
|
141
|
+
|
|
142
|
+
# @!attribute [r] region_text
|
|
143
|
+
# @return [String, nil] Human-readable region/county name
|
|
144
|
+
attribute :region_text, Types::String.optional.default(nil)
|
|
145
|
+
|
|
146
|
+
# Check if the company is currently active and operating
|
|
147
|
+
#
|
|
148
|
+
# Estonian companies with status 'R' (Registered) are considered active
|
|
149
|
+
# and are legally allowed to conduct business operations.
|
|
150
|
+
#
|
|
151
|
+
# @return [Boolean] true if company status is 'R' (active), false otherwise
|
|
152
|
+
# @example
|
|
153
|
+
# company.active? # => true for active companies
|
|
154
|
+
#
|
|
155
|
+
def active?
|
|
156
|
+
status == "R"
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Check if the company has been deleted/removed from the register
|
|
160
|
+
#
|
|
161
|
+
# Companies with status 'K' (Kustutatud) have been formally deleted
|
|
162
|
+
# from the Estonian Business Register and no longer exist legally.
|
|
163
|
+
#
|
|
164
|
+
# @return [Boolean] true if company status is 'K' (deleted), false otherwise
|
|
165
|
+
# @example
|
|
166
|
+
# company.deleted? # => true for deleted companies
|
|
167
|
+
#
|
|
168
|
+
def deleted?
|
|
169
|
+
status == "K"
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Check if the company is in liquidation process
|
|
173
|
+
#
|
|
174
|
+
# Companies can be in liquidation with status 'L' (Likvideerimisel) or
|
|
175
|
+
# 'N' (different liquidation state). These companies are winding down
|
|
176
|
+
# their operations but still exist legally until liquidation completes.
|
|
177
|
+
#
|
|
178
|
+
# @return [Boolean] true if company is in liquidation, false otherwise
|
|
179
|
+
# @example
|
|
180
|
+
# company.in_liquidation? # => true for companies being liquidated
|
|
181
|
+
#
|
|
182
|
+
def in_liquidation?
|
|
183
|
+
status == "L" || status == "N"
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# Check if the company is undergoing reorganization
|
|
187
|
+
#
|
|
188
|
+
# Companies with status 'S' (Sundlõpetamine) are undergoing forced
|
|
189
|
+
# reorganization or restructuring procedures, typically court-ordered.
|
|
190
|
+
#
|
|
191
|
+
# @return [Boolean] true if company status is 'S' (reorganization), false otherwise
|
|
192
|
+
# @example
|
|
193
|
+
# company.in_reorganization? # => true for companies in reorganization
|
|
194
|
+
#
|
|
195
|
+
def in_reorganization?
|
|
196
|
+
status == "S"
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# Detailed address information with full Estonian address system data
|
|
201
|
+
class DetailedAddress < Dry::Struct
|
|
202
|
+
attribute :record_id, Types::String.optional
|
|
203
|
+
attribute :card_region, Types::String.optional
|
|
204
|
+
attribute :card_number, Types::String.optional
|
|
205
|
+
attribute :card_type, Types::String.optional
|
|
206
|
+
attribute :entry_number, Types::String.optional
|
|
207
|
+
attribute :ehak_code, Types::String.optional
|
|
208
|
+
attribute :ehak_name, Types::String.optional
|
|
209
|
+
attribute :street_house_apartment, Types::String.optional
|
|
210
|
+
attribute :postal_code, Types::String.optional
|
|
211
|
+
attribute :ads_oid, Types::String.optional
|
|
212
|
+
attribute :ads_adr_id, Types::String.optional
|
|
213
|
+
attribute :full_normalized_address, Types::String.optional
|
|
214
|
+
attribute :start_date, Types::String.optional
|
|
215
|
+
attribute :end_date, Types::String.optional
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# Business activity information (EMTAK codes)
|
|
219
|
+
class BusinessActivity < Dry::Struct
|
|
220
|
+
attribute :record_id, Types::String.optional
|
|
221
|
+
attribute :emtak_code, Types::String.optional
|
|
222
|
+
attribute :emtak_text, Types::String.optional
|
|
223
|
+
attribute :emtak_version, Types::String.optional
|
|
224
|
+
attribute :nace_code, Types::String.optional
|
|
225
|
+
attribute :is_main_activity, Types::Bool.optional
|
|
226
|
+
attribute :start_date, Types::String.optional
|
|
227
|
+
attribute :end_date, Types::String.optional
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
# Contact method (phone, email, fax, etc.)
|
|
231
|
+
class ContactMethod < Dry::Struct
|
|
232
|
+
attribute :record_id, Types::String.optional
|
|
233
|
+
attribute :type, Types::String.optional
|
|
234
|
+
attribute :type_text, Types::String.optional
|
|
235
|
+
attribute :value, Types::String.optional
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
# Person associated with the company (board member, shareholder, etc.)
|
|
239
|
+
class Person < Dry::Struct
|
|
240
|
+
attribute :record_id, Types::String.optional
|
|
241
|
+
attribute :card_region, Types::String.optional
|
|
242
|
+
attribute :card_number, Types::String.optional
|
|
243
|
+
attribute :card_type, Types::String.optional
|
|
244
|
+
attribute :entry_number, Types::String.optional
|
|
245
|
+
attribute :person_type, Types::String.optional
|
|
246
|
+
attribute :role, Types::String.optional
|
|
247
|
+
attribute :role_text, Types::String.optional
|
|
248
|
+
attribute :first_name, Types::String.optional
|
|
249
|
+
attribute :name_business_name, Types::String.optional
|
|
250
|
+
attribute :personal_registry_code, Types::String.optional
|
|
251
|
+
attribute :address_country, Types::String.optional
|
|
252
|
+
attribute :address_country_text, Types::String.optional
|
|
253
|
+
attribute :address_street, Types::String.optional
|
|
254
|
+
attribute :start_date, Types::String.optional
|
|
255
|
+
attribute :end_date, Types::String.optional
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
# General company data (comprehensive information)
|
|
259
|
+
class GeneralData < Dry::Struct
|
|
260
|
+
attribute :first_registration_date, Types::String.optional
|
|
261
|
+
attribute :status, Types::String.optional
|
|
262
|
+
attribute :status_text, Types::String.optional
|
|
263
|
+
attribute :region, Types::String.optional
|
|
264
|
+
attribute :region_text, Types::String.optional
|
|
265
|
+
attribute :region_text_long, Types::String.optional
|
|
266
|
+
attribute :legal_form, Types::String.optional
|
|
267
|
+
attribute :legal_form_number, Types::String.optional
|
|
268
|
+
attribute :legal_form_text, Types::String.optional
|
|
269
|
+
attribute :legal_form_sub_type, Types::String.optional
|
|
270
|
+
attribute :legal_form_sub_type_text, Types::String.optional
|
|
271
|
+
attribute :addresses, Types::Array.of(DetailedAddress).default { [] }
|
|
272
|
+
attribute :business_activities, Types::Array.of(BusinessActivity).default { [] }
|
|
273
|
+
attribute :contact_methods, Types::Array.of(ContactMethod).default { [] }
|
|
274
|
+
attribute :accounting_obligation, Types::Bool.optional
|
|
275
|
+
attribute :founded_without_contribution, Types::Bool.optional
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
# Personnel data (people associated with the company)
|
|
279
|
+
class PersonnelData < Dry::Struct
|
|
280
|
+
attribute :registered_persons, Types::Array.of(Person).default { [] }
|
|
281
|
+
attribute :non_registered_persons, Types::Array.of(Person).default { [] }
|
|
282
|
+
attribute :building_society_members, Types::Array.of(Person).default { [] }
|
|
283
|
+
attribute :representation_rights, Types::Any.optional
|
|
284
|
+
attribute :special_representation_conditions, Types::Any.optional
|
|
285
|
+
attribute :pledges_and_transfers, Types::Any.optional
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
# Comprehensive detailed company information
|
|
289
|
+
class DetailedCompany < Dry::Struct
|
|
290
|
+
attribute :registry_code, Types::String
|
|
291
|
+
attribute :company_id, Types::String.optional
|
|
292
|
+
attribute :name, Types::String.optional
|
|
293
|
+
attribute :vat_number, Types::String.optional
|
|
294
|
+
attribute :general_data, GeneralData.optional
|
|
295
|
+
attribute :personnel_data, PersonnelData.optional
|
|
296
|
+
attribute :commercial_pledges, Types::Any.optional
|
|
297
|
+
attribute :applications, Types::Any.optional
|
|
298
|
+
attribute :rulings, Types::Any.optional
|
|
299
|
+
attribute :registry_cards, Types::Any.optional
|
|
300
|
+
|
|
301
|
+
def active?
|
|
302
|
+
general_data&.status == "R"
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
def deleted?
|
|
306
|
+
general_data&.status == "K"
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
def in_liquidation?
|
|
310
|
+
status = general_data&.status
|
|
311
|
+
status == "L" || status == "N"
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
def in_reorganization?
|
|
315
|
+
general_data&.status == "S"
|
|
316
|
+
end
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
# Company document (annual reports, articles of association)
|
|
320
|
+
class CompanyDocument < Dry::Struct
|
|
321
|
+
attribute :document_id, Types::String.optional
|
|
322
|
+
attribute :registry_code, Types::String.optional
|
|
323
|
+
attribute :document_type, Types::String.optional
|
|
324
|
+
attribute :name, Types::String.optional
|
|
325
|
+
attribute :size_bytes, Types::Integer.optional
|
|
326
|
+
attribute :status_date, Types::String.optional
|
|
327
|
+
attribute :validity, Types::String.optional
|
|
328
|
+
attribute :report_type, Types::String.optional
|
|
329
|
+
attribute :accounting_year, Types::String.optional
|
|
330
|
+
attribute :url, Types::String.optional
|
|
331
|
+
|
|
332
|
+
# Check if document is valid
|
|
333
|
+
# @return [Boolean] true if document has validity "K" (valid)
|
|
334
|
+
def valid?
|
|
335
|
+
validity == "K"
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
# Check if document is expired
|
|
339
|
+
# @return [Boolean] true if document has validity "A" (expired)
|
|
340
|
+
def expired?
|
|
341
|
+
validity == "A"
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
# Get human-readable document type description
|
|
345
|
+
# @return [String] Description of document type
|
|
346
|
+
def document_type_description
|
|
347
|
+
case document_type
|
|
348
|
+
when "A"
|
|
349
|
+
"Annual report (PDF)"
|
|
350
|
+
when "D"
|
|
351
|
+
"Annual report (DDOC/BDOC)"
|
|
352
|
+
when "X"
|
|
353
|
+
"Annual report (XBRL)"
|
|
354
|
+
when "P"
|
|
355
|
+
"Articles of association"
|
|
356
|
+
else
|
|
357
|
+
"Unknown document type"
|
|
358
|
+
end
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
# Get human-readable report type description
|
|
362
|
+
# @return [String] Description of report type
|
|
363
|
+
def report_type_description
|
|
364
|
+
case report_type
|
|
365
|
+
when "A"
|
|
366
|
+
"Annual report"
|
|
367
|
+
when "P"
|
|
368
|
+
"Final report"
|
|
369
|
+
when "L"
|
|
370
|
+
"Balance sheet prepared upon liquidation"
|
|
371
|
+
when "V"
|
|
372
|
+
"Interim report/balance sheet"
|
|
373
|
+
else
|
|
374
|
+
nil
|
|
375
|
+
end
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
# Check if this is an annual report
|
|
379
|
+
# @return [Boolean] true if document type is A, D, or X
|
|
380
|
+
def annual_report?
|
|
381
|
+
["A", "D", "X"].include?(document_type)
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
# Check if this is articles of association
|
|
385
|
+
# @return [Boolean] true if document type is P
|
|
386
|
+
def articles_of_association?
|
|
387
|
+
document_type == "P"
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
# Get file size in human-readable format
|
|
391
|
+
# @return [String] File size with appropriate unit
|
|
392
|
+
def human_readable_size
|
|
393
|
+
return "Unknown size" unless size_bytes
|
|
394
|
+
|
|
395
|
+
if size_bytes < 1024
|
|
396
|
+
"#{size_bytes} bytes"
|
|
397
|
+
elsif size_bytes < 1024 * 1024
|
|
398
|
+
"#{(size_bytes / 1024.0).round(1)} KB"
|
|
399
|
+
else
|
|
400
|
+
"#{(size_bytes / (1024.0 * 1024)).round(1)} MB"
|
|
401
|
+
end
|
|
402
|
+
end
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
# Annual report data entry
|
|
406
|
+
class ReportEntry < Dry::Struct
|
|
407
|
+
attribute :line_number, Types::String.optional.default(nil)
|
|
408
|
+
attribute :line_name, Types::String.optional.default(nil)
|
|
409
|
+
attribute :year_1_value, Types::String.optional.default(nil)
|
|
410
|
+
attribute :year_2_value, Types::String.optional.default(nil)
|
|
411
|
+
attribute :year_3_value, Types::String.optional.default(nil)
|
|
412
|
+
|
|
413
|
+
# Get the most recent year's value (usually year_1)
|
|
414
|
+
# @return [String, nil] Most recent value
|
|
415
|
+
def current_value
|
|
416
|
+
year_1_value
|
|
417
|
+
end
|
|
418
|
+
|
|
419
|
+
# Get previous year's value (usually year_2)
|
|
420
|
+
# @return [String, nil] Previous year value
|
|
421
|
+
def previous_value
|
|
422
|
+
year_2_value
|
|
423
|
+
end
|
|
424
|
+
|
|
425
|
+
# Check if entry has any values
|
|
426
|
+
# @return [Boolean] true if any year has a value
|
|
427
|
+
def has_values?
|
|
428
|
+
[year_1_value, year_2_value, year_3_value].any? { |v| v && !v.strip.empty? }
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
# Get numeric value for current year
|
|
432
|
+
# @return [Float, nil] Numeric value or nil if not numeric
|
|
433
|
+
def current_numeric_value
|
|
434
|
+
return nil unless year_1_value
|
|
435
|
+
Float(year_1_value.gsub(/[^\d.-]/, '')) rescue nil
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
# Get numeric value for previous year
|
|
439
|
+
# @return [Float, nil] Numeric value or nil if not numeric
|
|
440
|
+
def previous_numeric_value
|
|
441
|
+
return nil unless year_2_value
|
|
442
|
+
Float(year_2_value.gsub(/[^\d.-]/, '')) rescue nil
|
|
443
|
+
end
|
|
444
|
+
|
|
445
|
+
# Calculate year-over-year change
|
|
446
|
+
# @return [Float, nil] Percentage change or nil if calculation not possible
|
|
447
|
+
def year_over_year_change
|
|
448
|
+
current = current_numeric_value
|
|
449
|
+
previous = previous_numeric_value
|
|
450
|
+
return nil unless current && previous && previous != 0
|
|
451
|
+
|
|
452
|
+
((current - previous) / previous) * 100
|
|
453
|
+
end
|
|
454
|
+
end
|
|
455
|
+
|
|
456
|
+
# Annual report data structure
|
|
457
|
+
class AnnualReportData < Dry::Struct
|
|
458
|
+
attribute :registry_code, Types::String
|
|
459
|
+
attribute :report_type, Types::String
|
|
460
|
+
attribute :report_type_description, Types::String.optional.default(nil)
|
|
461
|
+
attribute :financial_year, Types::String.optional.default(nil)
|
|
462
|
+
attribute :entries, Types::Array.of(ReportEntry).default { [] }
|
|
463
|
+
attribute :report_date, Types::String.optional.default(nil)
|
|
464
|
+
attribute :currency, Types::String.optional.default(nil)
|
|
465
|
+
|
|
466
|
+
# Get specific report entry by line number
|
|
467
|
+
# @param line_number [String] Line number to find
|
|
468
|
+
# @return [ReportEntry, nil] Found entry or nil
|
|
469
|
+
def find_entry(line_number)
|
|
470
|
+
entries.find { |entry| entry.line_number == line_number.to_s }
|
|
471
|
+
end
|
|
472
|
+
|
|
473
|
+
# Get entries that match a pattern in line name
|
|
474
|
+
# @param pattern [Regexp, String] Pattern to match
|
|
475
|
+
# @return [Array<ReportEntry>] Matching entries
|
|
476
|
+
def find_entries_by_name(pattern)
|
|
477
|
+
regex = pattern.is_a?(Regexp) ? pattern : /#{Regexp.escape(pattern.to_s)}/i
|
|
478
|
+
entries.select { |entry| entry.line_name && entry.line_name.match?(regex) }
|
|
479
|
+
end
|
|
480
|
+
|
|
481
|
+
# Get all entries with values
|
|
482
|
+
# @return [Array<ReportEntry>] Entries that have at least one value
|
|
483
|
+
def entries_with_values
|
|
484
|
+
entries.select(&:has_values?)
|
|
485
|
+
end
|
|
486
|
+
|
|
487
|
+
# Check if this is a balance sheet report
|
|
488
|
+
# @return [Boolean] true for balance sheet report types
|
|
489
|
+
def balance_sheet?
|
|
490
|
+
["01"].include?(report_type)
|
|
491
|
+
end
|
|
492
|
+
|
|
493
|
+
# Check if this is an income statement report
|
|
494
|
+
# @return [Boolean] true for income statement report types
|
|
495
|
+
def income_statement?
|
|
496
|
+
["02"].include?(report_type)
|
|
497
|
+
end
|
|
498
|
+
|
|
499
|
+
# Check if this is a cash flow statement report
|
|
500
|
+
# @return [Boolean] true for cash flow statement report types
|
|
501
|
+
def cash_flow_statement?
|
|
502
|
+
["04"].include?(report_type)
|
|
503
|
+
end
|
|
504
|
+
|
|
505
|
+
# Get report type description based on code
|
|
506
|
+
# @return [String] Human-readable report type
|
|
507
|
+
def report_type_description
|
|
508
|
+
case report_type
|
|
509
|
+
when "01"
|
|
510
|
+
"Balance Sheet"
|
|
511
|
+
when "02"
|
|
512
|
+
"Income Statement"
|
|
513
|
+
when "03"
|
|
514
|
+
"Statement of Changes in Equity"
|
|
515
|
+
when "04"
|
|
516
|
+
"Cash Flow Statement"
|
|
517
|
+
when "05"
|
|
518
|
+
"Notes to Financial Statements"
|
|
519
|
+
when "06"
|
|
520
|
+
"Management Report"
|
|
521
|
+
when "07"
|
|
522
|
+
"Auditor's Report"
|
|
523
|
+
when "08"
|
|
524
|
+
"Profit Distribution Proposal"
|
|
525
|
+
when "09"
|
|
526
|
+
"Decision on Profit Distribution"
|
|
527
|
+
when "10"
|
|
528
|
+
"Consolidated Balance Sheet"
|
|
529
|
+
when "11"
|
|
530
|
+
"Consolidated Income Statement"
|
|
531
|
+
when "12"
|
|
532
|
+
"Consolidated Statement of Changes in Equity"
|
|
533
|
+
when "13"
|
|
534
|
+
"Consolidated Cash Flow Statement"
|
|
535
|
+
when "14"
|
|
536
|
+
"Notes to Consolidated Financial Statements"
|
|
537
|
+
when "15"
|
|
538
|
+
"Consolidated Management Report"
|
|
539
|
+
else
|
|
540
|
+
"Report Type #{report_type}"
|
|
541
|
+
end
|
|
542
|
+
end
|
|
543
|
+
end
|
|
544
|
+
|
|
545
|
+
# Person with representation rights
|
|
546
|
+
class RepresentationPerson < Dry::Struct
|
|
547
|
+
attribute :personal_code, Types::String.optional.default(nil)
|
|
548
|
+
attribute :personal_code_country, Types::String.optional.default(nil)
|
|
549
|
+
attribute :personal_code_country_text, Types::String.optional.default(nil)
|
|
550
|
+
attribute :birth_date, Types::String.optional.default(nil)
|
|
551
|
+
attribute :first_name, Types::String.optional.default(nil)
|
|
552
|
+
attribute :last_name, Types::String.optional.default(nil)
|
|
553
|
+
attribute :role, Types::String.optional.default(nil)
|
|
554
|
+
attribute :role_text, Types::String.optional.default(nil)
|
|
555
|
+
attribute :exclusive_representation_rights, Types::String.optional.default(nil)
|
|
556
|
+
attribute :representation_exceptions, Types::String.optional.default(nil)
|
|
557
|
+
|
|
558
|
+
# Get person's full name
|
|
559
|
+
# @return [String] Combined first and last name
|
|
560
|
+
def full_name
|
|
561
|
+
[first_name, last_name].compact.join(" ")
|
|
562
|
+
end
|
|
563
|
+
|
|
564
|
+
# Check if person has exclusive representation rights
|
|
565
|
+
# @return [Boolean] true if person has exclusive representation rights
|
|
566
|
+
def has_exclusive_representation_rights?
|
|
567
|
+
exclusive_representation_rights == "JAH"
|
|
568
|
+
end
|
|
569
|
+
|
|
570
|
+
# Check if person is Estonian citizen
|
|
571
|
+
# @return [Boolean] true if personal code country is EST
|
|
572
|
+
def estonian_citizen?
|
|
573
|
+
personal_code_country == "EST"
|
|
574
|
+
end
|
|
575
|
+
|
|
576
|
+
# Check if person is a natural person (has personal code)
|
|
577
|
+
# @return [Boolean] true if personal code is present
|
|
578
|
+
def natural_person?
|
|
579
|
+
!personal_code.nil? && !personal_code.empty?
|
|
580
|
+
end
|
|
581
|
+
|
|
582
|
+
# Check if person is a legal entity (no personal code or institutional code)
|
|
583
|
+
# @return [Boolean] true if appears to be a legal entity
|
|
584
|
+
def legal_entity?
|
|
585
|
+
!natural_person? || personal_code_country == "XXX"
|
|
586
|
+
end
|
|
587
|
+
|
|
588
|
+
# Get role type category
|
|
589
|
+
# @return [Symbol] Role category
|
|
590
|
+
def role_category
|
|
591
|
+
case role
|
|
592
|
+
when "ASES"
|
|
593
|
+
:authorized_representative
|
|
594
|
+
when "KOAS"
|
|
595
|
+
:higher_authority
|
|
596
|
+
when "JJUH", "JJLI"
|
|
597
|
+
:board_member
|
|
598
|
+
when "OSAS", "OSLI"
|
|
599
|
+
:shareholder
|
|
600
|
+
when "VANM"
|
|
601
|
+
:parent_company
|
|
602
|
+
else
|
|
603
|
+
:other
|
|
604
|
+
end
|
|
605
|
+
end
|
|
606
|
+
|
|
607
|
+
# Check if person is a board member
|
|
608
|
+
# @return [Boolean] true if role indicates board membership
|
|
609
|
+
def board_member?
|
|
610
|
+
role_category == :board_member
|
|
611
|
+
end
|
|
612
|
+
|
|
613
|
+
# Check if person is a shareholder
|
|
614
|
+
# @return [Boolean] true if role indicates shareholding
|
|
615
|
+
def shareholder?
|
|
616
|
+
role_category == :shareholder
|
|
617
|
+
end
|
|
618
|
+
|
|
619
|
+
# Check if person is an authorized representative
|
|
620
|
+
# @return [Boolean] true if role indicates authorized representation
|
|
621
|
+
def authorized_representative?
|
|
622
|
+
role_category == :authorized_representative
|
|
623
|
+
end
|
|
624
|
+
end
|
|
625
|
+
|
|
626
|
+
# Company representation rights information
|
|
627
|
+
class CompanyRepresentation < Dry::Struct
|
|
628
|
+
attribute :registry_code, Types::String
|
|
629
|
+
attribute :business_name, Types::String.optional.default(nil)
|
|
630
|
+
attribute :status, Types::String.optional.default(nil)
|
|
631
|
+
attribute :status_text, Types::String.optional.default(nil)
|
|
632
|
+
attribute :legal_form, Types::String.optional.default(nil)
|
|
633
|
+
attribute :legal_form_text, Types::String.optional.default(nil)
|
|
634
|
+
attribute :representation_exceptions, Types::String.optional.default(nil)
|
|
635
|
+
attribute :persons, Types::Array.of(RepresentationPerson).default { [] }
|
|
636
|
+
|
|
637
|
+
# Check if company is active
|
|
638
|
+
# @return [Boolean] true if company status is active
|
|
639
|
+
def active?
|
|
640
|
+
status == "R"
|
|
641
|
+
end
|
|
642
|
+
|
|
643
|
+
# Get all persons with exclusive representation rights
|
|
644
|
+
# @return [Array<RepresentationPerson>] Persons with exclusive rights
|
|
645
|
+
def persons_with_exclusive_rights
|
|
646
|
+
persons.select(&:has_exclusive_representation_rights?)
|
|
647
|
+
end
|
|
648
|
+
|
|
649
|
+
# Get all board members
|
|
650
|
+
# @return [Array<RepresentationPerson>] Board members
|
|
651
|
+
def board_members
|
|
652
|
+
persons.select(&:board_member?)
|
|
653
|
+
end
|
|
654
|
+
|
|
655
|
+
# Get all shareholders
|
|
656
|
+
# @return [Array<RepresentationPerson>] Shareholders
|
|
657
|
+
def shareholders
|
|
658
|
+
persons.select(&:shareholder?)
|
|
659
|
+
end
|
|
660
|
+
|
|
661
|
+
# Get all authorized representatives
|
|
662
|
+
# @return [Array<RepresentationPerson>] Authorized representatives
|
|
663
|
+
def authorized_representatives
|
|
664
|
+
persons.select(&:authorized_representative?)
|
|
665
|
+
end
|
|
666
|
+
|
|
667
|
+
# Get all natural persons (individuals)
|
|
668
|
+
# @return [Array<RepresentationPerson>] Natural persons
|
|
669
|
+
def natural_persons
|
|
670
|
+
persons.select(&:natural_person?)
|
|
671
|
+
end
|
|
672
|
+
|
|
673
|
+
# Get all legal entities
|
|
674
|
+
# @return [Array<RepresentationPerson>] Legal entities
|
|
675
|
+
def legal_entities
|
|
676
|
+
persons.select(&:legal_entity?)
|
|
677
|
+
end
|
|
678
|
+
|
|
679
|
+
# Get all Estonian citizens
|
|
680
|
+
# @return [Array<RepresentationPerson>] Estonian citizens
|
|
681
|
+
def estonian_citizens
|
|
682
|
+
persons.select(&:estonian_citizen?)
|
|
683
|
+
end
|
|
684
|
+
|
|
685
|
+
# Check if there are any representation exceptions or limitations
|
|
686
|
+
# @return [Boolean] true if representation exceptions exist
|
|
687
|
+
def has_representation_exceptions?
|
|
688
|
+
!representation_exceptions.nil? && !representation_exceptions.empty?
|
|
689
|
+
end
|
|
690
|
+
|
|
691
|
+
# Get persons by role
|
|
692
|
+
# @param role_code [String] Role code (e.g., "ASES", "JJUH")
|
|
693
|
+
# @return [Array<RepresentationPerson>] Persons with specified role
|
|
694
|
+
def persons_by_role(role_code)
|
|
695
|
+
persons.select { |person| person.role == role_code }
|
|
696
|
+
end
|
|
697
|
+
|
|
698
|
+
# Find person by personal code
|
|
699
|
+
# @param personal_code [String] Personal identification code
|
|
700
|
+
# @return [RepresentationPerson, nil] Found person or nil
|
|
701
|
+
def find_person_by_code(personal_code)
|
|
702
|
+
persons.find { |person| person.personal_code == personal_code }
|
|
703
|
+
end
|
|
704
|
+
|
|
705
|
+
# Find persons by name (case-insensitive partial match)
|
|
706
|
+
# @param name [String] Name to search for
|
|
707
|
+
# @return [Array<RepresentationPerson>] Matching persons
|
|
708
|
+
def find_persons_by_name(name)
|
|
709
|
+
return [] if name.nil? || name.empty?
|
|
710
|
+
|
|
711
|
+
search_term = name.downcase
|
|
712
|
+
persons.select do |person|
|
|
713
|
+
full_name_lower = person.full_name.downcase
|
|
714
|
+
person.first_name&.downcase&.include?(search_term) ||
|
|
715
|
+
person.last_name&.downcase&.include?(search_term) ||
|
|
716
|
+
full_name_lower.include?(search_term)
|
|
717
|
+
end
|
|
718
|
+
end
|
|
719
|
+
|
|
720
|
+
# Get representation structure summary
|
|
721
|
+
# @return [Hash] Summary of representation structure
|
|
722
|
+
def representation_summary
|
|
723
|
+
{
|
|
724
|
+
total_persons: persons.size,
|
|
725
|
+
natural_persons: natural_persons.size,
|
|
726
|
+
legal_entities: legal_entities.size,
|
|
727
|
+
board_members: board_members.size,
|
|
728
|
+
shareholders: shareholders.size,
|
|
729
|
+
authorized_representatives: authorized_representatives.size,
|
|
730
|
+
persons_with_exclusive_rights: persons_with_exclusive_rights.size,
|
|
731
|
+
estonian_citizens: estonian_citizens.size,
|
|
732
|
+
has_exceptions: has_representation_exceptions?
|
|
733
|
+
}
|
|
734
|
+
end
|
|
735
|
+
end
|
|
736
|
+
|
|
737
|
+
# Individual person change record
|
|
738
|
+
class PersonChange < Dry::Struct
|
|
739
|
+
attribute :person_role, Types::String.optional.default(nil)
|
|
740
|
+
attribute :person_role_text, Types::String.optional.default(nil)
|
|
741
|
+
attribute :change_type, Types::String.optional.default(nil)
|
|
742
|
+
attribute :change_time, Types::String.optional.default(nil)
|
|
743
|
+
attribute :old_person_id, Types::String.optional.default(nil)
|
|
744
|
+
attribute :new_person_id, Types::String.optional.default(nil)
|
|
745
|
+
|
|
746
|
+
# Get human-readable change type description
|
|
747
|
+
# @return [String] Description of the change type
|
|
748
|
+
def change_type_description
|
|
749
|
+
case change_type&.to_s
|
|
750
|
+
when "1"
|
|
751
|
+
"Person Added"
|
|
752
|
+
when "2"
|
|
753
|
+
"Person Removed"
|
|
754
|
+
when "3"
|
|
755
|
+
"Person Data Changed"
|
|
756
|
+
else
|
|
757
|
+
"Unknown Change Type"
|
|
758
|
+
end
|
|
759
|
+
end
|
|
760
|
+
|
|
761
|
+
# Check if this is an addition change
|
|
762
|
+
# @return [Boolean] true if change type is 1 (added)
|
|
763
|
+
def addition?
|
|
764
|
+
change_type&.to_s == "1"
|
|
765
|
+
end
|
|
766
|
+
|
|
767
|
+
# Check if this is a removal change
|
|
768
|
+
# @return [Boolean] true if change type is 2 (removed)
|
|
769
|
+
def removal?
|
|
770
|
+
change_type&.to_s == "2"
|
|
771
|
+
end
|
|
772
|
+
|
|
773
|
+
# Check if this is a data change
|
|
774
|
+
# @return [Boolean] true if change type is 3 (changed)
|
|
775
|
+
def data_change?
|
|
776
|
+
change_type&.to_s == "3"
|
|
777
|
+
end
|
|
778
|
+
|
|
779
|
+
# Check if this involves a beneficial owner
|
|
780
|
+
# @return [Boolean] true if person role is "W"
|
|
781
|
+
def beneficial_owner_change?
|
|
782
|
+
person_role == "W"
|
|
783
|
+
end
|
|
784
|
+
|
|
785
|
+
# Get the relevant person ID based on change type
|
|
786
|
+
# @return [String, nil] Person ID that's most relevant for this change
|
|
787
|
+
def relevant_person_id
|
|
788
|
+
case change_type&.to_s
|
|
789
|
+
when "1" # Addition - use new ID
|
|
790
|
+
new_person_id
|
|
791
|
+
when "2" # Removal - use old ID
|
|
792
|
+
old_person_id
|
|
793
|
+
when "3" # Change - prefer new ID, fallback to old
|
|
794
|
+
new_person_id || old_person_id
|
|
795
|
+
else
|
|
796
|
+
new_person_id || old_person_id
|
|
797
|
+
end
|
|
798
|
+
end
|
|
799
|
+
|
|
800
|
+
# Parse change time to DateTime if available
|
|
801
|
+
# @return [DateTime, nil] Parsed change time or nil
|
|
802
|
+
def change_time_parsed
|
|
803
|
+
return nil unless change_time
|
|
804
|
+
DateTime.parse(change_time) rescue nil
|
|
805
|
+
end
|
|
806
|
+
|
|
807
|
+
# Get change severity level for monitoring
|
|
808
|
+
# @return [Symbol] Severity level (:high, :medium, :low)
|
|
809
|
+
def change_severity
|
|
810
|
+
if beneficial_owner_change?
|
|
811
|
+
case change_type&.to_s
|
|
812
|
+
when "1", "2" # Addition or removal of beneficial owner
|
|
813
|
+
:high
|
|
814
|
+
when "3" # Change to beneficial owner data
|
|
815
|
+
:medium
|
|
816
|
+
else
|
|
817
|
+
:low
|
|
818
|
+
end
|
|
819
|
+
else
|
|
820
|
+
:low
|
|
821
|
+
end
|
|
822
|
+
end
|
|
823
|
+
end
|
|
824
|
+
|
|
825
|
+
# Company with person changes
|
|
826
|
+
class CompanyWithChanges < Dry::Struct
|
|
827
|
+
attribute :registry_code, Types::String
|
|
828
|
+
attribute :business_name, Types::String.optional.default(nil)
|
|
829
|
+
attribute :legal_form, Types::String.optional.default(nil)
|
|
830
|
+
attribute :legal_form_text, Types::String.optional.default(nil)
|
|
831
|
+
attribute :status, Types::String.optional.default(nil)
|
|
832
|
+
attribute :status_text, Types::String.optional.default(nil)
|
|
833
|
+
attribute :person_changes, Types::Array.of(PersonChange).default { [] }
|
|
834
|
+
|
|
835
|
+
# Check if company is active
|
|
836
|
+
# @return [Boolean] true if company status is active
|
|
837
|
+
def active?
|
|
838
|
+
status == "R"
|
|
839
|
+
end
|
|
840
|
+
|
|
841
|
+
# Get all addition changes
|
|
842
|
+
# @return [Array<PersonChange>] Changes where persons were added
|
|
843
|
+
def additions
|
|
844
|
+
person_changes.select(&:addition?)
|
|
845
|
+
end
|
|
846
|
+
|
|
847
|
+
# Get all removal changes
|
|
848
|
+
# @return [Array<PersonChange>] Changes where persons were removed
|
|
849
|
+
def removals
|
|
850
|
+
person_changes.select(&:removal?)
|
|
851
|
+
end
|
|
852
|
+
|
|
853
|
+
# Get all data changes
|
|
854
|
+
# @return [Array<PersonChange>] Changes where person data was modified
|
|
855
|
+
def data_changes
|
|
856
|
+
person_changes.select(&:data_change?)
|
|
857
|
+
end
|
|
858
|
+
|
|
859
|
+
# Get all beneficial owner changes
|
|
860
|
+
# @return [Array<PersonChange>] Changes involving beneficial owners
|
|
861
|
+
def beneficial_owner_changes
|
|
862
|
+
person_changes.select(&:beneficial_owner_change?)
|
|
863
|
+
end
|
|
864
|
+
|
|
865
|
+
# Get changes by severity level
|
|
866
|
+
# @param level [Symbol] Severity level (:high, :medium, :low)
|
|
867
|
+
# @return [Array<PersonChange>] Changes matching the severity level
|
|
868
|
+
def changes_by_severity(level)
|
|
869
|
+
person_changes.select { |change| change.change_severity == level }
|
|
870
|
+
end
|
|
871
|
+
|
|
872
|
+
# Get high-priority changes (beneficial owner additions/removals)
|
|
873
|
+
# @return [Array<PersonChange>] High-priority changes
|
|
874
|
+
def high_priority_changes
|
|
875
|
+
changes_by_severity(:high)
|
|
876
|
+
end
|
|
877
|
+
|
|
878
|
+
# Check if there are any high-priority changes
|
|
879
|
+
# @return [Boolean] true if any high-priority changes exist
|
|
880
|
+
def has_high_priority_changes?
|
|
881
|
+
high_priority_changes.any?
|
|
882
|
+
end
|
|
883
|
+
|
|
884
|
+
# Get changes by person role
|
|
885
|
+
# @param role [String] Person role code (e.g., "W", "JJUH")
|
|
886
|
+
# @return [Array<PersonChange>] Changes for the specified role
|
|
887
|
+
def changes_by_role(role)
|
|
888
|
+
person_changes.select { |change| change.person_role == role }
|
|
889
|
+
end
|
|
890
|
+
|
|
891
|
+
# Get unique person roles that had changes
|
|
892
|
+
# @return [Array<String>] Unique person role codes
|
|
893
|
+
def changed_roles
|
|
894
|
+
person_changes.map(&:person_role).compact.uniq
|
|
895
|
+
end
|
|
896
|
+
|
|
897
|
+
# Get changes within a time range
|
|
898
|
+
# @param start_time [DateTime, String] Start of time range
|
|
899
|
+
# @param end_time [DateTime, String] End of time range
|
|
900
|
+
# @return [Array<PersonChange>] Changes within the time range
|
|
901
|
+
def changes_in_time_range(start_time, end_time)
|
|
902
|
+
start_dt = start_time.is_a?(String) ? DateTime.parse(start_time) : start_time
|
|
903
|
+
end_dt = end_time.is_a?(String) ? DateTime.parse(end_time) : end_time
|
|
904
|
+
|
|
905
|
+
person_changes.select do |change|
|
|
906
|
+
change_dt = change.change_time_parsed
|
|
907
|
+
change_dt && change_dt >= start_dt && change_dt <= end_dt
|
|
908
|
+
end
|
|
909
|
+
rescue ArgumentError
|
|
910
|
+
[] # Return empty array if date parsing fails
|
|
911
|
+
end
|
|
912
|
+
|
|
913
|
+
# Get summary of changes
|
|
914
|
+
# @return [Hash] Summary statistics
|
|
915
|
+
def changes_summary
|
|
916
|
+
{
|
|
917
|
+
total_changes: person_changes.size,
|
|
918
|
+
additions: additions.size,
|
|
919
|
+
removals: removals.size,
|
|
920
|
+
data_changes: data_changes.size,
|
|
921
|
+
beneficial_owner_changes: beneficial_owner_changes.size,
|
|
922
|
+
high_priority_changes: high_priority_changes.size,
|
|
923
|
+
changed_roles: changed_roles.size,
|
|
924
|
+
roles_affected: changed_roles
|
|
925
|
+
}
|
|
926
|
+
end
|
|
927
|
+
end
|
|
928
|
+
|
|
929
|
+
# Person changes query results with pagination
|
|
930
|
+
class PersonChanges < Dry::Struct
|
|
931
|
+
attribute :companies, Types::Array.of(CompanyWithChanges).default { [] }
|
|
932
|
+
attribute :total_results, Types::Integer.default(0)
|
|
933
|
+
attribute :page, Types::Integer.default(1)
|
|
934
|
+
attribute :results_per_page, Types::Integer.default(100)
|
|
935
|
+
attribute :change_date, Types::String.optional.default(nil)
|
|
936
|
+
attribute :person_roles_searched, Types::Array.of(Types::String).default { [] }
|
|
937
|
+
attribute :change_types_searched, Types::Array.of(Types::String).default { [] }
|
|
938
|
+
|
|
939
|
+
# Check if there are more pages available
|
|
940
|
+
# @return [Boolean] true if more pages exist
|
|
941
|
+
def has_more_pages?
|
|
942
|
+
total_companies_pages > page
|
|
943
|
+
end
|
|
944
|
+
|
|
945
|
+
# Calculate total number of pages
|
|
946
|
+
# @return [Integer] Total pages available
|
|
947
|
+
def total_companies_pages
|
|
948
|
+
return 1 if results_per_page <= 0
|
|
949
|
+
(total_results.to_f / results_per_page).ceil
|
|
950
|
+
end
|
|
951
|
+
|
|
952
|
+
# Get next page number
|
|
953
|
+
# @return [Integer, nil] Next page number or nil if on last page
|
|
954
|
+
def next_page
|
|
955
|
+
has_more_pages? ? page + 1 : nil
|
|
956
|
+
end
|
|
957
|
+
|
|
958
|
+
# Get previous page number
|
|
959
|
+
# @return [Integer, nil] Previous page number or nil if on first page
|
|
960
|
+
def previous_page
|
|
961
|
+
page > 1 ? page - 1 : nil
|
|
962
|
+
end
|
|
963
|
+
|
|
964
|
+
# Check if this is the first page
|
|
965
|
+
# @return [Boolean] true if on page 1
|
|
966
|
+
def first_page?
|
|
967
|
+
page == 1
|
|
968
|
+
end
|
|
969
|
+
|
|
970
|
+
# Check if this is the last page
|
|
971
|
+
# @return [Boolean] true if on the last page
|
|
972
|
+
def last_page?
|
|
973
|
+
page >= total_companies_pages
|
|
974
|
+
end
|
|
975
|
+
|
|
976
|
+
# Get all companies with high-priority changes
|
|
977
|
+
# @return [Array<CompanyWithChanges>] Companies with high-priority changes
|
|
978
|
+
def companies_with_high_priority_changes
|
|
979
|
+
companies.select(&:has_high_priority_changes?)
|
|
980
|
+
end
|
|
981
|
+
|
|
982
|
+
# Get all companies with beneficial owner changes
|
|
983
|
+
# @return [Array<CompanyWithChanges>] Companies with beneficial owner changes
|
|
984
|
+
def companies_with_beneficial_owner_changes
|
|
985
|
+
companies.select { |company| company.beneficial_owner_changes.any? }
|
|
986
|
+
end
|
|
987
|
+
|
|
988
|
+
# Get companies by change type
|
|
989
|
+
# @param change_type [String, Integer] Change type to filter by
|
|
990
|
+
# @return [Array<CompanyWithChanges>] Companies with the specified change type
|
|
991
|
+
def companies_by_change_type(change_type)
|
|
992
|
+
type_str = change_type.to_s
|
|
993
|
+
companies.select do |company|
|
|
994
|
+
company.person_changes.any? { |change| change.change_type == type_str }
|
|
995
|
+
end
|
|
996
|
+
end
|
|
997
|
+
|
|
998
|
+
# Get all addition changes across all companies
|
|
999
|
+
# @return [Array<PersonChange>] All addition changes
|
|
1000
|
+
def all_additions
|
|
1001
|
+
companies.flat_map(&:additions)
|
|
1002
|
+
end
|
|
1003
|
+
|
|
1004
|
+
# Get all removal changes across all companies
|
|
1005
|
+
# @return [Array<PersonChange>] All removal changes
|
|
1006
|
+
def all_removals
|
|
1007
|
+
companies.flat_map(&:removals)
|
|
1008
|
+
end
|
|
1009
|
+
|
|
1010
|
+
# Get all data changes across all companies
|
|
1011
|
+
# @return [Array<PersonChange>] All data changes
|
|
1012
|
+
def all_data_changes
|
|
1013
|
+
companies.flat_map(&:data_changes)
|
|
1014
|
+
end
|
|
1015
|
+
|
|
1016
|
+
# Get overall summary across all companies
|
|
1017
|
+
# @return [Hash] Overall summary statistics
|
|
1018
|
+
def overall_summary
|
|
1019
|
+
total_changes = companies.sum { |company| company.person_changes.size }
|
|
1020
|
+
|
|
1021
|
+
{
|
|
1022
|
+
companies_with_changes: companies.size,
|
|
1023
|
+
total_changes: total_changes,
|
|
1024
|
+
additions: all_additions.size,
|
|
1025
|
+
removals: all_removals.size,
|
|
1026
|
+
data_changes: all_data_changes.size,
|
|
1027
|
+
beneficial_owner_changes: companies.sum { |c| c.beneficial_owner_changes.size },
|
|
1028
|
+
high_priority_changes: companies.sum { |c| c.high_priority_changes.size },
|
|
1029
|
+
companies_with_high_priority: companies_with_high_priority_changes.size,
|
|
1030
|
+
page_info: {
|
|
1031
|
+
current_page: page,
|
|
1032
|
+
total_pages: total_companies_pages,
|
|
1033
|
+
results_per_page: results_per_page,
|
|
1034
|
+
total_results: total_results
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
end
|
|
1038
|
+
|
|
1039
|
+
# Get changes for a specific date range (if change times are available)
|
|
1040
|
+
# @param start_time [DateTime, String] Start of time range
|
|
1041
|
+
# @param end_time [DateTime, String] End of time range
|
|
1042
|
+
# @return [Array<PersonChange>] All changes within the time range
|
|
1043
|
+
def changes_in_time_range(start_time, end_time)
|
|
1044
|
+
companies.flat_map { |company| company.changes_in_time_range(start_time, end_time) }
|
|
1045
|
+
end
|
|
1046
|
+
|
|
1047
|
+
# Find company by registry code
|
|
1048
|
+
# @param registry_code [String] Company registry code
|
|
1049
|
+
# @return [CompanyWithChanges, nil] Found company or nil
|
|
1050
|
+
def find_company(registry_code)
|
|
1051
|
+
companies.find { |company| company.registry_code == registry_code }
|
|
1052
|
+
end
|
|
1053
|
+
|
|
1054
|
+
# Get unique person roles that had changes across all companies
|
|
1055
|
+
# @return [Array<String>] Unique person role codes
|
|
1056
|
+
def all_changed_roles
|
|
1057
|
+
companies.flat_map(&:changed_roles).uniq
|
|
1058
|
+
end
|
|
1059
|
+
end
|
|
1060
|
+
|
|
1061
|
+
# Represents a beneficial owner of a company.
|
|
1062
|
+
#
|
|
1063
|
+
# Beneficial owners are natural persons who ultimately own or control
|
|
1064
|
+
# a company through various means (shareholding, voting rights, management).
|
|
1065
|
+
# This information is critical for AML compliance and transparency.
|
|
1066
|
+
class BeneficialOwner < Dry::Struct
|
|
1067
|
+
attribute :entry_id, Types::String.optional.default(nil)
|
|
1068
|
+
attribute :first_name, Types::String.optional.default(nil)
|
|
1069
|
+
attribute :last_name, Types::String.optional.default(nil)
|
|
1070
|
+
attribute :personal_code, Types::String.optional.default(nil)
|
|
1071
|
+
attribute :foreign_id_code, Types::String.optional.default(nil)
|
|
1072
|
+
attribute :birth_date, Types::String.optional.default(nil)
|
|
1073
|
+
attribute :id_country, Types::String.optional.default(nil)
|
|
1074
|
+
attribute :id_country_text, Types::String.optional.default(nil)
|
|
1075
|
+
attribute :residence_country, Types::String.optional.default(nil)
|
|
1076
|
+
attribute :residence_country_text, Types::String.optional.default(nil)
|
|
1077
|
+
attribute :control_type, Types::String.optional.default(nil)
|
|
1078
|
+
attribute :control_type_text, Types::String.optional.default(nil)
|
|
1079
|
+
attribute :ending_type, Types::String.optional.default(nil)
|
|
1080
|
+
attribute :ending_type_text, Types::String.optional.default(nil)
|
|
1081
|
+
attribute :start_date, Types::String.optional.default(nil)
|
|
1082
|
+
attribute :end_date, Types::String.optional.default(nil)
|
|
1083
|
+
attribute :discrepancy_notice, Types::Bool.default(false)
|
|
1084
|
+
|
|
1085
|
+
# Get full name of beneficial owner
|
|
1086
|
+
# @return [String] Combined first and last name
|
|
1087
|
+
def full_name
|
|
1088
|
+
[first_name, last_name].compact.join(" ")
|
|
1089
|
+
end
|
|
1090
|
+
|
|
1091
|
+
# Check if beneficial owner has a discrepancy notice
|
|
1092
|
+
# @return [Boolean] True if discrepancy notice exists
|
|
1093
|
+
def discrepancy_notice?
|
|
1094
|
+
discrepancy_notice == true
|
|
1095
|
+
end
|
|
1096
|
+
|
|
1097
|
+
# Check if beneficial owner is currently active (no end date)
|
|
1098
|
+
# @return [Boolean] True if owner is currently active
|
|
1099
|
+
def active?
|
|
1100
|
+
end_date.nil? || end_date.empty?
|
|
1101
|
+
end
|
|
1102
|
+
|
|
1103
|
+
# Parse start date to Date object
|
|
1104
|
+
# @return [Date, nil] Parsed start date or nil
|
|
1105
|
+
def start_date_parsed
|
|
1106
|
+
return nil if start_date.nil? || start_date.empty?
|
|
1107
|
+
Date.parse(start_date.sub(/Z$/, ''))
|
|
1108
|
+
rescue Date::Error
|
|
1109
|
+
nil
|
|
1110
|
+
end
|
|
1111
|
+
|
|
1112
|
+
# Parse end date to Date object
|
|
1113
|
+
# @return [Date, nil] Parsed end date or nil
|
|
1114
|
+
def end_date_parsed
|
|
1115
|
+
return nil if end_date.nil? || end_date.empty?
|
|
1116
|
+
Date.parse(end_date.sub(/Z$/, ''))
|
|
1117
|
+
rescue Date::Error
|
|
1118
|
+
nil
|
|
1119
|
+
end
|
|
1120
|
+
|
|
1121
|
+
# Get control type category for compliance analysis
|
|
1122
|
+
# @return [Symbol] Control type category (:ownership, :management, :other)
|
|
1123
|
+
def control_category
|
|
1124
|
+
case control_type
|
|
1125
|
+
when "X" then :ownership # >50% voting rights
|
|
1126
|
+
when "J" then :management # Board/council member
|
|
1127
|
+
when "K" then :ownership # >25% shareholding
|
|
1128
|
+
when "M" then :other # Other control means
|
|
1129
|
+
else :other
|
|
1130
|
+
end
|
|
1131
|
+
end
|
|
1132
|
+
|
|
1133
|
+
# Check if owner is foreign (non-Estonian)
|
|
1134
|
+
# @return [Boolean] True if owner is foreign
|
|
1135
|
+
def foreign?
|
|
1136
|
+
!foreign_id_code.nil? && !foreign_id_code.empty?
|
|
1137
|
+
end
|
|
1138
|
+
|
|
1139
|
+
# Get ownership duration in days
|
|
1140
|
+
# @return [Integer, nil] Days of ownership or nil if still active
|
|
1141
|
+
def ownership_duration_days
|
|
1142
|
+
return nil if start_date_parsed.nil?
|
|
1143
|
+
end_date = end_date_parsed || Date.today
|
|
1144
|
+
(end_date - start_date_parsed).to_i
|
|
1145
|
+
rescue
|
|
1146
|
+
nil
|
|
1147
|
+
end
|
|
1148
|
+
end
|
|
1149
|
+
|
|
1150
|
+
# Container for beneficial owners query results.
|
|
1151
|
+
#
|
|
1152
|
+
# Provides comprehensive information about a company's beneficial
|
|
1153
|
+
# ownership structure including statistics and compliance indicators.
|
|
1154
|
+
class BeneficialOwners < Dry::Struct
|
|
1155
|
+
attribute :registry_code, Types::String.optional.default(nil)
|
|
1156
|
+
attribute :required_to_submit, Types::Bool.default(false)
|
|
1157
|
+
attribute :total_count, Types::Integer.default(0)
|
|
1158
|
+
attribute :hidden_count, Types::Integer.default(0)
|
|
1159
|
+
attribute :no_discrepancy_notice, Types::Bool.default(false)
|
|
1160
|
+
attribute :beneficial_owners, Types::Array.of(BeneficialOwner).default([].freeze)
|
|
1161
|
+
|
|
1162
|
+
# Check if company is required to submit beneficial owners
|
|
1163
|
+
# @return [Boolean] True if submission is required
|
|
1164
|
+
def submission_required?
|
|
1165
|
+
required_to_submit == true
|
|
1166
|
+
end
|
|
1167
|
+
|
|
1168
|
+
# Check if any discrepancy notices exist
|
|
1169
|
+
# @return [Boolean] True if discrepancy notices exist
|
|
1170
|
+
def discrepancy_notice_exists?
|
|
1171
|
+
no_discrepancy_notice == false
|
|
1172
|
+
end
|
|
1173
|
+
|
|
1174
|
+
# Get count of visible (non-hidden) beneficial owners
|
|
1175
|
+
# @return [Integer] Number of visible owners
|
|
1176
|
+
def visible_count
|
|
1177
|
+
total_count - hidden_count
|
|
1178
|
+
end
|
|
1179
|
+
|
|
1180
|
+
# Get currently active beneficial owners
|
|
1181
|
+
# @return [Array<BeneficialOwner>] Active owners
|
|
1182
|
+
def active_owners
|
|
1183
|
+
beneficial_owners.select(&:active?)
|
|
1184
|
+
end
|
|
1185
|
+
|
|
1186
|
+
# Get historical (inactive) beneficial owners
|
|
1187
|
+
# @return [Array<BeneficialOwner>] Inactive owners
|
|
1188
|
+
def inactive_owners
|
|
1189
|
+
beneficial_owners.reject(&:active?)
|
|
1190
|
+
end
|
|
1191
|
+
|
|
1192
|
+
# Get owners with discrepancy notices
|
|
1193
|
+
# @return [Array<BeneficialOwner>] Owners with notices
|
|
1194
|
+
def owners_with_discrepancies
|
|
1195
|
+
beneficial_owners.select(&:discrepancy_notice?)
|
|
1196
|
+
end
|
|
1197
|
+
|
|
1198
|
+
# Group owners by control category
|
|
1199
|
+
# @return [Hash<Symbol, Array<BeneficialOwner>>] Grouped owners
|
|
1200
|
+
def by_control_category
|
|
1201
|
+
beneficial_owners.group_by(&:control_category)
|
|
1202
|
+
end
|
|
1203
|
+
|
|
1204
|
+
# Get foreign beneficial owners
|
|
1205
|
+
# @return [Array<BeneficialOwner>] Foreign owners
|
|
1206
|
+
def foreign_owners
|
|
1207
|
+
beneficial_owners.select(&:foreign?)
|
|
1208
|
+
end
|
|
1209
|
+
|
|
1210
|
+
# Get domestic (Estonian) beneficial owners
|
|
1211
|
+
# @return [Array<BeneficialOwner>] Domestic owners
|
|
1212
|
+
def domestic_owners
|
|
1213
|
+
beneficial_owners.reject(&:foreign?)
|
|
1214
|
+
end
|
|
1215
|
+
|
|
1216
|
+
# Calculate compliance score (0-100)
|
|
1217
|
+
# Higher score indicates better compliance
|
|
1218
|
+
# @return [Integer] Compliance score
|
|
1219
|
+
def compliance_score
|
|
1220
|
+
score = 100
|
|
1221
|
+
score -= 20 if !submission_required? && beneficial_owners.empty?
|
|
1222
|
+
score -= 15 if discrepancy_notice_exists?
|
|
1223
|
+
score -= 10 if hidden_count > 0
|
|
1224
|
+
score -= 10 if beneficial_owners.empty? && submission_required?
|
|
1225
|
+
score -= 5 * owners_with_discrepancies.size
|
|
1226
|
+
[score, 0].max
|
|
1227
|
+
end
|
|
1228
|
+
|
|
1229
|
+
# Generate compliance summary
|
|
1230
|
+
# @return [Hash] Compliance summary statistics
|
|
1231
|
+
def compliance_summary
|
|
1232
|
+
{
|
|
1233
|
+
submission_required: submission_required?,
|
|
1234
|
+
total_owners: total_count,
|
|
1235
|
+
visible_owners: visible_count,
|
|
1236
|
+
hidden_owners: hidden_count,
|
|
1237
|
+
active_owners: active_owners.size,
|
|
1238
|
+
inactive_owners: inactive_owners.size,
|
|
1239
|
+
foreign_owners: foreign_owners.size,
|
|
1240
|
+
domestic_owners: domestic_owners.size,
|
|
1241
|
+
discrepancy_notices: owners_with_discrepancies.size,
|
|
1242
|
+
compliance_score: compliance_score,
|
|
1243
|
+
by_control_type: by_control_category.transform_values(&:size)
|
|
1244
|
+
}
|
|
1245
|
+
end
|
|
1246
|
+
end
|
|
1247
|
+
|
|
1248
|
+
# Represents a change entry for a company.
|
|
1249
|
+
#
|
|
1250
|
+
# Each entry describes a specific type of change that occurred
|
|
1251
|
+
# in the company's registry data on a particular date.
|
|
1252
|
+
class ChangeEntry < Dry::Struct
|
|
1253
|
+
attribute :registry_card_area, Types::String.optional.default(nil)
|
|
1254
|
+
attribute :card_type, Types::String.optional.default(nil)
|
|
1255
|
+
attribute :registry_card_number, Types::String.optional.default(nil)
|
|
1256
|
+
attribute :entry_number, Types::String.optional.default(nil)
|
|
1257
|
+
attribute :entry_type, Types::String.optional.default(nil)
|
|
1258
|
+
attribute :entry_type_text, Types::String.optional.default(nil)
|
|
1259
|
+
attribute :changed_data_type, Types::String.optional.default(nil)
|
|
1260
|
+
|
|
1261
|
+
# Check if this entry represents a personnel change
|
|
1262
|
+
# @return [Boolean] True if entry is personnel-related
|
|
1263
|
+
def personnel_change?
|
|
1264
|
+
entry_type_text&.downcase&.include?("isik") ||
|
|
1265
|
+
changed_data_type&.downcase&.include?("isik")
|
|
1266
|
+
end
|
|
1267
|
+
|
|
1268
|
+
# Check if this entry represents a communication change
|
|
1269
|
+
# @return [Boolean] True if entry is communication-related
|
|
1270
|
+
def communication_change?
|
|
1271
|
+
changed_data_type&.downcase&.include?("side") ||
|
|
1272
|
+
changed_data_type&.downcase&.include?("kontakt")
|
|
1273
|
+
end
|
|
1274
|
+
|
|
1275
|
+
# Check if this entry represents an activity change
|
|
1276
|
+
# @return [Boolean] True if entry is activity-related
|
|
1277
|
+
def activity_change?
|
|
1278
|
+
changed_data_type&.downcase&.include?("tegevus") ||
|
|
1279
|
+
changed_data_type&.downcase&.include?("emtak")
|
|
1280
|
+
end
|
|
1281
|
+
|
|
1282
|
+
# Check if this entry represents an address change
|
|
1283
|
+
# @return [Boolean] True if entry is address-related
|
|
1284
|
+
def address_change?
|
|
1285
|
+
changed_data_type&.downcase&.include?("aadress")
|
|
1286
|
+
end
|
|
1287
|
+
|
|
1288
|
+
# Get change category for analysis
|
|
1289
|
+
# @return [Symbol] Category of change (:personnel, :communication, :activity, :address, :other)
|
|
1290
|
+
def change_category
|
|
1291
|
+
return :personnel if personnel_change?
|
|
1292
|
+
return :communication if communication_change?
|
|
1293
|
+
return :activity if activity_change?
|
|
1294
|
+
return :address if address_change?
|
|
1295
|
+
:other
|
|
1296
|
+
end
|
|
1297
|
+
end
|
|
1298
|
+
|
|
1299
|
+
# Represents a company with its changes on a specific date.
|
|
1300
|
+
#
|
|
1301
|
+
# Contains the company's basic information and all the changes
|
|
1302
|
+
# that occurred on the queried date.
|
|
1303
|
+
class CompanyChange < Dry::Struct
|
|
1304
|
+
attribute :registry_code, Types::String.optional.default(nil)
|
|
1305
|
+
attribute :business_name, Types::String.optional.default(nil)
|
|
1306
|
+
attribute :legal_form, Types::String.optional.default(nil)
|
|
1307
|
+
attribute :entries, Types::Array.of(ChangeEntry).default([].freeze)
|
|
1308
|
+
attribute :non_entered_persons, Types::String.optional.default(nil)
|
|
1309
|
+
attribute :communication_means, Types::String.optional.default(nil)
|
|
1310
|
+
attribute :activity_fields, Types::String.optional.default(nil)
|
|
1311
|
+
|
|
1312
|
+
# Get total number of changes for this company
|
|
1313
|
+
# @return [Integer] Number of change entries
|
|
1314
|
+
def total_changes
|
|
1315
|
+
entries.size
|
|
1316
|
+
end
|
|
1317
|
+
|
|
1318
|
+
# Check if company has personnel changes
|
|
1319
|
+
# @return [Boolean] True if any personnel changes exist
|
|
1320
|
+
def has_personnel_changes?
|
|
1321
|
+
entries.any?(&:personnel_change?) || !non_entered_persons.nil?
|
|
1322
|
+
end
|
|
1323
|
+
|
|
1324
|
+
# Check if company has communication changes
|
|
1325
|
+
# @return [Boolean] True if any communication changes exist
|
|
1326
|
+
def has_communication_changes?
|
|
1327
|
+
entries.any?(&:communication_change?) || !communication_means.nil?
|
|
1328
|
+
end
|
|
1329
|
+
|
|
1330
|
+
# Check if company has activity changes
|
|
1331
|
+
# @return [Boolean] True if any activity changes exist
|
|
1332
|
+
def has_activity_changes?
|
|
1333
|
+
entries.any?(&:activity_change?) || !activity_fields.nil?
|
|
1334
|
+
end
|
|
1335
|
+
|
|
1336
|
+
# Check if company has address changes
|
|
1337
|
+
# @return [Boolean] True if any address changes exist
|
|
1338
|
+
def has_address_changes?
|
|
1339
|
+
entries.any?(&:address_change?)
|
|
1340
|
+
end
|
|
1341
|
+
|
|
1342
|
+
# Group entries by change category
|
|
1343
|
+
# @return [Hash<Symbol, Array<ChangeEntry>>] Grouped entries
|
|
1344
|
+
def entries_by_category
|
|
1345
|
+
entries.group_by(&:change_category)
|
|
1346
|
+
end
|
|
1347
|
+
|
|
1348
|
+
# Get all change categories for this company
|
|
1349
|
+
# @return [Array<Symbol>] Unique change categories
|
|
1350
|
+
def change_categories
|
|
1351
|
+
categories = entries.map(&:change_category)
|
|
1352
|
+
categories << :personnel if !non_entered_persons.nil?
|
|
1353
|
+
categories << :communication if !communication_means.nil?
|
|
1354
|
+
categories << :activity if !activity_fields.nil?
|
|
1355
|
+
categories.uniq
|
|
1356
|
+
end
|
|
1357
|
+
|
|
1358
|
+
# Generate summary of changes
|
|
1359
|
+
# @return [Hash] Change summary with counts
|
|
1360
|
+
def changes_summary
|
|
1361
|
+
by_category = entries_by_category
|
|
1362
|
+
{
|
|
1363
|
+
total_changes: total_changes,
|
|
1364
|
+
personnel_changes: (by_category[:personnel]&.size || 0) + (non_entered_persons ? 1 : 0),
|
|
1365
|
+
communication_changes: (by_category[:communication]&.size || 0) + (communication_means ? 1 : 0),
|
|
1366
|
+
activity_changes: (by_category[:activity]&.size || 0) + (activity_fields ? 1 : 0),
|
|
1367
|
+
address_changes: by_category[:address]&.size || 0,
|
|
1368
|
+
other_changes: by_category[:other]&.size || 0,
|
|
1369
|
+
categories: change_categories.size
|
|
1370
|
+
}
|
|
1371
|
+
end
|
|
1372
|
+
end
|
|
1373
|
+
|
|
1374
|
+
# Container for company changes query results.
|
|
1375
|
+
#
|
|
1376
|
+
# Provides access to all companies that had changes on a specific date
|
|
1377
|
+
# along with pagination and filtering capabilities.
|
|
1378
|
+
class CompanyChanges < Dry::Struct
|
|
1379
|
+
attribute :change_date, Types::String.optional.default(nil)
|
|
1380
|
+
attribute :companies, Types::Array.of(CompanyChange).default([].freeze)
|
|
1381
|
+
attribute :total_count, Types::Integer.default(0)
|
|
1382
|
+
attribute :page, Types::Integer.default(1)
|
|
1383
|
+
attribute :results_per_page, Types::Integer.default(500)
|
|
1384
|
+
|
|
1385
|
+
# Check if there are more pages available
|
|
1386
|
+
# @return [Boolean] True if more pages exist
|
|
1387
|
+
def has_more_pages?
|
|
1388
|
+
(page * results_per_page) < total_count
|
|
1389
|
+
end
|
|
1390
|
+
|
|
1391
|
+
# Calculate total number of pages
|
|
1392
|
+
# @return [Integer] Total pages available
|
|
1393
|
+
def total_pages
|
|
1394
|
+
(total_count.to_f / results_per_page).ceil
|
|
1395
|
+
end
|
|
1396
|
+
|
|
1397
|
+
# Get next page number
|
|
1398
|
+
# @return [Integer, nil] Next page number or nil if last page
|
|
1399
|
+
def next_page
|
|
1400
|
+
has_more_pages? ? page + 1 : nil
|
|
1401
|
+
end
|
|
1402
|
+
|
|
1403
|
+
# Get companies with specific types of changes
|
|
1404
|
+
# @param category [Symbol] Change category (:personnel, :communication, :activity, :address)
|
|
1405
|
+
# @return [Array<CompanyChange>] Filtered companies
|
|
1406
|
+
def companies_with_changes(category)
|
|
1407
|
+
case category
|
|
1408
|
+
when :personnel
|
|
1409
|
+
companies.select(&:has_personnel_changes?)
|
|
1410
|
+
when :communication
|
|
1411
|
+
companies.select(&:has_communication_changes?)
|
|
1412
|
+
when :activity
|
|
1413
|
+
companies.select(&:has_activity_changes?)
|
|
1414
|
+
when :address
|
|
1415
|
+
companies.select(&:has_address_changes?)
|
|
1416
|
+
else
|
|
1417
|
+
[]
|
|
1418
|
+
end
|
|
1419
|
+
end
|
|
1420
|
+
|
|
1421
|
+
# Group companies by the types of changes they have
|
|
1422
|
+
# @return [Hash<Symbol, Array<CompanyChange>>] Companies grouped by change type
|
|
1423
|
+
def companies_by_change_type
|
|
1424
|
+
{
|
|
1425
|
+
personnel: companies_with_changes(:personnel),
|
|
1426
|
+
communication: companies_with_changes(:communication),
|
|
1427
|
+
activity: companies_with_changes(:activity),
|
|
1428
|
+
address: companies_with_changes(:address)
|
|
1429
|
+
}
|
|
1430
|
+
end
|
|
1431
|
+
|
|
1432
|
+
# Find company by registry code
|
|
1433
|
+
# @param registry_code [String] Company registry code
|
|
1434
|
+
# @return [CompanyChange, nil] Found company or nil
|
|
1435
|
+
def find_company(registry_code)
|
|
1436
|
+
companies.find { |c| c.registry_code == registry_code }
|
|
1437
|
+
end
|
|
1438
|
+
|
|
1439
|
+
# Get companies with multiple types of changes
|
|
1440
|
+
# @param min_categories [Integer] Minimum number of change categories (default: 2)
|
|
1441
|
+
# @return [Array<CompanyChange>] Companies with diverse changes
|
|
1442
|
+
def companies_with_multiple_changes(min_categories: 2)
|
|
1443
|
+
companies.select { |c| c.change_categories.size >= min_categories }
|
|
1444
|
+
end
|
|
1445
|
+
|
|
1446
|
+
# Generate summary statistics for all changes
|
|
1447
|
+
# @return [Hash] Comprehensive change statistics
|
|
1448
|
+
def summary
|
|
1449
|
+
all_changes = companies.flat_map(&:entries)
|
|
1450
|
+
by_category = all_changes.group_by(&:change_category)
|
|
1451
|
+
|
|
1452
|
+
{
|
|
1453
|
+
change_date: change_date,
|
|
1454
|
+
total_companies: companies.size,
|
|
1455
|
+
total_changes: all_changes.size,
|
|
1456
|
+
companies_with_personnel_changes: companies_with_changes(:personnel).size,
|
|
1457
|
+
companies_with_communication_changes: companies_with_changes(:communication).size,
|
|
1458
|
+
companies_with_activity_changes: companies_with_changes(:activity).size,
|
|
1459
|
+
companies_with_address_changes: companies_with_changes(:address).size,
|
|
1460
|
+
companies_with_multiple_changes: companies_with_multiple_changes.size,
|
|
1461
|
+
changes_by_category: by_category.transform_values(&:size),
|
|
1462
|
+
pagination: {
|
|
1463
|
+
current_page: page,
|
|
1464
|
+
total_pages: total_pages,
|
|
1465
|
+
results_per_page: results_per_page,
|
|
1466
|
+
total_results: total_count,
|
|
1467
|
+
has_more: has_more_pages?
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
end
|
|
1471
|
+
end
|
|
1472
|
+
|
|
1473
|
+
# Represents a status change for a court ruling.
|
|
1474
|
+
#
|
|
1475
|
+
# Each change tracks when and how a ruling's status was modified
|
|
1476
|
+
# throughout its lifecycle (signed, enforced, etc.).
|
|
1477
|
+
class RulingStatusChange < Dry::Struct
|
|
1478
|
+
attribute :change_id, Types::String.optional.default(nil)
|
|
1479
|
+
attribute :status, Types::String.optional.default(nil)
|
|
1480
|
+
attribute :status_text, Types::String.optional.default(nil)
|
|
1481
|
+
attribute :change_date, Types::String.optional.default(nil)
|
|
1482
|
+
|
|
1483
|
+
# Parse change date to Time object
|
|
1484
|
+
# @return [Time, nil] Parsed change date or nil
|
|
1485
|
+
def change_date_parsed
|
|
1486
|
+
return nil if change_date.nil? || change_date.empty?
|
|
1487
|
+
Time.parse(change_date)
|
|
1488
|
+
rescue ArgumentError
|
|
1489
|
+
nil
|
|
1490
|
+
end
|
|
1491
|
+
|
|
1492
|
+
# Check if this is a final status change (enforced)
|
|
1493
|
+
# @return [Boolean] True if status indicates enforcement
|
|
1494
|
+
def final_status?
|
|
1495
|
+
status == "J" || status_text&.downcase&.include?("jõustunud")
|
|
1496
|
+
end
|
|
1497
|
+
end
|
|
1498
|
+
|
|
1499
|
+
# Represents a registry entry.
|
|
1500
|
+
#
|
|
1501
|
+
# Registry entries document various changes and updates
|
|
1502
|
+
# made to a company's information in the business register.
|
|
1503
|
+
class Entry < Dry::Struct
|
|
1504
|
+
attribute :entry_number, Types::String.optional.default(nil)
|
|
1505
|
+
attribute :entry_date, Types::String.optional.default(nil)
|
|
1506
|
+
attribute :entry_type, Types::String.optional.default(nil)
|
|
1507
|
+
attribute :entry_type_text, Types::String.optional.default(nil)
|
|
1508
|
+
attribute :ruling_number, Types::String.optional.default(nil)
|
|
1509
|
+
attribute :ruling_date, Types::String.optional.default(nil)
|
|
1510
|
+
attribute :ruling_type, Types::String.optional.default(nil)
|
|
1511
|
+
attribute :ruling_type_text, Types::String.optional.default(nil)
|
|
1512
|
+
attribute :ruling_status, Types::String.optional.default(nil)
|
|
1513
|
+
attribute :ruling_status_text, Types::String.optional.default(nil)
|
|
1514
|
+
attribute :card_number, Types::String.optional.default(nil)
|
|
1515
|
+
attribute :card_type, Types::String.optional.default(nil)
|
|
1516
|
+
|
|
1517
|
+
# Parse entry date to Time object
|
|
1518
|
+
# @return [Time, nil] Parsed entry date or nil
|
|
1519
|
+
def entry_date_parsed
|
|
1520
|
+
return nil if entry_date.nil? || entry_date.empty?
|
|
1521
|
+
Time.parse(entry_date)
|
|
1522
|
+
rescue ArgumentError
|
|
1523
|
+
nil
|
|
1524
|
+
end
|
|
1525
|
+
|
|
1526
|
+
# Parse ruling date to Time object
|
|
1527
|
+
# @return [Time, nil] Parsed ruling date or nil
|
|
1528
|
+
def ruling_date_parsed
|
|
1529
|
+
return nil if ruling_date.nil? || ruling_date.empty?
|
|
1530
|
+
Time.parse(ruling_date)
|
|
1531
|
+
rescue ArgumentError
|
|
1532
|
+
nil
|
|
1533
|
+
end
|
|
1534
|
+
|
|
1535
|
+
# Check if this entry has an associated ruling
|
|
1536
|
+
# @return [Boolean] True if ruling information exists
|
|
1537
|
+
def has_ruling?
|
|
1538
|
+
!ruling_number.nil? && !ruling_number.empty?
|
|
1539
|
+
end
|
|
1540
|
+
|
|
1541
|
+
# Check if the associated ruling is enforced
|
|
1542
|
+
# @return [Boolean] True if ruling status indicates enforcement
|
|
1543
|
+
def ruling_enforced?
|
|
1544
|
+
ruling_status == "J" || ruling_status_text&.downcase&.include?("jõustunud")
|
|
1545
|
+
end
|
|
1546
|
+
|
|
1547
|
+
# Get entry category for analysis
|
|
1548
|
+
# @return [Symbol] Entry category (:modification, :registration, :liquidation, :other)
|
|
1549
|
+
def entry_category
|
|
1550
|
+
return :other unless entry_type_text
|
|
1551
|
+
|
|
1552
|
+
text = entry_type_text.downcase
|
|
1553
|
+
return :modification if text.include?("muutmis") || text.include?("muudatus")
|
|
1554
|
+
return :registration if text.include?("registreeri") || text.include?("kanne")
|
|
1555
|
+
return :liquidation if text.include?("likvideeri") || text.include?("lõpetami")
|
|
1556
|
+
:other
|
|
1557
|
+
end
|
|
1558
|
+
end
|
|
1559
|
+
|
|
1560
|
+
# Represents a court ruling.
|
|
1561
|
+
#
|
|
1562
|
+
# Court rulings are official decisions that affect company
|
|
1563
|
+
# registration, modifications, or legal proceedings.
|
|
1564
|
+
class Ruling < Dry::Struct
|
|
1565
|
+
attribute :entry_number, Types::String.optional.default(nil)
|
|
1566
|
+
attribute :entry_date, Types::String.optional.default(nil)
|
|
1567
|
+
attribute :entry_type, Types::String.optional.default(nil)
|
|
1568
|
+
attribute :entry_type_text, Types::String.optional.default(nil)
|
|
1569
|
+
attribute :ruling_number, Types::String.optional.default(nil)
|
|
1570
|
+
attribute :ruling_date, Types::String.optional.default(nil)
|
|
1571
|
+
attribute :ruling_type, Types::String.optional.default(nil)
|
|
1572
|
+
attribute :ruling_type_text, Types::String.optional.default(nil)
|
|
1573
|
+
attribute :ruling_status, Types::String.optional.default(nil)
|
|
1574
|
+
attribute :ruling_status_text, Types::String.optional.default(nil)
|
|
1575
|
+
attribute :ruling_status_date, Types::String.optional.default(nil)
|
|
1576
|
+
attribute :additional_deadline, Types::String.optional.default(nil)
|
|
1577
|
+
attribute :card_number, Types::String.optional.default(nil)
|
|
1578
|
+
attribute :card_type, Types::String.optional.default(nil)
|
|
1579
|
+
attribute :status_changes, Types::Array.of(RulingStatusChange).default([].freeze)
|
|
1580
|
+
|
|
1581
|
+
# Parse entry date to Time object
|
|
1582
|
+
# @return [Time, nil] Parsed entry date or nil
|
|
1583
|
+
def entry_date_parsed
|
|
1584
|
+
return nil if entry_date.nil? || entry_date.empty?
|
|
1585
|
+
Time.parse(entry_date)
|
|
1586
|
+
rescue ArgumentError
|
|
1587
|
+
nil
|
|
1588
|
+
end
|
|
1589
|
+
|
|
1590
|
+
# Parse ruling date to Time object
|
|
1591
|
+
# @return [Time, nil] Parsed ruling date or nil
|
|
1592
|
+
def ruling_date_parsed
|
|
1593
|
+
return nil if ruling_date.nil? || ruling_date.empty?
|
|
1594
|
+
Time.parse(ruling_date)
|
|
1595
|
+
rescue ArgumentError
|
|
1596
|
+
nil
|
|
1597
|
+
end
|
|
1598
|
+
|
|
1599
|
+
# Parse ruling status date to Time object
|
|
1600
|
+
# @return [Time, nil] Parsed status date or nil
|
|
1601
|
+
def ruling_status_date_parsed
|
|
1602
|
+
return nil if ruling_status_date.nil? || ruling_status_date.empty?
|
|
1603
|
+
Time.parse(ruling_status_date)
|
|
1604
|
+
rescue ArgumentError
|
|
1605
|
+
nil
|
|
1606
|
+
end
|
|
1607
|
+
|
|
1608
|
+
# Parse additional deadline to Time object
|
|
1609
|
+
# @return [Time, nil] Parsed deadline or nil
|
|
1610
|
+
def additional_deadline_parsed
|
|
1611
|
+
return nil if additional_deadline.nil? || additional_deadline.empty?
|
|
1612
|
+
Time.parse(additional_deadline)
|
|
1613
|
+
rescue ArgumentError
|
|
1614
|
+
nil
|
|
1615
|
+
end
|
|
1616
|
+
|
|
1617
|
+
# Check if ruling is enforced
|
|
1618
|
+
# @return [Boolean] True if ruling status indicates enforcement
|
|
1619
|
+
def enforced?
|
|
1620
|
+
ruling_status == "J" || ruling_status_text&.downcase&.include?("jõustunud")
|
|
1621
|
+
end
|
|
1622
|
+
|
|
1623
|
+
# Check if ruling is signed
|
|
1624
|
+
# @return [Boolean] True if ruling status indicates signing
|
|
1625
|
+
def signed?
|
|
1626
|
+
ruling_status == "D" || ruling_status_text&.downcase&.include?("allkirjastatud")
|
|
1627
|
+
end
|
|
1628
|
+
|
|
1629
|
+
# Check if ruling has additional deadline
|
|
1630
|
+
# @return [Boolean] True if additional deadline exists
|
|
1631
|
+
def has_additional_deadline?
|
|
1632
|
+
!additional_deadline.nil? && !additional_deadline.empty?
|
|
1633
|
+
end
|
|
1634
|
+
|
|
1635
|
+
# Get most recent status change
|
|
1636
|
+
# @return [RulingStatusChange, nil] Latest status change or nil
|
|
1637
|
+
def latest_status_change
|
|
1638
|
+
return nil if status_changes.empty?
|
|
1639
|
+
|
|
1640
|
+
status_changes.max_by do |change|
|
|
1641
|
+
change.change_date_parsed || Time.at(0)
|
|
1642
|
+
end
|
|
1643
|
+
end
|
|
1644
|
+
|
|
1645
|
+
# Get ruling processing duration (entry to enforcement)
|
|
1646
|
+
# @return [Float, nil] Duration in days or nil
|
|
1647
|
+
def processing_duration_days
|
|
1648
|
+
entry_time = entry_date_parsed
|
|
1649
|
+
status_time = ruling_status_date_parsed
|
|
1650
|
+
|
|
1651
|
+
return nil unless entry_time && status_time
|
|
1652
|
+
|
|
1653
|
+
(status_time - entry_time) / (24 * 60 * 60) # Convert seconds to days
|
|
1654
|
+
end
|
|
1655
|
+
|
|
1656
|
+
# Get ruling type category
|
|
1657
|
+
# @return [Symbol] Ruling category (:entry, :procedural, :other)
|
|
1658
|
+
def ruling_category
|
|
1659
|
+
return :other unless ruling_type
|
|
1660
|
+
|
|
1661
|
+
case ruling_type.upcase
|
|
1662
|
+
when "K1" then :entry # Entry ruling
|
|
1663
|
+
when "U2" then :procedural # Procedural ruling
|
|
1664
|
+
else :other
|
|
1665
|
+
end
|
|
1666
|
+
end
|
|
1667
|
+
end
|
|
1668
|
+
|
|
1669
|
+
# Represents a company with its entries and rulings.
|
|
1670
|
+
#
|
|
1671
|
+
# Contains company information along with all entries and rulings
|
|
1672
|
+
# for a specific time period query.
|
|
1673
|
+
class CompanyWithEntriesRulings < Dry::Struct
|
|
1674
|
+
attribute :company_id, Types::String.optional.default(nil)
|
|
1675
|
+
attribute :business_name, Types::String.optional.default(nil)
|
|
1676
|
+
attribute :legal_form, Types::String.optional.default(nil)
|
|
1677
|
+
attribute :legal_form_text, Types::String.optional.default(nil)
|
|
1678
|
+
attribute :legal_form_subtype, Types::String.optional.default(nil)
|
|
1679
|
+
attribute :legal_form_subtype_text, Types::String.optional.default(nil)
|
|
1680
|
+
attribute :registry_code, Types::String.optional.default(nil)
|
|
1681
|
+
attribute :status, Types::String.optional.default(nil)
|
|
1682
|
+
attribute :status_text, Types::String.optional.default(nil)
|
|
1683
|
+
attribute :entries, Types::Array.of(Entry).default([].freeze)
|
|
1684
|
+
attribute :rulings, Types::Array.of(Ruling).default([].freeze)
|
|
1685
|
+
|
|
1686
|
+
# Get total number of entries
|
|
1687
|
+
# @return [Integer] Number of entries
|
|
1688
|
+
def total_entries
|
|
1689
|
+
entries.size
|
|
1690
|
+
end
|
|
1691
|
+
|
|
1692
|
+
# Get total number of rulings
|
|
1693
|
+
# @return [Integer] Number of rulings
|
|
1694
|
+
def total_rulings
|
|
1695
|
+
rulings.size
|
|
1696
|
+
end
|
|
1697
|
+
|
|
1698
|
+
# Get enforced rulings
|
|
1699
|
+
# @return [Array<Ruling>] Enforced rulings
|
|
1700
|
+
def enforced_rulings
|
|
1701
|
+
rulings.select(&:enforced?)
|
|
1702
|
+
end
|
|
1703
|
+
|
|
1704
|
+
# Get pending rulings (not yet enforced)
|
|
1705
|
+
# @return [Array<Ruling>] Pending rulings
|
|
1706
|
+
def pending_rulings
|
|
1707
|
+
rulings.reject(&:enforced?)
|
|
1708
|
+
end
|
|
1709
|
+
|
|
1710
|
+
# Get entries by category
|
|
1711
|
+
# @return [Hash<Symbol, Array<Entry>>] Grouped entries
|
|
1712
|
+
def entries_by_category
|
|
1713
|
+
entries.group_by(&:entry_category)
|
|
1714
|
+
end
|
|
1715
|
+
|
|
1716
|
+
# Get rulings by category
|
|
1717
|
+
# @return [Hash<Symbol, Array<Ruling>>] Grouped rulings
|
|
1718
|
+
def rulings_by_category
|
|
1719
|
+
rulings.group_by(&:ruling_category)
|
|
1720
|
+
end
|
|
1721
|
+
|
|
1722
|
+
# Check if company has any enforced rulings
|
|
1723
|
+
# @return [Boolean] True if any rulings are enforced
|
|
1724
|
+
def has_enforced_rulings?
|
|
1725
|
+
rulings.any?(&:enforced?)
|
|
1726
|
+
end
|
|
1727
|
+
|
|
1728
|
+
# Check if company has any pending rulings
|
|
1729
|
+
# @return [Boolean] True if any rulings are pending
|
|
1730
|
+
def has_pending_rulings?
|
|
1731
|
+
rulings.any? { |r| !r.enforced? }
|
|
1732
|
+
end
|
|
1733
|
+
|
|
1734
|
+
# Get average ruling processing time
|
|
1735
|
+
# @return [Float, nil] Average days or nil if no data
|
|
1736
|
+
def average_ruling_processing_days
|
|
1737
|
+
durations = rulings.map(&:processing_duration_days).compact
|
|
1738
|
+
return nil if durations.empty?
|
|
1739
|
+
|
|
1740
|
+
durations.sum / durations.size.to_f
|
|
1741
|
+
end
|
|
1742
|
+
|
|
1743
|
+
# Check if company is currently registered
|
|
1744
|
+
# @return [Boolean] True if status indicates registration
|
|
1745
|
+
def registered?
|
|
1746
|
+
status == "R" || status_text&.downcase&.include?("registrisse kantud")
|
|
1747
|
+
end
|
|
1748
|
+
|
|
1749
|
+
# Generate summary of company's legal activity
|
|
1750
|
+
# @return [Hash] Activity summary with statistics
|
|
1751
|
+
def legal_activity_summary
|
|
1752
|
+
{
|
|
1753
|
+
total_entries: total_entries,
|
|
1754
|
+
total_rulings: total_rulings,
|
|
1755
|
+
enforced_rulings: enforced_rulings.size,
|
|
1756
|
+
pending_rulings: pending_rulings.size,
|
|
1757
|
+
registered: registered?,
|
|
1758
|
+
entries_by_category: entries_by_category.transform_values(&:size),
|
|
1759
|
+
rulings_by_category: rulings_by_category.transform_values(&:size),
|
|
1760
|
+
average_processing_days: average_ruling_processing_days&.round(1)
|
|
1761
|
+
}
|
|
1762
|
+
end
|
|
1763
|
+
end
|
|
1764
|
+
|
|
1765
|
+
# Represents a single annual report listing entry.
|
|
1766
|
+
#
|
|
1767
|
+
# Contains metadata about available annual reports for a company,
|
|
1768
|
+
# including report type, financial year, and period information.
|
|
1769
|
+
class AnnualReportListing < Dry::Struct
|
|
1770
|
+
attribute :report_code, Types::String.optional.default(nil)
|
|
1771
|
+
attribute :report_name, Types::String.optional.default(nil)
|
|
1772
|
+
attribute :accounting_year, Types::String.optional.default(nil)
|
|
1773
|
+
attribute :financial_year_start, Types::String.optional.default(nil)
|
|
1774
|
+
attribute :financial_year_end, Types::String.optional.default(nil)
|
|
1775
|
+
|
|
1776
|
+
# Parse financial year start to Date object
|
|
1777
|
+
# @return [Date, nil] Parsed start date or nil
|
|
1778
|
+
def financial_year_start_parsed
|
|
1779
|
+
return nil if financial_year_start.nil? || financial_year_start.empty?
|
|
1780
|
+
Date.parse(financial_year_start.sub(/Z$/, ''))
|
|
1781
|
+
rescue Date::Error
|
|
1782
|
+
nil
|
|
1783
|
+
end
|
|
1784
|
+
|
|
1785
|
+
# Parse financial year end to Date object
|
|
1786
|
+
# @return [Date, nil] Parsed end date or nil
|
|
1787
|
+
def financial_year_end_parsed
|
|
1788
|
+
return nil if financial_year_end.nil? || financial_year_end.empty?
|
|
1789
|
+
Date.parse(financial_year_end.sub(/Z$/, ''))
|
|
1790
|
+
rescue Date::Error
|
|
1791
|
+
nil
|
|
1792
|
+
end
|
|
1793
|
+
|
|
1794
|
+
# Get financial year period description
|
|
1795
|
+
# @return [String] Human-readable period description
|
|
1796
|
+
def financial_year_period
|
|
1797
|
+
return accounting_year.to_s unless financial_year_start && financial_year_end
|
|
1798
|
+
|
|
1799
|
+
start_date = financial_year_start_parsed
|
|
1800
|
+
end_date = financial_year_end_parsed
|
|
1801
|
+
|
|
1802
|
+
if start_date && end_date
|
|
1803
|
+
"#{start_date.strftime('%Y-%m-%d')} to #{end_date.strftime('%Y-%m-%d')}"
|
|
1804
|
+
else
|
|
1805
|
+
accounting_year.to_s
|
|
1806
|
+
end
|
|
1807
|
+
end
|
|
1808
|
+
|
|
1809
|
+
# Check if this is a balance sheet report
|
|
1810
|
+
# @return [Boolean] true for balance sheet report codes
|
|
1811
|
+
def balance_sheet?
|
|
1812
|
+
["14"].include?(report_code)
|
|
1813
|
+
end
|
|
1814
|
+
|
|
1815
|
+
# Check if this is an income statement report
|
|
1816
|
+
# @return [Boolean] true for income statement report codes
|
|
1817
|
+
def income_statement?
|
|
1818
|
+
["35"].include?(report_code)
|
|
1819
|
+
end
|
|
1820
|
+
|
|
1821
|
+
# Check if this is a cash flow statement report
|
|
1822
|
+
# @return [Boolean] true for cash flow statement report codes
|
|
1823
|
+
def cash_flow_statement?
|
|
1824
|
+
["18"].include?(report_code)
|
|
1825
|
+
end
|
|
1826
|
+
|
|
1827
|
+
# Get report type category
|
|
1828
|
+
# @return [Symbol] Report category (:balance_sheet, :income_statement, :cash_flow, :other)
|
|
1829
|
+
def report_category
|
|
1830
|
+
return :balance_sheet if balance_sheet?
|
|
1831
|
+
return :income_statement if income_statement?
|
|
1832
|
+
return :cash_flow if cash_flow_statement?
|
|
1833
|
+
:other
|
|
1834
|
+
end
|
|
1835
|
+
end
|
|
1836
|
+
|
|
1837
|
+
# Container for annual reports list query results.
|
|
1838
|
+
#
|
|
1839
|
+
# Provides access to all available annual reports for a company
|
|
1840
|
+
# with filtering and analysis capabilities.
|
|
1841
|
+
class AnnualReportsListing < Dry::Struct
|
|
1842
|
+
attribute :registry_code, Types::String.optional.default(nil)
|
|
1843
|
+
attribute :reports, Types::Array.of(AnnualReportListing).default([].freeze)
|
|
1844
|
+
|
|
1845
|
+
# Get reports by accounting year
|
|
1846
|
+
# @param year [String, Integer] Accounting year to filter by
|
|
1847
|
+
# @return [Array<AnnualReportListing>] Reports for the specified year
|
|
1848
|
+
def reports_for_year(year)
|
|
1849
|
+
year_str = year.to_s
|
|
1850
|
+
reports.select { |report| report.accounting_year == year_str }
|
|
1851
|
+
end
|
|
1852
|
+
|
|
1853
|
+
# Get unique accounting years available
|
|
1854
|
+
# @return [Array<String>] Sorted list of available years
|
|
1855
|
+
def available_years
|
|
1856
|
+
reports.map(&:accounting_year).compact.uniq.sort.reverse
|
|
1857
|
+
end
|
|
1858
|
+
|
|
1859
|
+
# Get reports by type category
|
|
1860
|
+
# @param category [Symbol] Report category (:balance_sheet, :income_statement, :cash_flow, :other)
|
|
1861
|
+
# @return [Array<AnnualReportListing>] Reports in the specified category
|
|
1862
|
+
def reports_by_category(category)
|
|
1863
|
+
reports.select { |report| report.report_category == category }
|
|
1864
|
+
end
|
|
1865
|
+
|
|
1866
|
+
# Get balance sheet reports
|
|
1867
|
+
# @return [Array<AnnualReportListing>] Balance sheet reports
|
|
1868
|
+
def balance_sheet_reports
|
|
1869
|
+
reports_by_category(:balance_sheet)
|
|
1870
|
+
end
|
|
1871
|
+
|
|
1872
|
+
# Get income statement reports
|
|
1873
|
+
# @return [Array<AnnualReportListing>] Income statement reports
|
|
1874
|
+
def income_statement_reports
|
|
1875
|
+
reports_by_category(:income_statement)
|
|
1876
|
+
end
|
|
1877
|
+
|
|
1878
|
+
# Get cash flow statement reports
|
|
1879
|
+
# @return [Array<AnnualReportListing>] Cash flow statement reports
|
|
1880
|
+
def cash_flow_statement_reports
|
|
1881
|
+
reports_by_category(:cash_flow)
|
|
1882
|
+
end
|
|
1883
|
+
|
|
1884
|
+
# Get the most recent year with reports
|
|
1885
|
+
# @return [String, nil] Most recent accounting year or nil
|
|
1886
|
+
def latest_year
|
|
1887
|
+
available_years.first
|
|
1888
|
+
end
|
|
1889
|
+
|
|
1890
|
+
# Get reports for the most recent year
|
|
1891
|
+
# @return [Array<AnnualReportListing>] Reports for latest year
|
|
1892
|
+
def latest_year_reports
|
|
1893
|
+
return [] unless latest_year
|
|
1894
|
+
reports_for_year(latest_year)
|
|
1895
|
+
end
|
|
1896
|
+
|
|
1897
|
+
# Check if reports are available for a specific year
|
|
1898
|
+
# @param year [String, Integer] Accounting year to check
|
|
1899
|
+
# @return [Boolean] true if reports exist for the year
|
|
1900
|
+
def has_reports_for_year?(year)
|
|
1901
|
+
reports_for_year(year).any?
|
|
1902
|
+
end
|
|
1903
|
+
|
|
1904
|
+
# Get total number of reports
|
|
1905
|
+
# @return [Integer] Total number of reports
|
|
1906
|
+
def total_reports
|
|
1907
|
+
reports.size
|
|
1908
|
+
end
|
|
1909
|
+
|
|
1910
|
+
# Check if any reports are available
|
|
1911
|
+
# @return [Boolean] true if reports exist
|
|
1912
|
+
def has_reports?
|
|
1913
|
+
reports.any?
|
|
1914
|
+
end
|
|
1915
|
+
|
|
1916
|
+
# Group reports by accounting year
|
|
1917
|
+
# @return [Hash<String, Array<AnnualReportListing>>] Reports grouped by year
|
|
1918
|
+
def reports_by_year
|
|
1919
|
+
reports.group_by(&:accounting_year)
|
|
1920
|
+
end
|
|
1921
|
+
|
|
1922
|
+
# Get summary of available reports
|
|
1923
|
+
# @return [Hash] Summary with statistics and groupings
|
|
1924
|
+
def summary
|
|
1925
|
+
{
|
|
1926
|
+
registry_code: registry_code,
|
|
1927
|
+
total_reports: total_reports,
|
|
1928
|
+
available_years: available_years,
|
|
1929
|
+
latest_year: latest_year,
|
|
1930
|
+
reports_by_year: reports_by_year.transform_values(&:size),
|
|
1931
|
+
reports_by_category: {
|
|
1932
|
+
balance_sheet: balance_sheet_reports.size,
|
|
1933
|
+
income_statement: income_statement_reports.size,
|
|
1934
|
+
cash_flow: cash_flow_statement_reports.size,
|
|
1935
|
+
other: reports_by_category(:other).size
|
|
1936
|
+
}
|
|
1937
|
+
}
|
|
1938
|
+
end
|
|
1939
|
+
end
|
|
1940
|
+
|
|
1941
|
+
# Represents the company requisites file download information.
|
|
1942
|
+
#
|
|
1943
|
+
# This endpoint provides a downloadable file containing company
|
|
1944
|
+
# requisite information (registry code, name, address, VAT number)
|
|
1945
|
+
# for all companies. The file is updated once daily.
|
|
1946
|
+
class CompanyRequisitesFile < Dry::Struct
|
|
1947
|
+
attribute :creation_date, Types::String.optional.default(nil)
|
|
1948
|
+
attribute :total_companies, Types::Integer.default(0)
|
|
1949
|
+
attribute :file_name, Types::String.optional.default(nil)
|
|
1950
|
+
attribute :file_reference, Types::String.optional.default(nil)
|
|
1951
|
+
attribute :file_format, Types::String.default("xml")
|
|
1952
|
+
|
|
1953
|
+
# Parse creation date to Date object
|
|
1954
|
+
# @return [Date, nil] Parsed creation date or nil
|
|
1955
|
+
def creation_date_parsed
|
|
1956
|
+
return nil if creation_date.nil? || creation_date.empty?
|
|
1957
|
+
Date.parse(creation_date.sub(/Z$/, ''))
|
|
1958
|
+
rescue Date::Error
|
|
1959
|
+
nil
|
|
1960
|
+
end
|
|
1961
|
+
|
|
1962
|
+
# Check if file is available for download
|
|
1963
|
+
# @return [Boolean] true if file reference is present
|
|
1964
|
+
def file_available?
|
|
1965
|
+
!file_reference.nil? && !file_reference.empty?
|
|
1966
|
+
end
|
|
1967
|
+
|
|
1968
|
+
# Get expected file name with creation date
|
|
1969
|
+
# @return [String] Expected file name with date
|
|
1970
|
+
def expected_file_name
|
|
1971
|
+
date = creation_date_parsed
|
|
1972
|
+
if date
|
|
1973
|
+
"company_requisites_#{date.strftime('%Y-%m-%d')}"
|
|
1974
|
+
else
|
|
1975
|
+
"company_requisites"
|
|
1976
|
+
end
|
|
1977
|
+
end
|
|
1978
|
+
|
|
1979
|
+
# Check if file is recent (created today)
|
|
1980
|
+
# @return [Boolean] true if file was created today
|
|
1981
|
+
def recent?
|
|
1982
|
+
date = creation_date_parsed
|
|
1983
|
+
date && date == Date.today
|
|
1984
|
+
end
|
|
1985
|
+
|
|
1986
|
+
# Get file size category based on company count
|
|
1987
|
+
# @return [Symbol] Size category (:small, :medium, :large, :huge)
|
|
1988
|
+
def size_category
|
|
1989
|
+
case total_companies
|
|
1990
|
+
when 0...10_000
|
|
1991
|
+
:small
|
|
1992
|
+
when 10_000...100_000
|
|
1993
|
+
:medium
|
|
1994
|
+
when 100_000...500_000
|
|
1995
|
+
:large
|
|
1996
|
+
else
|
|
1997
|
+
:huge
|
|
1998
|
+
end
|
|
1999
|
+
end
|
|
2000
|
+
|
|
2001
|
+
# Get human-readable file size estimate
|
|
2002
|
+
# @return [String] Estimated file size description
|
|
2003
|
+
def estimated_size
|
|
2004
|
+
case size_category
|
|
2005
|
+
when :small
|
|
2006
|
+
"< 10MB"
|
|
2007
|
+
when :medium
|
|
2008
|
+
"10-100MB"
|
|
2009
|
+
when :large
|
|
2010
|
+
"100-500MB"
|
|
2011
|
+
when :huge
|
|
2012
|
+
"> 500MB"
|
|
2013
|
+
end
|
|
2014
|
+
end
|
|
2015
|
+
|
|
2016
|
+
# Get download summary information
|
|
2017
|
+
# @return [Hash] Summary with key download information
|
|
2018
|
+
def download_summary
|
|
2019
|
+
{
|
|
2020
|
+
file_name: file_name,
|
|
2021
|
+
expected_name: expected_file_name,
|
|
2022
|
+
creation_date: creation_date,
|
|
2023
|
+
total_companies: total_companies,
|
|
2024
|
+
file_format: file_format,
|
|
2025
|
+
file_available: file_available?,
|
|
2026
|
+
recent: recent?,
|
|
2027
|
+
size_category: size_category,
|
|
2028
|
+
estimated_size: estimated_size
|
|
2029
|
+
}
|
|
2030
|
+
end
|
|
2031
|
+
end
|
|
2032
|
+
|
|
2033
|
+
# Represents a single e-invoice recipient.
|
|
2034
|
+
#
|
|
2035
|
+
# Contains information about a company's e-invoice receiving capabilities,
|
|
2036
|
+
# including their service provider and current status.
|
|
2037
|
+
class EInvoiceRecipient < Dry::Struct
|
|
2038
|
+
attribute :registry_code, Types::String.optional.default(nil)
|
|
2039
|
+
attribute :name, Types::String.optional.default(nil)
|
|
2040
|
+
attribute :service_provider, Types::String.optional.default(nil)
|
|
2041
|
+
attribute :status, Types::String.optional.default(nil)
|
|
2042
|
+
|
|
2043
|
+
# Check if the recipient has a valid e-invoice relationship
|
|
2044
|
+
# @return [Boolean] true if status is "OK"
|
|
2045
|
+
def valid_recipient?
|
|
2046
|
+
status == "OK"
|
|
2047
|
+
end
|
|
2048
|
+
|
|
2049
|
+
# Check if the registry code was not found or invalid
|
|
2050
|
+
# @return [Boolean] true if status is "MR"
|
|
2051
|
+
def not_found?
|
|
2052
|
+
status == "MR"
|
|
2053
|
+
end
|
|
2054
|
+
|
|
2055
|
+
# Check if company name is available in the response
|
|
2056
|
+
# @return [Boolean] true if name is present and not empty
|
|
2057
|
+
def has_name?
|
|
2058
|
+
!name.nil? && !name.empty?
|
|
2059
|
+
end
|
|
2060
|
+
|
|
2061
|
+
# Check if service provider information is available
|
|
2062
|
+
# @return [Boolean] true if service provider is present
|
|
2063
|
+
def has_service_provider?
|
|
2064
|
+
!service_provider.nil? && !service_provider.empty?
|
|
2065
|
+
end
|
|
2066
|
+
|
|
2067
|
+
# Get status description
|
|
2068
|
+
# @return [String] Human-readable status description
|
|
2069
|
+
def status_description
|
|
2070
|
+
case status
|
|
2071
|
+
when "OK"
|
|
2072
|
+
"Valid e-invoice recipient"
|
|
2073
|
+
when "MR"
|
|
2074
|
+
"Registry code not found or no active relationship"
|
|
2075
|
+
else
|
|
2076
|
+
"Unknown status"
|
|
2077
|
+
end
|
|
2078
|
+
end
|
|
2079
|
+
|
|
2080
|
+
# Get recipient summary
|
|
2081
|
+
# @return [Hash] Summary information
|
|
2082
|
+
def summary
|
|
2083
|
+
{
|
|
2084
|
+
registry_code: registry_code,
|
|
2085
|
+
name: name,
|
|
2086
|
+
service_provider: service_provider,
|
|
2087
|
+
status: status,
|
|
2088
|
+
status_description: status_description,
|
|
2089
|
+
valid_recipient: valid_recipient?,
|
|
2090
|
+
has_name: has_name?,
|
|
2091
|
+
has_service_provider: has_service_provider?
|
|
2092
|
+
}
|
|
2093
|
+
end
|
|
2094
|
+
end
|
|
2095
|
+
|
|
2096
|
+
# Container for e-invoice recipients query results.
|
|
2097
|
+
#
|
|
2098
|
+
# Provides access to e-invoice recipient information with
|
|
2099
|
+
# pagination and filtering capabilities.
|
|
2100
|
+
class EInvoiceRecipients < Dry::Struct
|
|
2101
|
+
attribute :total_pages, Types::Integer.default(1)
|
|
2102
|
+
attribute :current_page, Types::Integer.default(1)
|
|
2103
|
+
attribute :recipients, Types::Array.of(EInvoiceRecipient).default([].freeze)
|
|
2104
|
+
attribute :return_names, Types::Bool.default(false)
|
|
2105
|
+
|
|
2106
|
+
# Get valid e-invoice recipients
|
|
2107
|
+
# @return [Array<EInvoiceRecipient>] Recipients with status "OK"
|
|
2108
|
+
def valid_recipients
|
|
2109
|
+
recipients.select(&:valid_recipient?)
|
|
2110
|
+
end
|
|
2111
|
+
|
|
2112
|
+
# Get recipients that were not found
|
|
2113
|
+
# @return [Array<EInvoiceRecipient>] Recipients with status "MR"
|
|
2114
|
+
def not_found_recipients
|
|
2115
|
+
recipients.select(&:not_found?)
|
|
2116
|
+
end
|
|
2117
|
+
|
|
2118
|
+
# Get recipients with service provider information
|
|
2119
|
+
# @return [Array<EInvoiceRecipient>] Recipients that have service provider data
|
|
2120
|
+
def recipients_with_service_provider
|
|
2121
|
+
recipients.select(&:has_service_provider?)
|
|
2122
|
+
end
|
|
2123
|
+
|
|
2124
|
+
# Group recipients by status
|
|
2125
|
+
# @return [Hash<String, Array<EInvoiceRecipient>>] Recipients grouped by status
|
|
2126
|
+
def by_status
|
|
2127
|
+
recipients.group_by(&:status)
|
|
2128
|
+
end
|
|
2129
|
+
|
|
2130
|
+
# Group recipients by service provider
|
|
2131
|
+
# @return [Hash<String, Array<EInvoiceRecipient>>] Recipients grouped by service provider
|
|
2132
|
+
def by_service_provider
|
|
2133
|
+
recipients_with_service_provider.group_by(&:service_provider)
|
|
2134
|
+
end
|
|
2135
|
+
|
|
2136
|
+
# Check if there are more pages available
|
|
2137
|
+
# @return [Boolean] true if more pages exist
|
|
2138
|
+
def has_more_pages?
|
|
2139
|
+
current_page < total_pages
|
|
2140
|
+
end
|
|
2141
|
+
|
|
2142
|
+
# Get next page number
|
|
2143
|
+
# @return [Integer, nil] Next page number or nil if on last page
|
|
2144
|
+
def next_page
|
|
2145
|
+
has_more_pages? ? current_page + 1 : nil
|
|
2146
|
+
end
|
|
2147
|
+
|
|
2148
|
+
# Get previous page number
|
|
2149
|
+
# @return [Integer, nil] Previous page number or nil if on first page
|
|
2150
|
+
def previous_page
|
|
2151
|
+
current_page > 1 ? current_page - 1 : nil
|
|
2152
|
+
end
|
|
2153
|
+
|
|
2154
|
+
# Check if this is the first page
|
|
2155
|
+
# @return [Boolean] true if on page 1
|
|
2156
|
+
def first_page?
|
|
2157
|
+
current_page == 1
|
|
2158
|
+
end
|
|
2159
|
+
|
|
2160
|
+
# Check if this is the last page
|
|
2161
|
+
# @return [Boolean] true if on the last page
|
|
2162
|
+
def last_page?
|
|
2163
|
+
current_page >= total_pages
|
|
2164
|
+
end
|
|
2165
|
+
|
|
2166
|
+
# Get total number of recipients
|
|
2167
|
+
# @return [Integer] Total number of recipients
|
|
2168
|
+
def total_recipients
|
|
2169
|
+
recipients.size
|
|
2170
|
+
end
|
|
2171
|
+
|
|
2172
|
+
# Get summary statistics
|
|
2173
|
+
# @return [Hash] Summary with counts and statistics
|
|
2174
|
+
def summary
|
|
2175
|
+
{
|
|
2176
|
+
total_recipients: total_recipients,
|
|
2177
|
+
valid_recipients: valid_recipients.size,
|
|
2178
|
+
not_found_recipients: not_found_recipients.size,
|
|
2179
|
+
recipients_with_service_provider: recipients_with_service_provider.size,
|
|
2180
|
+
return_names: return_names,
|
|
2181
|
+
pagination: {
|
|
2182
|
+
current_page: current_page,
|
|
2183
|
+
total_pages: total_pages,
|
|
2184
|
+
has_more_pages: has_more_pages?,
|
|
2185
|
+
next_page: next_page,
|
|
2186
|
+
previous_page: previous_page,
|
|
2187
|
+
first_page: first_page?,
|
|
2188
|
+
last_page: last_page?
|
|
2189
|
+
},
|
|
2190
|
+
service_providers: by_service_provider.keys,
|
|
2191
|
+
status_breakdown: by_status.transform_values(&:size)
|
|
2192
|
+
}
|
|
2193
|
+
end
|
|
2194
|
+
|
|
2195
|
+
# Find recipient by registry code
|
|
2196
|
+
# @param registry_code [String] Registry code to find
|
|
2197
|
+
# @return [EInvoiceRecipient, nil] Found recipient or nil
|
|
2198
|
+
def find_recipient(registry_code)
|
|
2199
|
+
recipients.find { |r| r.registry_code == registry_code }
|
|
2200
|
+
end
|
|
2201
|
+
|
|
2202
|
+
# Check if a registry code can receive e-invoices
|
|
2203
|
+
# @param registry_code [String] Registry code to check
|
|
2204
|
+
# @return [Boolean] true if the company can receive e-invoices
|
|
2205
|
+
def can_receive_einvoices?(registry_code)
|
|
2206
|
+
recipient = find_recipient(registry_code)
|
|
2207
|
+
recipient&.valid_recipient? || false
|
|
2208
|
+
end
|
|
2209
|
+
end
|
|
2210
|
+
|
|
2211
|
+
# Container for entries and rulings query results.
|
|
2212
|
+
#
|
|
2213
|
+
# Provides access to all companies with entries or rulings
|
|
2214
|
+
# within a specific time period, with filtering capabilities.
|
|
2215
|
+
class EntriesAndRulings < Dry::Struct
|
|
2216
|
+
attribute :request_type, Types::String.optional.default(nil)
|
|
2217
|
+
attribute :start_time, Types::String.optional.default(nil)
|
|
2218
|
+
attribute :end_time, Types::String.optional.default(nil)
|
|
2219
|
+
attribute :language, Types::String.optional.default(nil)
|
|
2220
|
+
attribute :companies, Types::Array.of(CompanyWithEntriesRulings).default([].freeze)
|
|
2221
|
+
attribute :page, Types::Integer.default(1)
|
|
2222
|
+
|
|
2223
|
+
# Check if this was a entries request
|
|
2224
|
+
# @return [Boolean] True if request type was 'K'
|
|
2225
|
+
def entries_request?
|
|
2226
|
+
request_type == "K"
|
|
2227
|
+
end
|
|
2228
|
+
|
|
2229
|
+
# Check if this was a rulings request
|
|
2230
|
+
# @return [Boolean] True if request type was 'M'
|
|
2231
|
+
def rulings_request?
|
|
2232
|
+
request_type == "M"
|
|
2233
|
+
end
|
|
2234
|
+
|
|
2235
|
+
# Get companies with enforced rulings
|
|
2236
|
+
# @return [Array<CompanyWithEntriesRulings>] Companies with enforced rulings
|
|
2237
|
+
def companies_with_enforced_rulings
|
|
2238
|
+
companies.select(&:has_enforced_rulings?)
|
|
2239
|
+
end
|
|
2240
|
+
|
|
2241
|
+
# Get companies with pending rulings
|
|
2242
|
+
# @return [Array<CompanyWithEntriesRulings>] Companies with pending rulings
|
|
2243
|
+
def companies_with_pending_rulings
|
|
2244
|
+
companies.select(&:has_pending_rulings?)
|
|
2245
|
+
end
|
|
2246
|
+
|
|
2247
|
+
# Get companies by legal form
|
|
2248
|
+
# @param legal_form [String] Legal form code (e.g., "OÜ", "AS")
|
|
2249
|
+
# @return [Array<CompanyWithEntriesRulings>] Filtered companies
|
|
2250
|
+
def companies_by_legal_form(legal_form)
|
|
2251
|
+
companies.select { |c| c.legal_form == legal_form }
|
|
2252
|
+
end
|
|
2253
|
+
|
|
2254
|
+
# Get registered companies
|
|
2255
|
+
# @return [Array<CompanyWithEntriesRulings>] Currently registered companies
|
|
2256
|
+
def registered_companies
|
|
2257
|
+
companies.select(&:registered?)
|
|
2258
|
+
end
|
|
2259
|
+
|
|
2260
|
+
# Find company by registry code
|
|
2261
|
+
# @param registry_code [String] Company registry code
|
|
2262
|
+
# @return [CompanyWithEntriesRulings, nil] Found company or nil
|
|
2263
|
+
def find_company(registry_code)
|
|
2264
|
+
companies.find { |c| c.registry_code == registry_code }
|
|
2265
|
+
end
|
|
2266
|
+
|
|
2267
|
+
# Get all entries across all companies
|
|
2268
|
+
# @return [Array<Entry>] All entries
|
|
2269
|
+
def all_entries
|
|
2270
|
+
companies.flat_map(&:entries)
|
|
2271
|
+
end
|
|
2272
|
+
|
|
2273
|
+
# Get all rulings across all companies
|
|
2274
|
+
# @return [Array<Ruling>] All rulings
|
|
2275
|
+
def all_rulings
|
|
2276
|
+
companies.flat_map(&:rulings)
|
|
2277
|
+
end
|
|
2278
|
+
|
|
2279
|
+
# Parse start time to Time object
|
|
2280
|
+
# @return [Time, nil] Parsed start time or nil
|
|
2281
|
+
def start_time_parsed
|
|
2282
|
+
return nil if start_time.nil? || start_time.empty?
|
|
2283
|
+
Time.parse(start_time)
|
|
2284
|
+
rescue ArgumentError
|
|
2285
|
+
nil
|
|
2286
|
+
end
|
|
2287
|
+
|
|
2288
|
+
# Parse end time to Time object
|
|
2289
|
+
# @return [Time, nil] Parsed end time or nil
|
|
2290
|
+
def end_time_parsed
|
|
2291
|
+
return nil if end_time.nil? || end_time.empty?
|
|
2292
|
+
Time.parse(end_time)
|
|
2293
|
+
rescue ArgumentError
|
|
2294
|
+
nil
|
|
2295
|
+
end
|
|
2296
|
+
|
|
2297
|
+
# Get query duration in hours
|
|
2298
|
+
# @return [Float, nil] Duration in hours or nil
|
|
2299
|
+
def query_duration_hours
|
|
2300
|
+
start_t = start_time_parsed
|
|
2301
|
+
end_t = end_time_parsed
|
|
2302
|
+
|
|
2303
|
+
return nil unless start_t && end_t
|
|
2304
|
+
|
|
2305
|
+
(end_t - start_t) / 3600.0 # Convert seconds to hours
|
|
2306
|
+
end
|
|
2307
|
+
|
|
2308
|
+
# Generate comprehensive summary statistics
|
|
2309
|
+
# @return [Hash] Complete summary with all statistics
|
|
2310
|
+
def summary
|
|
2311
|
+
all_entries_list = all_entries
|
|
2312
|
+
all_rulings_list = all_rulings
|
|
2313
|
+
|
|
2314
|
+
{
|
|
2315
|
+
request_type: request_type,
|
|
2316
|
+
request_type_text: entries_request? ? "Entries" : "Rulings",
|
|
2317
|
+
time_period: {
|
|
2318
|
+
start: start_time,
|
|
2319
|
+
end: end_time,
|
|
2320
|
+
duration_hours: query_duration_hours&.round(2)
|
|
2321
|
+
},
|
|
2322
|
+
total_companies: companies.size,
|
|
2323
|
+
registered_companies: registered_companies.size,
|
|
2324
|
+
total_entries: all_entries_list.size,
|
|
2325
|
+
total_rulings: all_rulings_list.size,
|
|
2326
|
+
enforced_rulings: all_rulings_list.count(&:enforced?),
|
|
2327
|
+
pending_rulings: all_rulings_list.count { |r| !r.enforced? },
|
|
2328
|
+
companies_with_enforced_rulings: companies_with_enforced_rulings.size,
|
|
2329
|
+
companies_with_pending_rulings: companies_with_pending_rulings.size,
|
|
2330
|
+
entries_by_category: all_entries_list.group_by(&:entry_category).transform_values(&:size),
|
|
2331
|
+
rulings_by_category: all_rulings_list.group_by(&:ruling_category).transform_values(&:size),
|
|
2332
|
+
legal_forms: companies.group_by(&:legal_form).transform_values(&:size),
|
|
2333
|
+
page: page,
|
|
2334
|
+
language: language
|
|
2335
|
+
}
|
|
2336
|
+
end
|
|
2337
|
+
end
|
|
2338
|
+
|
|
2339
|
+
class RevenueByEMTAK < Dry::Struct
|
|
2340
|
+
attribute :emtak_code, Types::String.optional.default(nil)
|
|
2341
|
+
attribute :emtak_version, Types::String.optional.default(nil)
|
|
2342
|
+
attribute :amount, Types::Float.optional.default(nil)
|
|
2343
|
+
attribute :percentage, Types::Float.optional.default(nil)
|
|
2344
|
+
attribute :coefficient, Types::Float.optional.default(nil)
|
|
2345
|
+
attribute :is_main_activity, Types::Bool.optional.default(nil)
|
|
2346
|
+
end
|
|
2347
|
+
|
|
2348
|
+
class RevenueBreakdownEntry < Dry::Struct
|
|
2349
|
+
attribute :registry_code, Types::String.optional.default(nil)
|
|
2350
|
+
attribute :legal_form, Types::String.optional.default(nil)
|
|
2351
|
+
attribute :submission_time, Types::String.optional.default(nil)
|
|
2352
|
+
attribute :accounting_year, Types::String.optional.default(nil)
|
|
2353
|
+
attribute :operating_without_revenue, Types::Bool.optional.default(nil)
|
|
2354
|
+
attribute :revenues, Types::Array.of(RevenueByEMTAK).optional.default([].freeze)
|
|
2355
|
+
end
|
|
2356
|
+
|
|
2357
|
+
class RevenueBreakdown < Dry::Struct
|
|
2358
|
+
attribute :entries, Types::Array.of(RevenueBreakdownEntry).optional.default([].freeze)
|
|
2359
|
+
attribute :total_count, Types::Integer.optional.default(nil)
|
|
2360
|
+
attribute :page, Types::Integer.optional.default(nil)
|
|
2361
|
+
end
|
|
2362
|
+
end
|
|
2363
|
+
end
|