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.
@@ -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