diff --git a/README.md b/README.md
index 6b1ba42..aa0f9ea 100644
--- a/README.md
+++ b/README.md
@@ -42,6 +42,14 @@ php artisan vendor:publish --tag=laravel-ares::config
| `http_options.timeout` | `5.0` | Request timeout in seconds |
| `http_options.connect_timeout` | `3.0` | Connection timeout in seconds |
+Environment overrides:
+
+- `ARES_API_URL`
+- `ARES_CACHE_TTL`
+- `ARES_LOG_CHANNEL`
+- `ARES_HTTP_TIMEOUT`
+- `ARES_HTTP_CONNECT_TIMEOUT`
+
## Usage
Use dependency injection when you want explicit contracts:
diff --git a/composer.json b/composer.json
index 718ad91..a75bd3c 100644
--- a/composer.json
+++ b/composer.json
@@ -27,7 +27,10 @@
"autoload": {
"psr-4": {
"NyonCode\\Ares\\": "src/"
- }
+ },
+ "files": [
+ "src/helpers.php"
+ ]
},
"autoload-dev": {
"psr-4": {
diff --git a/config/ares.php b/config/ares.php
index ef2f512..0b21d09 100644
--- a/config/ares.php
+++ b/config/ares.php
@@ -28,7 +28,7 @@
|--------------------------------------------------------------------------
*/
'http_options' => [
- 'timeout' => 5.0,
- 'connect_timeout' => 3.0,
+ 'timeout' => env('ARES_HTTP_TIMEOUT', 5.0),
+ 'connect_timeout' => env('ARES_HTTP_CONNECT_TIMEOUT', 3.0),
],
];
diff --git a/docs/README.md b/docs/README.md
new file mode 100644
index 0000000..c6cf296
--- /dev/null
+++ b/docs/README.md
@@ -0,0 +1,113 @@
+# Laravel ARES Package Documentation
+
+A comprehensive Laravel package for interacting with the Czech ARES (Administrativní registr ekonomických subjektů) business register API.
+
+## Table of Contents
+
+- [Installation](installation.md)
+- [Configuration](configuration.md)
+- [Usage Examples](usage.md)
+- [Helper Functions](helpers.md)
+- [API Reference](api.md)
+- [Events](events.md)
+- [Frequently Asked Questions](faq.md)
+
+## Overview
+
+The Laravel ARES package provides a simple and elegant way to interact with the Czech ARES business register. It includes:
+
+- **Caching**: Built-in caching support to reduce API calls
+- **Events**: Laravel events for successful and failed lookups
+- **Validation**: IC (identification number) format validation
+- **Helper Functions**: Global helper functions for common operations
+- **Artisan Commands**: Command-line tools for testing and debugging
+- **Type Safety**: Full PHP 8.2+ type safety and strict typing
+
+## Quick Start
+
+```bash
+composer require nyoncode/laravel-ares
+```
+
+```php
+// Basic usage
+$company = ares()->findCompany('12345678');
+
+// Using helper functions
+if (ares_is_company_active('12345678')) {
+ $address = ares_get_address('12345678');
+}
+
+// Using facade
+$company = Ares::findCompanyOrFail('12345678');
+
+// Using fluent API - most elegant way
+$companies = ares()
+ ->findMany(['12345678', '87654321'])
+ ->active()
+ ->withVat()
+ ->limit(10)
+ ->getFormatted();
+
+// Or get statistics
+$stats = ares()
+ ->findMany(['12345678', '87654321'])
+ ->get()
+ ->stats();
+```
+
+## Features
+
+### 🔍 Company Lookup
+- Find companies by identification number (IC)
+- Raw API data access
+- Exception-based error handling
+
+### ✅ Validation
+- IC format validation with checksum verification
+- Normalization of IC numbers
+
+### 🚀 Performance
+- Configurable caching
+- HTTP timeout configuration
+- Efficient data processing
+
+### 📊 Data Processing
+- Company statistics
+- Filtering and searching capabilities
+- Formatted display data
+
+### 🎯 Helper Functions
+- Global helper functions like `ares()`, `ares_is_company_active()`
+- Facade-based methods
+- Dependency injection support
+
+### 🌊 Fluent API
+- Method chaining for elegant queries
+- Advanced filtering and pagination
+- Data extraction and statistics
+- Cache management
+
+### 🔧 Laravel Integration
+- Service provider registration
+- Configuration file publishing
+- Artisan commands
+- Event system integration
+
+## Requirements
+
+- PHP 8.2 or higher
+- Laravel 10.0, 11.0, 12.0, or 13.0
+- Guzzle HTTP Client 7.0 or higher
+
+## License
+
+This package is open-source software licensed under the [MIT license](https://opensource.org/licenses/MIT).
+
+## Support
+
+For support and questions, please visit our [GitHub repository](https://github.com/nyoncode/laravel-ares).
+
+---
+
+*Next: [Installation Guide](installation.md)*
diff --git a/docs/api.md b/docs/api.md
new file mode 100644
index 0000000..16a75fd
--- /dev/null
+++ b/docs/api.md
@@ -0,0 +1,411 @@
+# API Reference
+
+This document provides a comprehensive reference for all classes, methods, and interfaces in the Laravel ARES package.
+
+## Core Interfaces
+
+### AresClientInterface
+
+The main interface that defines all ARES operations.
+
+```php
+namespace NyonCode\Ares\Contracts;
+
+interface AresClientInterface
+{
+ public function findCompany(string $ic): ?CompanyData;
+ public function findCompanyRaw(string $ic): ?array;
+ public function findCompanyOrFail(string $ic): CompanyData;
+ public function forgetCompany(string $ic): bool;
+ public function isValidIc(string $ic): bool;
+ public function normalizeIc(string $ic): string;
+}
+```
+
+## Main Classes
+
+### AresClient
+
+The primary implementation of the ARES client.
+
+#### Constructor
+
+```php
+public function __construct(
+ string $baseUrl,
+ int $cacheTtl,
+ LoggerInterface $logger,
+ Dispatcher $events,
+ Cache $cache,
+ Http $http
+)
+```
+
+#### Methods
+
+##### findCompany(string $ic): ?CompanyData
+
+Find a company by its identification number.
+
+**Parameters:**
+- `$ic` (string) - The company identification number
+
+**Returns:**
+- `CompanyData|null` - Company data or null if not found/invalid
+
+**Example:**
+```php
+$company = $ares->findCompany('12345678');
+if ($company) {
+ echo $company->name;
+}
+```
+
+##### findCompanyRaw(string $ic): ?array
+
+Find a company and return raw API response data.
+
+**Parameters:**
+- `$ic` (string) - The company identification number
+
+**Returns:**
+- `array|null` - Raw API response or null if not found
+
+**Example:**
+```php
+$raw = $ares->findCompanyRaw('12345678');
+if ($raw) {
+ echo $raw['obchodniJmeno'];
+}
+```
+
+##### findCompanyOrFail(string $ic): CompanyData
+
+Find a company or throw an exception.
+
+**Parameters:**
+- `$ic` (string) - The company identification number
+
+**Returns:**
+- `CompanyData` - Company data
+
+**Throws:**
+- `InvalidIcException` - When IC format is invalid
+- `CompanyNotFoundException` - When company is not found
+
+**Example:**
+```php
+try {
+ $company = $ares->findCompanyOrFail('12345678');
+} catch (CompanyNotFoundException $e) {
+ echo "Company not found";
+}
+```
+
+##### forgetCompany(string $ic): bool
+
+Remove a company from cache.
+
+**Parameters:**
+- `$ic` (string) - The company identification number
+
+**Returns:**
+- `bool` - True if cache entry was removed
+
+**Example:**
+```php
+$ares->forgetCompany('12345678');
+```
+
+##### isValidIc(string $ic): bool
+
+Validate IC format and checksum.
+
+**Parameters:**
+- `$ic` (string) - The identification number to validate
+
+**Returns:**
+- `bool` - True if IC is valid
+
+**Example:**
+```php
+if ($ares->isValidIc('12345678')) {
+ echo "Valid IC";
+}
+```
+
+##### normalizeIc(string $ic): string
+
+Normalize IC to 8-digit format.
+
+**Parameters:**
+- `$ic` (string) - The identification number to normalize
+
+**Returns:**
+- `string` - Normalized 8-digit IC
+
+**Example:**
+```php
+$normalized = $ares->normalizeIc('123 456 78');
+echo $normalized; // '12345678'
+```
+
+## Data Classes
+
+### CompanyData
+
+Represents a company with all its information.
+
+#### Properties
+
+```php
+public string $ic;
+public string $name;
+public ?string $dic;
+public RegistrationData $registration;
+public ?AddressData $registeredOffice;
+public ?AddressData $deliveryAddress;
+public ?array $rawData;
+```
+
+#### Methods
+
+##### fromApiResponse(array $data): CompanyData
+
+Create CompanyData from API response.
+
+**Parameters:**
+- `$data` (array) - Raw API response data
+
+**Returns:**
+- `CompanyData` - Company data object
+
+### RegistrationData
+
+Contains company registration information.
+
+#### Properties
+
+```php
+public bool $active;
+public ?string $dateOfEstablishment;
+public ?string $legalForm;
+public ?string $financialOffice;
+public ?string $primarySource;
+public ?string $businessRegisterFileMark;
+```
+
+### AddressData
+
+Represents an address.
+
+#### Properties
+
+```php
+public ?string $street;
+public ?string $city;
+public ?string $zipCode;
+public ?string $country;
+public ?string $formatted;
+```
+
+### DeliveryAddressData
+
+Represents a delivery address.
+
+#### Properties
+
+```php
+public ?string $street;
+public ?string $city;
+public ?string $zipCode;
+public ?string $country;
+public ?string $formatted;
+```
+
+## Exceptions
+
+### InvalidIcException
+
+Thrown when IC format is invalid.
+
+#### Methods
+
+##### forIc(string $ic): self
+
+Create exception for invalid IC.
+
+**Parameters:**
+- `$ic` (string) - The invalid IC
+
+**Returns:**
+- `InvalidIcException` - Exception instance
+
+### CompanyNotFoundException
+
+Thrown when company is not found in ARES.
+
+#### Methods
+
+##### forIc(string $ic): self
+
+Create exception for company not found.
+
+**Parameters:**
+- `$ic` (string) - The IC that was not found
+
+**Returns:**
+- `CompanyNotFoundException` - Exception instance
+
+### InvalidApiResponseException
+
+Thrown when API response is invalid.
+
+#### Methods
+
+##### missingRequiredField(string $field): self
+
+Create exception for missing field.
+
+**Parameters:**
+- `$field` (string) - Missing field name
+
+**Returns:**
+- `InvalidApiResponseException` - Exception instance
+
+##### invalidPayloadType(): self
+
+Create exception for invalid payload type.
+
+**Returns:**
+- `InvalidApiResponseException` - Exception instance
+
+## Facade
+
+### Ares
+
+Laravel facade for accessing ARES client.
+
+#### Available Methods
+
+All methods from `AresClientInterface` are available:
+
+```php
+Ares::findCompany($ic);
+Ares::findCompanyRaw($ic);
+Ares::findCompanyOrFail($ic);
+Ares::forgetCompany($ic);
+Ares::isValidIc($ic);
+Ares::normalizeIc($ic);
+```
+
+## Artisan Commands
+
+### TestAresCommand
+
+Test ARES API communication.
+
+#### Usage
+
+```bash
+php artisan ares:test {ic}
+```
+
+**Parameters:**
+- `ic` - Company identification number
+
+**Example:**
+```bash
+php artisan ares:test 12345678
+```
+
+## Events
+
+### CompanyLookupSucceeded
+
+Dispatched when company lookup succeeds.
+
+#### Constructor
+
+```php
+public function __construct(CompanyData $company)
+```
+
+#### Properties
+
+```php
+public CompanyData $company;
+```
+
+### CompanyLookupFailed
+
+Dispatched when company lookup fails.
+
+#### Constructor
+
+```php
+public function __construct(string $ic, int $status = 0, ?Throwable $exception = null)
+```
+
+#### Properties
+
+```php
+public string $ic;
+public int $status;
+public ?Throwable $exception;
+```
+
+## Enums
+
+### RegistrationSourceState
+
+Represents registration source states.
+
+#### Values
+
+```php
+case ACTIVE = 'ACTIVE';
+case INACTIVE = 'INACTIVE';
+```
+
+## Type Hints
+
+The package uses strict typing throughout. All methods have proper return type declarations and parameter type hints.
+
+```php
+// Example method signature
+public function findCompany(string $ic): ?CompanyData
+```
+
+## Error Handling
+
+All methods follow consistent error handling patterns:
+
+1. **Null returns**: Methods that might not find data return `null`
+2. **Exceptions**: Methods that should always succeed throw exceptions on failure
+3. **Validation**: Input validation happens before API calls
+4. **Logging**: Errors are automatically logged
+
+## Performance Considerations
+
+### Caching
+
+- Results are cached automatically based on configuration
+- Use `forgetCompany()` to clear specific cache entries
+- Cache TTL is configurable
+
+### HTTP Timeouts
+
+- Connection timeout: 3 seconds (configurable)
+- Request timeout: 5 seconds (configurable)
+- Failed requests are logged and events are dispatched
+
+### Memory Usage
+
+- Large API responses are processed efficiently
+- Only necessary data is stored in objects
+- Raw data is available but optional
+
+---
+
+*Previous: [Usage Examples](usage.md) | Next: [Helper Functions](helpers.md)*
diff --git a/docs/configuration.md b/docs/configuration.md
new file mode 100644
index 0000000..dde7e63
--- /dev/null
+++ b/docs/configuration.md
@@ -0,0 +1,466 @@
+# Configuration Guide
+
+This guide covers all configuration options available in the Laravel ARES package.
+
+## Configuration File
+
+Publish the configuration file to customize package settings:
+
+```bash
+php artisan vendor:publish --tag="ares-config"
+```
+
+This creates `config/ares.php` with the following structure:
+
+```php
+ env('ARES_API_URL', 'https://ares.gov.cz/ekonomicke-subjekty-v-be/rest'),
+ 'cache_ttl' => env('ARES_CACHE_TTL', 86400),
+ 'log_channel' => env('ARES_LOG_CHANNEL', 'stack'),
+ 'http_options' => [
+ 'timeout' => env('ARES_HTTP_TIMEOUT', 5.0),
+ 'connect_timeout' => env('ARES_HTTP_CONNECT_TIMEOUT', 3.0),
+ ],
+];
+```
+
+## Configuration Options
+
+### api_url
+
+The base URL for the ARES API endpoint.
+
+**Type:** `string`
+**Default:** `https://ares.gov.cz/ekonomicke-subjekty-v-be/rest`
+**Environment Variable:** `ARES_API_URL`
+
+#### Available Endpoints
+
+- **Production:** `https://ares.gov.cz/ekonomicke-subjekty-v-be/rest`
+- **Testing:** You can use mock services for testing
+
+#### Example
+
+```env
+ARES_API_URL=https://ares.gov.cz/ekonomicke-subjekty-v-be/rest
+```
+
+```php
+// config/ares.php
+'api_url' => 'https://ares.gov.cz/ekonomicke-subjekty-v-be/rest',
+```
+
+### cache_ttl
+
+Cache time-to-live in seconds for ARES API responses.
+
+**Type:** `int`
+**Default:** `86400` (24 hours)
+**Environment Variable:** `ARES_CACHE_TTL`
+
+#### Recommended Values
+
+- **Development:** `60` (1 minute) - for frequent testing
+- **Production:** `3600` (1 hour) - balanced performance
+- **High Traffic:** `86400` (24 hours) - maximum performance
+- **Real-time Data:** `0` - disable caching
+
+#### Examples
+
+```env
+# Development
+ARES_CACHE_TTL=60
+
+# Production
+ARES_CACHE_TTL=3600
+
+# High traffic applications
+ARES_CACHE_TTL=86400
+
+# Disable caching
+ARES_CACHE_TTL=0
+```
+
+```php
+// config/ares.php
+'cache_ttl' => 86400, // 24 hours
+```
+
+### log_channel
+
+The Laravel log channel used for ARES-related logging.
+
+**Type:** `string`
+**Default:** `stack`
+**Environment Variable:** `ARES_LOG_CHANNEL`
+
+#### Available Channels
+
+- `default` - Uses your default log channel
+- `single` - Logs to `storage/logs/laravel.log`
+- `daily` - Creates daily log files
+- `stack` - Multiple log channels
+- Custom channels defined in `config/logging.php`
+
+#### Examples
+
+```env
+# Use default channel
+ARES_LOG_CHANNEL=default
+
+# Use dedicated ARES log file
+ARES_LOG_CHANNEL=ares
+
+# Use stack for multiple channels
+ARES_LOG_CHANNEL=stack
+```
+
+```php
+// config/logging.php - add custom channel
+'channels' => [
+ 'ares' => [
+ 'driver' => 'single',
+ 'path' => storage_path('logs/ares.log'),
+ 'level' => 'info',
+ ],
+],
+
+// config/ares.php
+'log_channel' => 'ares',
+```
+
+### http_options
+
+HTTP client configuration for API requests.
+
+#### timeout
+
+Request timeout in seconds.
+
+**Type:** `float`
+**Default:** `5.0`
+**Environment Variable:** `ARES_HTTP_TIMEOUT`
+
+#### connect_timeout
+
+Connection timeout in seconds.
+
+**Type:** `float`
+**Default:** `3.0`
+**Environment Variable:** `ARES_HTTP_CONNECT_TIMEOUT`
+
+#### Recommended Values
+
+- **Fast Networks:** `timeout: 3.0, connect_timeout: 2.0`
+- **Standard:** `timeout: 5.0, connect_timeout: 3.0` (default)
+- **Slow Networks:** `timeout: 10.0, connect_timeout: 5.0`
+- **Unreliable Networks:** `timeout: 15.0, connect_timeout: 8.0`
+
+#### Examples
+
+```env
+# Standard configuration
+ARES_HTTP_TIMEOUT=5.0
+ARES_HTTP_CONNECT_TIMEOUT=3.0
+
+# Fast networks
+ARES_HTTP_TIMEOUT=3.0
+ARES_HTTP_CONNECT_TIMEOUT=2.0
+
+# Slow networks
+ARES_HTTP_TIMEOUT=10.0
+ARES_HTTP_CONNECT_TIMEOUT=5.0
+```
+
+```php
+// config/ares.php
+'http_options' => [
+ 'timeout' => 5.0,
+ 'connect_timeout' => 3.0,
+],
+```
+
+## Environment Configuration
+
+### .env File
+
+Add these variables to your `.env` file:
+
+```env
+# ARES Configuration
+ARES_API_URL=https://ares.gov.cz/ekonomicke-subjekty-v-be/rest
+ARES_CACHE_TTL=86400
+ARES_LOG_CHANNEL=stack
+ARES_HTTP_TIMEOUT=5.0
+ARES_HTTP_CONNECT_TIMEOUT=3.0
+```
+
+### Environment-Specific Configurations
+
+#### Development Environment
+
+```env
+# .env
+ARES_CACHE_TTL=60
+ARES_LOG_CHANNEL=stack
+ARES_HTTP_TIMEOUT=3.0
+ARES_HTTP_CONNECT_TIMEOUT=2.0
+```
+
+```php
+// config/ares.php (development)
+'cache_ttl' => env('ARES_CACHE_TTL', 60),
+'log_channel' => env('ARES_LOG_CHANNEL', 'stack'),
+'http_options' => [
+ 'timeout' => env('ARES_HTTP_TIMEOUT', 3.0),
+ 'connect_timeout' => env('ARES_HTTP_CONNECT_TIMEOUT', 2.0),
+],
+```
+
+#### Production Environment
+
+```env
+# .env.production
+ARES_CACHE_TTL=3600
+ARES_LOG_CHANNEL=daily
+ARES_HTTP_TIMEOUT=5.0
+ARES_HTTP_CONNECT_TIMEOUT=3.0
+```
+
+```php
+// config/ares.php (production)
+'cache_ttl' => env('ARES_CACHE_TTL', 3600),
+'log_channel' => env('ARES_LOG_CHANNEL', 'daily'),
+'http_options' => [
+ 'timeout' => env('ARES_HTTP_TIMEOUT', 5.0),
+ 'connect_timeout' => env('ARES_HTTP_CONNECT_TIMEOUT', 3.0),
+],
+```
+
+#### Testing Environment
+
+```env
+# .env.testing
+ARES_CACHE_TTL=0
+ARES_LOG_CHANNEL=stack
+ARES_HTTP_TIMEOUT=1.0
+ARES_HTTP_CONNECT_TIMEOUT=0.5
+```
+
+## Advanced Configuration
+
+### Custom Cache Configuration
+
+You can customize cache behavior by modifying the cache store:
+
+```php
+// config/ares.php
+// Use Redis for better performance
+'cache_store' => env('ARES_CACHE_STORE', 'redis'),
+'cache_prefix' => env('ARES_CACHE_PREFIX', 'ares'),
+```
+
+### Retry Configuration
+
+Configure retry logic for failed requests:
+
+```php
+// config/ares.php
+'retry' => [
+ 'attempts' => env('ARES_RETRY_ATTEMPTS', 3),
+ 'delay' => env('ARES_RETRY_DELAY', 1000), // milliseconds
+],
+```
+
+### Rate Limiting
+
+Configure rate limiting to avoid API limits:
+
+```php
+// config/ares.php
+'rate_limit' => [
+ 'requests_per_minute' => env('ARES_RATE_LIMIT', 60),
+ 'requests_per_hour' => env('ARES_RATE_LIMIT_HOUR', 1000),
+],
+```
+
+### Custom Headers
+
+Add custom HTTP headers:
+
+```php
+// config/ares.php
+'http_headers' => [
+ 'User-Agent' => env('ARES_USER_AGENT', 'Laravel-ARES/1.0'),
+ 'Accept' => 'application/json',
+],
+```
+
+## Cache Configuration
+
+### Cache Store Configuration
+
+Configure different cache stores for ARES:
+
+```php
+// config/cache.php
+'stores' => [
+ 'ares' => [
+ 'driver' => 'redis',
+ 'connection' => 'cache',
+ 'prefix' => 'ares_cache',
+ ],
+],
+```
+
+```env
+ARES_CACHE_STORE=ares
+```
+
+### Cache Key Strategy
+
+The package uses the following cache key pattern:
+```
+ares:v1:company:{ic}
+```
+
+You can customize the prefix:
+
+```php
+// config/ares.php
+'cache_key_prefix' => env('ARES_CACHE_PREFIX', 'ares:v1'),
+```
+
+## Logging Configuration
+
+### Dedicated ARES Log Channel
+
+Create a dedicated log channel for ARES:
+
+```php
+// config/logging.php
+'channels' => [
+ 'ares' => [
+ 'driver' => 'daily',
+ 'path' => storage_path('logs/ares.log'),
+ 'level' => env('ARES_LOG_LEVEL', 'info'),
+ 'days' => 30,
+ ],
+],
+```
+
+```env
+ARES_LOG_CHANNEL=ares
+ARES_LOG_LEVEL=debug
+```
+
+### Log Levels
+
+Control the verbosity of ARES logging:
+
+```env
+# Production - only errors and warnings
+ARES_LOG_LEVEL=warning
+
+# Development - include info and debug
+ARES_LOG_LEVEL=debug
+
+# Minimal logging
+ARES_LOG_LEVEL=error
+```
+
+## Performance Optimization
+
+### High Traffic Configuration
+
+For high-traffic applications:
+
+```env
+ARES_CACHE_TTL=86400
+ARES_HTTP_TIMEOUT=3.0
+ARES_HTTP_CONNECT_TIMEOUT=2.0
+ARES_CACHE_STORE=redis
+```
+
+### Memory Optimization
+
+For memory-constrained environments:
+
+```env
+ARES_CACHE_TTL=1800
+ARES_HTTP_TIMEOUT=2.0
+ARES_HTTP_CONNECT_TIMEOUT=1.0
+```
+
+### Real-time Data Configuration
+
+When you need the most current data:
+
+```env
+ARES_CACHE_TTL=0
+ARES_HTTP_TIMEOUT=10.0
+ARES_HTTP_CONNECT_TIMEOUT=5.0
+```
+
+## Troubleshooting Configuration
+
+### Common Issues
+
+#### 1. Cache Not Working
+
+```php
+// Clear cache
+php artisan cache:clear
+
+// Check cache configuration
+php artisan config:cache
+
+// Verify cache store
+php artisan tinker
+>>> cache()->store('redis')->put('test', 'value', 60);
+```
+
+#### 2. Logging Issues
+
+```php
+// Test log channel
+php artisan tinker
+>>> Log::channel('ares')->info('Test message');
+```
+
+#### 3. HTTP Timeout Issues
+
+```php
+// Test HTTP connectivity
+php artisan tinker
+>>> Http::timeout(5.0)->get('https://ares.gov.cz/ekonomicke-subjekty-v-be/rest');
+```
+
+### Debug Configuration
+
+Enable debug mode for troubleshooting:
+
+```php
+// config/ares.php
+'debug' => env('ARES_DEBUG', false),
+```
+
+```env
+ARES_DEBUG=true
+ARES_LOG_LEVEL=debug
+```
+
+## Configuration Validation
+
+The package validates configuration on startup. Common validation errors:
+
+- **Invalid URL**: `api_url` must be a valid URL
+- **Invalid TTL**: `cache_ttl` must be a non-negative integer
+- **Invalid Timeout**: HTTP timeouts must be positive numbers
+- **Missing Channel**: `log_channel` must exist in logging configuration
+
+---
+
+*Previous: [Helper Functions](helpers.md) | Next: [Events](events.md)*
diff --git a/docs/events.md b/docs/events.md
new file mode 100644
index 0000000..8ed5e68
--- /dev/null
+++ b/docs/events.md
@@ -0,0 +1,519 @@
+# Events Documentation
+
+The Laravel ARES package dispatches events for various operations, allowing you to hook into the lookup process and implement custom logic.
+
+## Available Events
+
+### CompanyLookupSucceeded
+
+Dispatched when a company lookup is successful.
+
+#### Event Class
+
+```php
+namespace NyonCode\Ares\Events;
+
+class CompanyLookupSucceeded
+{
+ public function __construct(
+ public CompanyData $company
+ ) {}
+}
+```
+
+#### Properties
+
+- `$company` (`CompanyData`) - The successfully retrieved company data
+
+#### Usage Example
+
+```php
+use NyonCode\Ares\Events\CompanyLookupSucceeded;
+use Illuminate\Support\Facades\Log;
+
+class EventServiceProvider extends ServiceProvider
+{
+ protected $listen = [
+ CompanyLookupSucceeded::class => [
+ CompanyLookupSuccessListener::class,
+ ],
+ ];
+}
+
+class CompanyLookupSuccessListener
+{
+ public function handle(CompanyLookupSucceeded $event): void
+ {
+ $company = $event->company;
+
+ Log::info("Company lookup successful", [
+ 'ic' => $company->ic,
+ 'name' => $company->name,
+ 'active' => $company->registration->active,
+ ]);
+
+ // Update local database
+ LocalCompany::updateOrCreate(
+ ['ic' => $company->ic],
+ [
+ 'name' => $company->name,
+ 'address' => $company->registeredOffice?->formatted,
+ 'active' => $company->registration->active,
+ 'updated_at' => now(),
+ ]
+ );
+ }
+}
+```
+
+#### Closure Listener
+
+```php
+use NyonCode\Ares\Events\CompanyLookupSucceeded;
+
+Event::listen(CompanyLookupSucceeded::class, function ($event) {
+ $company = $event->company;
+
+ // Send notification
+ if (!$company->registration->active) {
+ Notification::route('slack', config('services.slack.webhook'))
+ ->notify(new InactiveCompanyFound($company));
+ }
+});
+```
+
+### CompanyLookupFailed
+
+Dispatched when a company lookup fails.
+
+#### Event Class
+
+```php
+namespace NyonCode\Ares\Events;
+
+class CompanyLookupFailed
+{
+ public function __construct(
+ public string $ic,
+ public int $status = 0,
+ public ?Throwable $exception = null
+ ) {}
+}
+```
+
+#### Properties
+
+- `$ic` (`string`) - The identification number that was looked up
+- `$status` (`int`) - HTTP status code (0 for exceptions)
+- `$exception` (`Throwable|null`) - The exception that caused the failure
+
+#### Usage Example
+
+```php
+use NyonCode\Ares\Events\CompanyLookupFailed;
+use Illuminate\Support\Facades\Log;
+
+class CompanyLookupFailedListener
+{
+ public function handle(CompanyLookupFailed $event): void
+ {
+ Log::error("Company lookup failed", [
+ 'ic' => $event->ic,
+ 'status' => $event->status,
+ 'exception' => $event->exception?->getMessage(),
+ ]);
+
+ // Track failed lookups for monitoring
+ FailedLookup::create([
+ 'ic' => $event->ic,
+ 'status' => $event->status,
+ 'error_message' => $event->exception?->getMessage(),
+ 'occurred_at' => now(),
+ ]);
+
+ // Send alert for critical failures
+ if ($event->status >= 500 || $event->exception) {
+ Notification::route('mail', config('alerts.email'))
+ ->notify(new AresServiceDown($event));
+ }
+ }
+}
+```
+
+#### Handling Specific Failure Types
+
+```php
+Event::listen(CompanyLookupFailed::class, function ($event) {
+ // Handle HTTP errors
+ if ($event->status >= 400) {
+ $this->handleHttpError($event);
+ }
+
+ // Handle exceptions
+ if ($event->exception) {
+ $this->handleException($event);
+ }
+
+ // Handle specific status codes
+ match ($event->status) {
+ 404 => $this->handleNotFound($event),
+ 429 => $this->handleRateLimit($event),
+ 500 => $this->handleServerError($event),
+ default => $this->handleGenericFailure($event),
+ };
+});
+```
+
+## Event Registration
+
+### Service Provider Registration
+
+Register event listeners in your `EventServiceProvider`:
+
+```php
+// app/Providers/EventServiceProvider.php
+
+protected $listen = [
+ CompanyLookupSucceeded::class => [
+ Listeners\LogSuccessfulLookup::class,
+ Listeners\UpdateLocalDatabase::class,
+ ],
+ CompanyLookupFailed::class => [
+ Listeners\LogFailedLookup::class,
+ Listeners\TrackFailures::class,
+ Listeners\SendAlerts::class,
+ ],
+];
+```
+
+### Manual Registration
+
+Register listeners manually:
+
+```php
+// app/Providers/AppServiceProvider.php
+
+public function boot(): void
+{
+ Event::listen(CompanyLookupSucceeded::class, function ($event) {
+ // Handle successful lookup
+ });
+
+ Event::listen(CompanyLookupFailed::class, function ($event) {
+ // Handle failed lookup
+ });
+}
+```
+
+### Subscriber Pattern
+
+Use event subscribers for complex logic:
+
+```php
+class AresEventSubscriber
+{
+ public function subscribe(Dispatcher $events): void
+ {
+ $events->listen(
+ CompanyLookupSucceeded::class,
+ [self::class, 'handleSuccessfulLookup']
+ );
+
+ $events->listen(
+ CompanyLookupFailed::class,
+ [self::class, 'handleFailedLookup']
+ );
+ }
+
+ public function handleSuccessfulLookup(CompanyLookupSucceeded $event): void
+ {
+ // Handle success
+ }
+
+ public function handleFailedLookup(CompanyLookupFailed $event): void
+ {
+ // Handle failure
+ }
+}
+```
+
+Register the subscriber:
+
+```php
+// app/Providers/EventServiceProvider.php
+
+protected $subscribe = [
+ AresEventSubscriber::class,
+];
+```
+
+## Common Use Cases
+
+### Database Synchronization
+
+Keep your local database synchronized with ARES:
+
+```php
+class SyncCompanyToLocalDatabase
+{
+ public function handle(CompanyLookupSucceeded $event): void
+ {
+ $company = $event->company;
+
+ Company::updateOrCreate(
+ ['ic' => $company->ic],
+ [
+ 'name' => $company->name,
+ 'dic' => $company->dic,
+ 'legal_form' => $company->registration->legalForm,
+ 'address' => $company->registeredOffice?->formatted,
+ 'active' => $company->registration->active,
+ 'established_at' => $company->registration->dateOfEstablishment,
+ 'financial_office' => $company->registration->financialOffice,
+ 'raw_data' => $company->rawData,
+ 'synced_at' => now(),
+ ]
+ );
+ }
+}
+```
+
+### Monitoring and Alerting
+
+Monitor lookup performance and failures:
+
+```php
+class AresMonitoringListener
+{
+ public function handleSuccessfulLookup(CompanyLookupSucceeded $event): void
+ {
+ // Record successful lookup metrics
+ Metrics::increment('ares.lookups.success');
+ Metrics::histogram('ares.lookup.duration', $this->getLookupDuration());
+
+ // Check for inactive companies
+ if (!$event->company->registration->active) {
+ Metrics::increment('ares.companies.inactive');
+ }
+ }
+
+ public function handleFailedLookup(CompanyLookupFailed $event): void
+ {
+ // Record failure metrics
+ Metrics::increment('ares.lookups.failed');
+ Metrics::increment('ares.lookups.failed.by_status', ['status' => $event->status]);
+
+ // Alert on high failure rates
+ if ($this->getFailureRate() > 0.1) { // 10% failure rate
+ Notification::route('slack', config('monitoring.slack'))
+ ->notify(new HighFailureRateAlert());
+ }
+ }
+}
+```
+
+### Caching Strategy
+
+Implement custom caching logic:
+
+```php
+class CustomCacheListener
+{
+ public function handleSuccessfulLookup(CompanyLookupSucceeded $event): void
+ {
+ $company = $event->company;
+
+ // Cache for different durations based on company status
+ $ttl = $company->registration->active ? 3600 : 7200; // 1h vs 2h
+
+ Cache::put(
+ "company:{$company->ic}:formatted",
+ $this->formatCompany($company),
+ $ttl
+ );
+ }
+
+ public function handleFailedLookup(CompanyLookupFailed $event): void
+ {
+ // Cache negative lookups to prevent repeated API calls
+ if ($event->status === 404) {
+ Cache::put("company:{$event->ic}:not_found", true, 1800); // 30 minutes
+ }
+ }
+}
+```
+
+### Audit Logging
+
+Maintain audit trails of all lookups:
+
+```php
+class AresAuditLogger
+{
+ public function handleSuccessfulLookup(CompanyLookupSucceeded $event): void
+ {
+ AuditLog::create([
+ 'action' => 'company_lookup',
+ 'status' => 'success',
+ 'ic' => $event->company->ic,
+ 'company_name' => $event->company->name,
+ 'user_id' => auth()->id(),
+ 'ip_address' => request()->ip(),
+ 'user_agent' => request()->userAgent(),
+ 'occurred_at' => now(),
+ ]);
+ }
+
+ public function handleFailedLookup(CompanyLookupFailed $event): void
+ {
+ AuditLog::create([
+ 'action' => 'company_lookup',
+ 'status' => 'failed',
+ 'ic' => $event->ic,
+ 'error_code' => $event->status,
+ 'error_message' => $event->exception?->getMessage(),
+ 'user_id' => auth()->id(),
+ 'ip_address' => request()->ip(),
+ 'user_agent' => request()->userAgent(),
+ 'occurred_at' => now(),
+ ]);
+ }
+}
+```
+
+## Performance Considerations
+
+### Event Queueing
+
+For high-traffic applications, queue event handlers:
+
+```php
+class QueueableAresListener implements ShouldQueue
+{
+ use InteractsWithQueue;
+
+ public function handle(CompanyLookupSucceeded $event): void
+ {
+ // Heavy processing here
+ $this->processCompanyData($event->company);
+ }
+
+ public function failed(CompanyLookupSucceeded $event, Throwable $exception): void
+ {
+ Log::error("Event processing failed", [
+ 'ic' => $event->company->ic,
+ 'exception' => $exception->getMessage(),
+ ]);
+ }
+}
+```
+
+### Conditional Event Handling
+
+Only process events under certain conditions:
+
+```php
+class ConditionalAresListener
+{
+ public function handle(CompanyLookupSucceeded $event): void
+ {
+ // Only process certain legal forms
+ if (!in_array($event->company->registration->legalForm, ['s.r.o.', 'a.s.'])) {
+ return;
+ }
+
+ // Only process active companies
+ if (!$event->company->registration->active) {
+ return;
+ }
+
+ $this->processCompany($event->company);
+ }
+}
+```
+
+### Batch Processing
+
+Collect events and process them in batches:
+
+```php
+class BatchAresProcessor
+{
+ private array $companies = [];
+
+ public function handle(CompanyLookupSucceeded $event): void
+ {
+ $this->companies[] = $event->company;
+
+ if (count($this->companies) >= 100) {
+ $this->processBatch();
+ }
+ }
+
+ private function processBatch(): void
+ {
+ DB::transaction(function () {
+ foreach ($this->companies as $company) {
+ // Batch insert/update
+ }
+ });
+
+ $this->companies = [];
+ }
+}
+```
+
+## Testing Event Handlers
+
+### Unit Testing
+
+```php
+class AresEventListenerTest extends TestCase
+{
+ public function test_successful_lookup_event(): void
+ {
+ Event::fake();
+
+ $company = CompanyData::fromApiResponse($this->getSampleData());
+ event(new CompanyLookupSucceeded($company));
+
+ Event::assertDispatched(CompanyLookupSucceeded::class, function ($event) use ($company) {
+ return $event->company->ic === $company->ic;
+ });
+ }
+
+ public function test_failed_lookup_event(): void
+ {
+ Event::fake();
+
+ event(new CompanyLookupFailed('12345678', 404));
+
+ Event::assertDispatched(CompanyLookupFailed::class, function ($event) {
+ return $event->ic === '12345678' && $event->status === 404;
+ });
+ }
+}
+```
+
+### Feature Testing
+
+```php
+class AresIntegrationTest extends TestCase
+{
+ public function test_event_fires_on_successful_lookup(): void
+ {
+ Event::fake();
+
+ // Perform lookup
+ $company = Ares::findCompany('12345678');
+
+ if ($company) {
+ Event::assertDispatched(CompanyLookupSucceeded::class);
+ }
+ }
+}
+```
+
+---
+
+*Previous: [Configuration](configuration.md) | Next: [FAQ](faq.md)*
diff --git a/docs/faq.md b/docs/faq.md
new file mode 100644
index 0000000..d4f14cf
--- /dev/null
+++ b/docs/faq.md
@@ -0,0 +1,387 @@
+# Frequently Asked Questions
+
+This document answers common questions about the Laravel ARES package.
+
+## General Questions
+
+### What is ARES?
+
+ARES (Administrativní registr ekonomických subjektů) is the Czech Republic's business register containing information about all registered companies, including their identification numbers (IC), addresses, legal forms, and registration status.
+
+### What Laravel versions are supported?
+
+The package supports Laravel 10.0, 11.0, 12.0, and 13.0.
+
+### What PHP version is required?
+
+PHP 8.2 or higher is required.
+
+### Is this package free?
+
+Yes, the package is open-source and licensed under the MIT license.
+
+## Installation and Setup
+
+### How do I install the package?
+
+```bash
+composer require nyoncode/laravel-ares
+```
+
+### Do I need to publish configuration files?
+
+No, the package works out of the box with sensible defaults. However, you can publish the configuration file to customize settings:
+
+```bash
+php artisan vendor:publish --tag="ares-config"
+```
+
+### The helper functions are not available. What should I do?
+
+Run `composer dump-autoload` to ensure the helper files are properly autoloaded:
+
+```bash
+composer dump-autoload
+```
+
+### How do I register the service provider?
+
+The package uses Laravel's auto-discovery, so the service provider is registered automatically. If you're not using auto-discovery, add it to `config/app.php`:
+
+```php
+'providers' => [
+ // ...
+ NyonCode\Ares\Providers\AresServiceProvider::class,
+],
+```
+
+## Usage
+
+### How do I find a company by IC?
+
+```php
+use NyonCode\Ares\Facades\Ares;
+
+$company = Ares::findCompany('12345678');
+if ($company) {
+ echo $company->name;
+}
+```
+
+### How do I use the helper functions?
+
+```php
+// Check if company is active
+if (ares_is_company_active('12345678')) {
+ echo "Company is active";
+}
+
+// Get company address
+$address = ares_get_address('12345678');
+
+// Validate IC format
+if (ares_validate_ic('12345678')) {
+ echo "Valid IC";
+}
+```
+
+### What's the difference between `findCompany()` and `findCompanyOrFail()`?
+
+- `findCompany()` returns `null` if the company is not found
+- `findCompanyOrFail()` throws a `CompanyNotFoundException` if the company is not found
+
+```php
+// Safe approach
+$company = Ares::findCompany('12345678');
+if ($company) {
+ // Use company
+}
+
+// Exception handling
+try {
+ $company = Ares::findCompanyOrFail('12345678');
+ // Use company
+} catch (CompanyNotFoundException $e) {
+ // Handle not found
+}
+```
+
+### How do I get raw API response data?
+
+```php
+$rawData = Ares::findCompanyRaw('12345678');
+if ($rawData) {
+ echo $rawData['obchodniJmeno']; // Raw API field
+}
+```
+
+### How do I validate an IC without making an API call?
+
+```php
+if (ares_validate_ic('12345678')) {
+ echo "IC format is valid";
+}
+```
+
+## Data and Fields
+
+### What information is available for a company?
+
+The package provides access to:
+
+- **Basic Info**: Name, IC, DIC (VAT number)
+- **Registration**: Active status, establishment date, legal form, financial office
+- **Address**: Registered office and delivery address
+- **Raw Data**: Complete API response
+
+### How do I check if a company is active?
+
+```php
+$company = Ares::findCompany('12345678');
+if ($company && $company->registration->active) {
+ echo "Company is active";
+}
+
+// Or using helper
+if (ares_is_company_active('12345678')) {
+ echo "Company is active";
+}
+```
+
+### How do I get the company's address?
+
+```php
+$company = Ares::findCompany('12345678');
+if ($company && $company->registeredOffice) {
+ echo $company->registeredOffice->formatted;
+}
+
+// Or using helper
+$address = ares_get_address('12345678');
+```
+
+### What does the `dic` field contain?
+
+The `dic` field contains the VAT identification number (DIČ) if the company has one. It may be null if the company doesn't have VAT registration.
+
+## Caching
+
+### How does caching work?
+
+The package automatically caches API responses based on your `cache_ttl` configuration. Subsequent requests for the same IC within the cache period will return cached data.
+
+### How do I clear the cache for a specific company?
+
+```php
+Ares::forgetCompany('12345678');
+```
+
+### How do I disable caching?
+
+Set `cache_ttl` to `0` in your configuration:
+
+```env
+ARES_CACHE_TTL=0
+```
+
+### How long should I set the cache TTL?
+
+- **Development**: 60 seconds (1 minute)
+- **Production**: 3600 seconds (1 hour)
+- **High Traffic**: 86400 seconds (24 hours)
+- **Real-time Data**: 0 (disabled)
+
+## Errors and Troubleshooting
+
+### I'm getting a "Company not found" error. What does this mean?
+
+This means the IC number either doesn't exist in the ARES database or is invalid. Use `ares_validate_ic()` to check if the format is correct.
+
+### Why am I getting timeout errors?
+
+The ARES API might be slow or unavailable. Try increasing the timeout values:
+
+```env
+ARES_HTTP_TIMEOUT=10.0
+ARES_HTTP_CONNECT_TIMEOUT=5.0
+```
+
+### How do I debug API issues?
+
+1. Enable debug logging:
+ ```env
+ ARES_LOG_LEVEL=debug
+ ARES_LOG_CHANNEL=stack
+ ```
+
+2. Check the logs:
+ ```bash
+ tail -f storage/logs/laravel.log
+ ```
+
+3. Test the API directly:
+ ```bash
+ curl -v https://ares.gov.cz/ekonomicke-subjekty-v-be/rest/ekonomicke-subjekty/12345678
+ ```
+
+### What does "Invalid IC format" mean?
+
+The IC number doesn't match the required 8-digit format or has an invalid checksum. Use `ares_validate_ic()` to check the format.
+
+## Performance
+
+### How can I improve performance?
+
+1. **Enable Caching**: Set appropriate `cache_ttl`
+2. **Use Redis**: Configure Redis for better cache performance
+3. **Batch Operations**: Use `validateMultipleIcs()` for multiple lookups
+4. **Optimize Timeouts**: Adjust HTTP timeouts based on your network
+
+### Is there a rate limit?
+
+The ARES API doesn't have an official rate limit, but it's good practice to:
+- Cache results appropriately
+- Avoid excessive requests
+- Implement retry logic for failures
+
+### How do I handle multiple IC lookups efficiently?
+
+```php
+$ics = ['12345678', '87654321', '11223344'];
+$results = ares('validateMultipleIcs', $ics);
+
+foreach ($results as $ic => $company) {
+ if ($company) {
+ echo "Found: {$company->name}";
+ }
+}
+```
+
+## Integration
+
+### How do I integrate with my existing database?
+
+Use events to sync ARES data with your database:
+
+```php
+// In your EventServiceProvider
+protected $listen = [
+ CompanyLookupSucceeded::class => [
+ SyncCompanyToLocalDatabase::class,
+ ],
+];
+```
+
+### Can I use this in API endpoints?
+
+Yes, the package works well in API controllers:
+
+```php
+public function show(string $ic): JsonResponse
+{
+ try {
+ $company = Ares::findCompanyOrFail($ic);
+ return response()->json($company);
+ } catch (CompanyNotFoundException $e) {
+ return response()->json(['error' => 'Company not found'], 404);
+ }
+}
+```
+
+### How do I use this in queue jobs?
+
+Inject the ARES client into your job:
+
+```php
+class ProcessCompanyLookup implements ShouldQueue
+{
+ public function __construct(
+ private string $ic
+ ) {}
+
+ public function handle(AresClientInterface $ares): void
+ {
+ $company = $ares->findCompany($this->ic);
+ // Process company data
+ }
+}
+```
+
+## Testing
+
+### How do I test ARES functionality?
+
+Use the provided test command:
+
+```bash
+php artisan ares:test 12345678
+```
+
+### How do I mock ARES in tests?
+
+```php
+use NyonCode\Ares\Contracts\AresClientInterface;
+
+class CompanyTest extends TestCase
+{
+ public function test_company_lookup(): void
+ {
+ $mock = $this->mock(AresClientInterface::class);
+ $mock->shouldReceive('findCompany')
+ ->with('12345678')
+ ->andReturn($this->createMockCompany());
+
+ // Test your code
+ }
+}
+```
+
+### Should I use real IC numbers in tests?
+
+No, use mock data or test IC numbers provided in the documentation. Avoid using real company data in automated tests.
+
+## Security and Privacy
+
+### Is the data from ARES public?
+
+Yes, all data from the ARES register is public information. However, be mindful of:
+- Data retention policies
+- GDPR compliance for EU users
+- Rate limiting and fair use
+
+### Should I store ARES data in my database?
+
+Yes, you can store public ARES data, but:
+- Keep it updated (companies can change status)
+- Consider data freshness requirements
+- Implement proper data retention policies
+
+### How do I handle sensitive data?
+
+The package only accesses public company information. No sensitive personal data is retrieved from ARES.
+
+## Support and Contributing
+
+### Where can I get help?
+
+- **GitHub Issues**: Report bugs and request features
+- **Documentation**: Check these docs first
+- **Laravel Community**: Ask in Laravel forums and communities
+
+### How do I contribute?
+
+1. Fork the repository
+2. Create a feature branch
+3. Write tests for your changes
+4. Submit a pull request
+
+### What's the roadmap?
+
+- Enhanced filtering and search capabilities
+- Additional data sources
+- Performance optimizations
+- More helper functions
+
+---
+
+*Previous: [Events](events.md) | Back to [README](README.md)*
diff --git a/docs/helpers.md b/docs/helpers.md
new file mode 100644
index 0000000..09a2a5a
--- /dev/null
+++ b/docs/helpers.md
@@ -0,0 +1,717 @@
+# Helper Functions
+
+This document covers all helper functions available in the Laravel ARES package, including global functions and the AresHelper class.
+
+## Global Helper Functions
+
+The package provides global helper functions that work like Laravel's built-in helpers (`app()`, `auth()`, etc.).
+
+### Main ares() Function
+
+The primary helper function that provides access to all ARES functionality.
+
+#### Usage
+
+```php
+// Get the fluent builder / client proxy instance
+$ares = ares();
+$company = $ares->findCompany('12345678');
+
+// Call helper methods dynamically
+$result = ares('methodName', ...$args);
+```
+
+#### Available Methods
+
+| Method | Parameters | Returns | Description |
+|--------|------------|---------|-------------|
+| `client` | none | `AresClientInterface` | Get the ARES client instance |
+| `isCompanyActiveByIc` | `string $ic` | `bool` | Check if company is active |
+| `getAddressByIc` | `string $ic` | `string` | Get formatted address |
+| `getLegalFormByIc` | `string $ic` | `string` | Get legal form |
+| `hasVatNumberByIc` | `string $ic` | `bool` | Check if company has VAT |
+| `getEstablishmentDateByIc` | `string $ic, string $format = 'Y-m-d'` | `string` | Get establishment date |
+| `formatCompanyByIc` | `string $ic` | `array` | Get formatted company data |
+| `validateIcFormat` | `string $ic` | `bool` | Validate IC format |
+| `normalizeIcFormat` | `string $ic` | `string` | Normalize IC format |
+| `isCompanyActive` | `CompanyData $company` | `bool` | Check if company object is active |
+| `getFullAddress` | `CompanyData $company` | `string` | Get formatted address from object |
+| `getLegalForm` | `CompanyData $company` | `string` | Get legal form from object |
+| `hasVatNumber` | `CompanyData $company` | `bool` | Check VAT from object |
+| `getEstablishmentDate` | `CompanyData $company, string $format = 'Y-m-d'` | `string` | Get date from object |
+| `formatCompanyForDisplay` | `CompanyData $company` | `array` | Format object for display |
+| `validateMultipleIcs` | `array $ics` | `array` | Validate multiple ICs |
+| `filterByLegalForm` | `array $companies, string $legalForm` | `array` | Filter by legal form |
+| `filterActiveCompanies` | `array $companies` | `array` | Filter active companies |
+| `getCompanyStatistics` | `array $companies` | `array` | Get statistics |
+| `searchByName` | `array $companies, string $searchTerm, bool $caseSensitive = false` | `array` | Search by name |
+
+#### Examples
+
+```php
+// Get the proxy and use client methods directly
+$company = ares()->findCompany('12345678');
+
+// Use dynamic method calls
+$isActive = ares('isCompanyActiveByIc', '12345678');
+$address = ares('getAddressByIc', '12345678');
+$date = ares('getEstablishmentDateByIc', '12345678', 'd.m.Y');
+
+// Handle invalid method
+try {
+ ares('nonExistentMethod', '12345678');
+} catch (InvalidArgumentException $e) {
+ echo $e->getMessage(); // "Method [nonExistentMethod] does not exist on AresHelper."
+}
+```
+
+### Dedicated Global Functions
+
+These functions provide convenient shortcuts for common operations.
+
+#### ares_is_company_active(string $ic): bool
+
+Check if a company is active by its IC.
+
+```php
+if (ares_is_company_active('12345678')) {
+ echo "Company is active and exists";
+}
+```
+
+#### ares_get_address(string $ic): string
+
+Get company's formatted address.
+
+```php
+$address = ares_get_address('12345678');
+echo $address; // "Street 123, City, 123 45" or "N/A"
+```
+
+#### ares_has_vat(string $ic): bool
+
+Check if company has VAT registration.
+
+```php
+if (ares_has_vat('12345678')) {
+ echo "Company has VAT number";
+}
+```
+
+#### ares_get_legal_form(string $ic): string
+
+Get company's legal form.
+
+```php
+$legalForm = ares_get_legal_form('12345678');
+echo $legalForm; // "s.r.o." or "N/A"
+```
+
+#### ares_get_establishment_date(string $ic, string $format = 'Y-m-d'): string
+
+Get company's establishment date.
+
+```php
+$date = ares_get_establishment_date('12345678', 'd.m.Y');
+echo $date; // "01.01.2020" or "N/A"
+```
+
+#### ares_format_company(string $ic): array
+
+Get formatted company data in one call.
+
+```php
+$display = ares_format_company('12345678');
+```
+
+#### ares_get_company_statistics(array $ics): array
+
+Get statistics for multiple companies by IC.
+
+```php
+$stats = ares_get_company_statistics(['12345678', '87654321']);
+```
+
+#### ares_validate_ic(string $ic): bool
+
+Validate IC format and checksum.
+
+```php
+if (ares_validate_ic('12345678')) {
+ echo "IC format is valid";
+}
+```
+
+#### ares_normalize_ic(string $ic): string
+
+Normalize IC to 8-digit format.
+
+```php
+$normalized = ares_normalize_ic('123 456 78');
+echo $normalized; // '12345678'
+```
+
+## AresHelper Class
+
+The `AresHelper` class contains all utility methods used by the global helpers. You can also use it directly.
+
+### Company Status Methods
+
+#### isCompanyActive(CompanyData $company): bool
+
+Check if a company is active based on registration status.
+
+```php
+use NyonCode\Ares\Helpers\AresHelper;
+use NyonCode\Ares\Facades\Ares;
+
+$company = Ares::findCompany('12345678');
+if ($company && AresHelper::isCompanyActive($company)) {
+ echo "Company is active";
+}
+```
+
+#### isCompanyActiveByIc(string $ic): bool
+
+Find company and check if it's active in one call.
+
+```php
+if (AresHelper::isCompanyActiveByIc('12345678')) {
+ echo "Company exists and is active";
+}
+```
+
+### Address Methods
+
+#### getFullAddress(CompanyData $company): string
+
+Get the full formatted address of a company.
+
+```php
+$address = AresHelper::getFullAddress($company);
+echo $address; // "Street 123, City, 123 45" or "N/A"
+```
+
+#### getAddressByIc(string $ic): string
+
+Find company and get its formatted address in one call.
+
+```php
+$address = AresHelper::getAddressByIc('12345678');
+```
+
+### Company Information Methods
+
+#### getLegalForm(CompanyData $company): string
+
+Get the company's legal form.
+
+```php
+$legalForm = AresHelper::getLegalForm($company);
+echo $legalForm; // "s.r.o.", "a.s.", etc. or "N/A"
+```
+
+#### getLegalFormByIc(string $ic): string
+
+Find company and get its legal form in one call.
+
+```php
+$legalForm = AresHelper::getLegalFormByIc('12345678');
+```
+
+#### hasVatNumber(CompanyData $company): bool
+
+Check if a company has a VAT number.
+
+```php
+if (AresHelper::hasVatNumber($company)) {
+ echo "VAT number: " . $company->dic;
+}
+```
+
+#### hasVatNumberByIc(string $ic): bool
+
+Find company and check if it has VAT number in one call.
+
+```php
+if (AresHelper::hasVatNumberByIc('12345678')) {
+ echo "Company has VAT registration";
+}
+```
+
+#### getEstablishmentDate(CompanyData $company, string $format = 'Y-m-d'): string
+
+Get the company's establishment date in a formatted way.
+
+```php
+$date = AresHelper::getEstablishmentDate($company, 'd.m.Y');
+echo $date; // "01.01.2020" or "N/A"
+```
+
+#### getEstablishmentDateByIc(string $ic, string $format = 'Y-m-d'): string
+
+Find company and get its establishment date in one call.
+
+```php
+$date = AresHelper::getEstablishmentDateByIc('12345678', 'F j, Y');
+echo $date; // "January 1, 2020"
+```
+
+### Data Processing Methods
+
+#### validateMultipleIcs(array $ics): array
+
+Validate multiple IC numbers and return the valid ones with their data.
+
+```php
+$ics = ['12345678', '87654321', 'invalid'];
+$results = AresHelper::validateMultipleIcs($ics);
+
+foreach ($results as $ic => $company) {
+ if ($company) {
+ echo "IC {$ic}: {$company->name}";
+ } else {
+ echo "IC {$ic}: Not found or invalid";
+ }
+}
+```
+
+#### filterByLegalForm(array $companies, string $legalForm): array
+
+Filter companies by legal form.
+
+```php
+$companies = [/* array of CompanyData objects */];
+$sroCompanies = AresHelper::filterByLegalForm($companies, 's.r.o.');
+```
+
+#### filterActiveCompanies(array $companies): array
+
+Filter active companies from an array.
+
+```php
+$activeCompanies = AresHelper::filterActiveCompanies($companies);
+```
+
+#### searchByName(array $companies, string $searchTerm, bool $caseSensitive = false): array
+
+Search companies by name in an array.
+
+```php
+$results = AresHelper::searchByName($companies, 'Název');
+$results = AresHelper::searchByName($companies, 'NAZEV', true); // case sensitive
+```
+
+### Statistics Methods
+
+#### getCompanyStatistics(array $companies): array
+
+Get comprehensive statistics about companies.
+
+```php
+$stats = AresHelper::getCompanyStatistics($companies);
+
+echo "Total: " . $stats['total'];
+echo "Active: " . $stats['active'];
+echo "Inactive: " . $stats['inactive'];
+echo "With VAT: " . $stats['with_vat'];
+echo "Active percentage: " . $stats['active_percentage'] . '%';
+echo "VAT percentage: " . $stats['vat_percentage'] . '%';
+```
+
+**Returns:**
+```php
+[
+ 'total' => int, // Total number of companies
+ 'active' => int, // Number of active companies
+ 'inactive' => int, // Number of inactive companies
+ 'with_vat' => int, // Companies with VAT number
+ 'without_vat' => int, // Companies without VAT number
+ 'active_percentage' => float, // Percentage of active companies
+ 'vat_percentage' => float, // Percentage of companies with VAT
+]
+```
+
+### Display Methods
+
+#### formatCompanyForDisplay(CompanyData $company): array
+
+Format company information as an array for display purposes.
+
+```php
+$display = AresHelper::formatCompanyForDisplay($company);
+
+// $display contains:
+[
+ 'IC' => '12345678',
+ 'Name' => 'Company Name',
+ 'DIC' => 'CZ12345678',
+ 'Status' => 'Active',
+ 'Legal Form' => 's.r.o.',
+ 'Establishment Date' => '2020-01-01',
+ 'Address' => 'Street 123, City, 123 45',
+ 'Financial Office' => 'Finanční úřad',
+ 'Primary Source' => 'RES',
+]
+```
+
+#### formatCompanyByIc(string $ic): array
+
+Find company and format it for display in one call.
+
+```php
+$display = AresHelper::formatCompanyByIc('12345678');
+// Returns formatted array or empty array if not found
+```
+
+### Validation Methods
+
+#### validateIcFormat(string $ic): bool
+
+Validate IC format without making API call.
+
+```php
+if (AresHelper::validateIcFormat('12345678')) {
+ echo "IC format is valid";
+}
+```
+
+#### normalizeIcFormat(string $ic): string
+
+Normalize IC format without making API call.
+
+```php
+$normalized = AresHelper::normalizeIcFormat('123 456 78');
+echo $normalized; // '12345678'
+```
+
+## Usage Patterns
+
+### Dependency Injection
+
+You can inject the AresHelper into your classes:
+
+```php
+use NyonCode\Ares\Helpers\AresHelper;
+
+class CompanyService
+{
+ public function __construct(
+ private AresHelper $aresHelper
+ ) {}
+
+ public function getActiveCompanies(array $ics): array
+ {
+ $results = $this->aresHelper->validateMultipleIcs($ics);
+ return $this->aresHelper->filterActiveCompanies($results);
+ }
+}
+```
+
+### Service Container Access
+
+Access the helper through the service container:
+
+```php
+$helper = app('ares.helper');
+$stats = $helper->getCompanyStatistics($companies);
+```
+
+### chaining Operations
+
+Combine multiple helper methods for complex operations:
+
+```php
+// Get statistics for active companies with VAT
+$companies = [/* array of companies */];
+$activeCompanies = AresHelper::filterActiveCompanies($companies);
+$stats = AresHelper::getCompanyStatistics($activeCompanies);
+
+// Search for specific legal form among active companies
+$sroCompanies = AresHelper::filterByLegalForm($companies, 's.r.o.');
+$activeSroCompanies = AresHelper::filterActiveCompanies($sroCompanies);
+```
+
+## Performance Considerations
+
+### Caching
+
+Helper methods that call `findCompany()` benefit from automatic caching:
+- `isCompanyActiveByIc()`
+- `getAddressByIc()`
+- `getLegalFormByIc()`
+- `hasVatNumberByIc()`
+- `getEstablishmentDateByIc()`
+- `formatCompanyByIc()`
+
+### Memory Usage
+
+- Methods that work with existing `CompanyData` objects are memory efficient
+- `validateMultipleIcs()` processes ICs in batches
+- Large arrays are handled efficiently in filtering methods
+
+### Error Handling
+
+All helper methods follow consistent error handling:
+- Invalid ICs return `null` or default values
+- Missing data returns 'N/A' for strings
+- Array methods return empty arrays for invalid input
+
+## Fluent API
+
+The package includes a powerful fluent API that allows method chaining for elegant queries.
+
+### Getting Started
+
+```php
+// Direct fluent API - most elegant way
+$companies = ares()
+ ->findMany(['12345678', '87654321'])
+ ->active()
+ ->withVat()
+ ->limit(10)
+ ->get();
+
+// Single company lookup
+$company = ares()
+ ->find('12345678')
+ ->active()
+ ->firstOrFail();
+```
+
+### Common Fluent Operations
+
+#### Finding Companies
+```php
+// Single company
+$company = ares()->find('12345678')->firstOrFail();
+
+// Multiple companies
+$companies = ares()->findMany($ics)->get();
+
+// With exception handling
+$company = ares()->findOrFail('12345678')->firstOrFail();
+```
+
+#### Filtering
+```php
+// Active companies only
+$active = ares()->findMany($ics)->active()->get();
+
+// By legal form
+$sroCompanies = ares()->findMany($ics)->legalForm('s.r.o.')->get();
+
+// With VAT number
+$withVat = ares()->findMany($ics)->withVat()->get();
+
+// Search by name
+$results = ares()->findMany($ics)->search('Technology')->get();
+```
+
+#### Pagination
+```php
+// Limit results
+$companies = ares()->findMany($ics)->limit(5)->get();
+
+// Offset and limit
+$companies = ares()->findMany($ics)->offset(10)->limit(5)->get();
+
+// Get first result
+$company = ares()->findMany($ics)->active()->first()->firstOrFail();
+```
+
+#### Data Extraction
+```php
+// Get formatted arrays
+$formatted = ares()->findMany($ics)->active()->getFormatted();
+
+// Get names only
+$names = ares()->findMany($ics)->active()->names();
+
+// Get addresses only
+$addresses = ares()->findMany($ics)->active()->addresses();
+
+// Key by IC
+$keyed = ares()->findMany($ics)->active()->keyByIc();
+```
+
+#### Checking Results
+```php
+// Check if exists
+if (ares()->findMany($ics)->active()->exists()) {
+ echo "Found active companies";
+}
+
+// Count results
+$count = ares()->findMany($ics)->active()->count();
+
+// Check if empty
+if (ares()->findMany($ics)->active()->isEmpty()) {
+ echo "No active companies";
+}
+```
+
+#### Statistics
+```php
+$stats = ares()->findMany($ics)->stats();
+echo "Active: {$stats['active']} ({$stats['active_percentage']}%)";
+```
+
+### Advanced Usage
+
+#### Complex Filtering
+```php
+$companies = ares()
+ ->findMany($ics)
+ ->active()
+ ->withVat()
+ ->legalForm('s.r.o.')
+ ->search('Technology')
+ ->limit(20)
+ ->getFormatted();
+```
+
+#### Cache Management
+```php
+// Clear cache and get fresh data
+$company = ares()
+ ->find('12345678')
+ ->forget()
+ ->firstOrFail();
+```
+
+#### Reset Builder
+```php
+$builder = ares();
+$results1 = $builder->findMany($ics1)->active()->get();
+$builder->reset();
+$results2 = $builder->findMany($ics2)->withVat()->get();
+```
+
+### Fluent API Methods
+
+| Method | Description | Example |
+|--------|-------------|---------|
+| `find($ic)` | Find single company | `->find('12345678')` |
+| `findMany($ics)` | Find multiple companies | `->findMany($ics)` |
+| `findOrFail($ic)` | Find or throw exception | `->findOrFail('12345678')` |
+| `active()` | Filter active companies | `->active()` |
+| `inactive()` | Filter inactive companies | `->inactive()` |
+| `legalForm($form)` | Filter by legal form | `->legalForm('s.r.o.')` |
+| `withVat()` | Filter companies with VAT | `->withVat()` |
+| `withoutVat()` | Filter without VAT | `->withoutVat()` |
+| `search($term)` | Search by name | `->search('Technology')` |
+| `limit($n)` | Limit results | `->limit(10)` |
+| `offset($n)` | Skip results | `->offset(5)` |
+| `first()` | Get first result | `->first()` |
+| `get()` | Get CompanyData objects | `->get()` |
+| `getFormatted()` | Get formatted arrays | `->getFormatted()` |
+| `firstOrFail()` | Get first or null | `->firstOrFail()` |
+| `company()` | Get single company | `->company()` |
+| `exists()` | Check if results exist | `->exists()` |
+| `isEmpty()` | Check if empty | `->isEmpty()` |
+| `count()` | Count results | `->count()` |
+| `names()` | Get names array | `->names()` |
+| `ics()` | Get ICs array | `->ics()` |
+| `addresses()` | Get addresses array | `->addresses()` |
+| `keyByIc()` | Get IC-keyed array | `->keyByIc()` |
+| `keyByIcFormatted()` | Get formatted IC-keyed | `->keyByIcFormatted()` |
+| `stats()` | Get statistics | `->stats()` |
+| `forget()` | Clear cache | `->forget()` |
+| `reset()` | Reset builder | `->reset()` |
+
+## Practical Examples & Best Practices
+
+### Common Patterns
+
+#### Find Active Companies with VAT
+```php
+$activeCompaniesWithVat = ares()
+ ->findMany(['12345678', '87654321', '11223344'])
+ ->active()
+ ->withVat()
+ ->getFormatted();
+
+foreach ($activeCompaniesWithVat as $company) {
+ echo "{$company['Name']} - {$company['Address']} (VAT: {$company['DIC']})\n";
+}
+```
+
+#### Search and Limit Results
+```php
+$limitedSroCompanies = ares()
+ ->findMany($ics)
+ ->legalForm('s.r.o.')
+ ->active()
+ ->limit(5)
+ ->get();
+```
+
+#### Data Extraction
+```php
+// Get just names
+$names = ares()
+ ->findMany($ics)
+ ->active()
+ ->names();
+
+// Get key-value pairs by IC
+$keyed = ares()
+ ->findMany($ics)
+ ->active()
+ ->keyByIcFormatted();
+```
+
+#### Statistics
+```php
+$stats = ares()
+ ->findMany($ics)
+ ->stats();
+
+echo "Total: {$stats['total']}, Active: {$stats['active']} ({$stats['active_percentage']}%)";
+```
+
+### Performance Tips
+
+#### Filter Early
+```php
+// Good: Filter early to reduce dataset
+$companies = ares()
+ ->findMany($ics)
+ ->active() // Filter early
+ ->withVat() // Then apply more filters
+ ->limit(10) // Finally limit results
+ ->get();
+```
+
+#### Use Pagination for Large Datasets
+```php
+$page = 2;
+$perPage = 10;
+
+$companies = ares()
+ ->findMany($ics)
+ ->active()
+ ->offset(($page - 1) * $perPage)
+ ->limit($perPage)
+ ->getFormatted();
+```
+
+### Error Handling
+
+```php
+// Invalid IC returns empty results
+$companies = ares()
+ ->findMany(['invalid', '12345678'])
+ ->active()
+ ->get();
+
+// Check if results exist
+if (ares()->find('12345678')->active()->exists()) {
+ echo "Company is active";
+}
+```
+
+---
+
+*Previous: [API Reference](api.md) | Next: [Configuration](configuration.md)*
diff --git a/docs/installation.md b/docs/installation.md
new file mode 100644
index 0000000..8b69e14
--- /dev/null
+++ b/docs/installation.md
@@ -0,0 +1,199 @@
+# Installation Guide
+
+This guide will walk you through installing and setting up the Laravel ARES package in your Laravel application.
+
+## Requirements
+
+Before installing, ensure your system meets the following requirements:
+
+- **PHP**: 8.2 or higher
+- **Laravel**: 10.0, 11.0, 12.0, or 13.0
+- **Composer**: Latest stable version
+- **Guzzle**: 7.0 or higher (usually included with Laravel)
+
+## Installation
+
+### 1. Install via Composer
+
+Install the package using Composer:
+
+```bash
+composer require nyoncode/laravel-ares
+```
+
+### 2. Publish Configuration (Optional)
+
+Publish the configuration file to customize the package settings:
+
+```bash
+php artisan vendor:publish --tag="ares-config"
+```
+
+This will create a `config/ares.php` file in your application.
+
+### 3. Register Service Provider
+
+The package uses Laravel's package discovery, so the service provider is automatically registered. If you're not using package discovery, add it manually to your `config/app.php`:
+
+```php
+'providers' => [
+ // ... other providers
+ NyonCode\Ares\Providers\AresServiceProvider::class,
+],
+```
+
+### 4. Register Facade (Optional)
+
+If you want to use the Ares facade, add it to your `config/app.php` aliases:
+
+```php
+'aliases' => [
+ // ... other aliases
+ 'Ares' => NyonCode\Ares\Facades\Ares::class,
+],
+```
+
+## Configuration
+
+After publishing the configuration file, you can customize the settings in `config/ares.php`:
+
+```php
+ env('ARES_API_URL', 'https://ares.gov.cz/ekonomicke-subjekty-v-be/rest'),
+ 'cache_ttl' => env('ARES_CACHE_TTL', 86400), // 24 hours
+ 'log_channel' => env('ARES_LOG_CHANNEL', 'stack'),
+ 'http_options' => [
+ 'timeout' => env('ARES_HTTP_TIMEOUT', 5.0),
+ 'connect_timeout' => env('ARES_HTTP_CONNECT_TIMEOUT', 3.0),
+ ],
+];
+```
+
+### Environment Variables
+
+Add the following environment variables to your `.env` file:
+
+```env
+# ARES Configuration
+ARES_API_URL=https://ares.gov.cz/ekonomicke-subjekty-v-be/rest
+ARES_CACHE_TTL=86400
+ARES_LOG_CHANNEL=stack
+ARES_HTTP_TIMEOUT=5.0
+ARES_HTTP_CONNECT_TIMEOUT=3.0
+```
+
+## Verification
+
+### 1. Test the Installation
+
+You can test the installation using the provided Artisan command:
+
+```bash
+php artisan ares:test 12345678
+```
+
+Replace `12345678` with a valid Czech company identification number.
+
+### 2. Check Helper Functions
+
+The global helper functions should be available after installation:
+
+```php
+// In any Laravel route, controller, or service:
+if (function_exists('ares')) {
+ echo "ARES helper is available!";
+}
+```
+
+### 3. Verify Service Container
+
+Check if the service is properly registered in the Laravel container:
+
+```php
+// In a route or controller:
+$ares = app(\NyonCode\Ares\Contracts\AresClientInterface::class);
+// Should return an instance of AresClient
+```
+
+## Troubleshooting
+
+### Common Issues
+
+#### 1. Helper Functions Not Available
+
+If the global helper functions are not available, run:
+
+```bash
+composer dump-autoload
+```
+
+This ensures the helper files are properly autoloaded.
+
+#### 2. Configuration Not Found
+
+If you get configuration errors, make sure to publish the config file:
+
+```bash
+php artisan vendor:publish --tag="ares-config"
+```
+
+#### 3. Cache Issues
+
+If you're experiencing caching problems, clear your application cache:
+
+```bash
+php artisan cache:clear
+php artisan config:clear
+```
+
+#### 4. HTTP Connection Issues
+
+If you're experiencing connection timeouts, adjust the HTTP timeout settings in your `.env` file:
+
+```env
+ARES_HTTP_TIMEOUT=10.0
+ARES_HTTP_CONNECT_TIMEOUT=5.0
+```
+
+### Debug Mode
+
+To enable debug mode for troubleshooting, you can temporarily modify your configuration:
+
+```php
+'log_channel' => 'stack', // Use verbose logging
+'cache_ttl' => 60, // Short cache for testing
+```
+
+## Upgrade Guide
+
+### From Previous Versions
+
+When upgrading to a new version, always:
+
+1. **Backup your configuration**: Copy your `config/ares.php` file
+2. **Update dependencies**: Run `composer update nyoncode/laravel-ares`
+3. **Check for breaking changes**: Review the release notes
+4. **Test your application**: Ensure all functionality works as expected
+
+### Version Compatibility
+
+| Package Version | Laravel Version | PHP Version |
+|----------------|----------------|-------------|
+| 1.x | 10.x | 8.2+ |
+| 2.x | 10.x, 11.x | 8.2+ |
+| 3.x | 10.x, 11.x, 12.x | 8.2+ |
+
+## Next Steps
+
+After successful installation:
+
+1. Read the [Usage Examples](usage.md) to learn how to use the package
+2. Check the [Helper Functions](helpers.md) documentation for available helpers
+3. Review the [API Reference](api.md) for detailed method documentation
+4. Configure [Events](events.md) if you need to handle lookup events
+
+---
+
+*Previous: [Overview](README.md) | Next: [Usage Examples](usage.md)*
diff --git a/docs/usage.md b/docs/usage.md
new file mode 100644
index 0000000..169d44a
--- /dev/null
+++ b/docs/usage.md
@@ -0,0 +1,382 @@
+# Usage Examples
+
+This guide provides comprehensive examples of how to use the Laravel ARES package in your application.
+
+## Basic Usage
+
+### Finding a Company
+
+The most common operation is finding a company by its identification number (IC):
+
+```php
+use NyonCode\Ares\Facades\Ares;
+
+// Find company (returns null if not found)
+$company = Ares::findCompany('12345678');
+
+if ($company) {
+ echo $company->name; // Company name
+ echo $company->ic; // Identification number
+ echo $company->dic; // VAT number (if available)
+}
+```
+
+### Using Helper Functions
+
+Global helper functions provide convenient shortcuts:
+
+```php
+// Get the ARES client
+$client = ares();
+
+// Check if company exists and is active
+if (ares_is_company_active('12345678')) {
+ echo "Company is active!";
+}
+
+// Get company address
+$address = ares_get_address('12345678');
+echo $address; // Formatted address or 'N/A'
+
+// Check if company has VAT number
+if (ares_has_vat('12345678')) {
+ echo "Company has VAT registration";
+}
+
+// Validate IC format
+if (ares_validate_ic('12345678')) {
+ echo "IC format is valid";
+}
+
+// Normalize IC format
+$normalized = ares_normalize_ic('123 456 78');
+echo $normalized; // '12345678'
+```
+
+### Dependency Injection
+
+You can inject the ARES client into your classes:
+
+```php
+use NyonCode\Ares\Contracts\AresClientInterface;
+
+class CompanyService
+{
+ public function __construct(
+ private AresClientInterface $ares
+ ) {}
+
+ public function getCompanyInfo(string $ic): ?array
+ {
+ $company = $this->ares->findCompany($ic);
+
+ if (!$company) {
+ return null;
+ }
+
+ return [
+ 'name' => $company->name,
+ 'ic' => $company->ic,
+ 'address' => $company->registeredOffice?->formatted,
+ 'active' => $company->registration->active,
+ ];
+ }
+}
+```
+
+## Advanced Usage
+
+### Exception Handling
+
+Use `findCompanyOrFail()` for automatic exception handling:
+
+```php
+use NyonCode\Ares\Facades\Ares;
+use NyonCode\Ares\Exceptions\CompanyNotFoundException;
+use NyonCode\Ares\Exceptions\InvalidIcException;
+
+try {
+ $company = Ares::findCompanyOrFail('12345678');
+ echo $company->name;
+} catch (InvalidIcException $e) {
+ echo "Invalid IC format: " . $e->getMessage();
+} catch (CompanyNotFoundException $e) {
+ echo "Company not found: " . $e->getMessage();
+}
+```
+
+### Raw API Data
+
+Access the raw API response data:
+
+```php
+$rawData = Ares::findCompanyRaw('12345678');
+
+if ($rawData) {
+ // Access raw API fields
+ echo $rawData['obchodniJmeno'] ?? 'N/A';
+ echo $rawData['ico'] ?? 'N/A';
+}
+```
+
+### Working with Company Data
+
+The `CompanyData` object provides structured access to company information:
+
+```php
+$company = Ares::findCompany('12345678');
+
+if ($company) {
+ // Basic information
+ echo $company->name;
+ echo $company->ic;
+ echo $company->dic;
+
+ // Registration information
+ echo $company->registration->active ? 'Active' : 'Inactive';
+ echo $company->registration->dateOfEstablishment;
+ echo $company->registration->legalForm;
+ echo $company->registration->financialOffice;
+
+ // Address information
+ echo $company->registeredOffice->street;
+ echo $company->registeredOffice->city;
+ echo $company->registeredOffice->zipCode;
+ echo $company->registeredOffice->formatted; // Full formatted address
+
+ // Delivery address (if available)
+ if ($company->deliveryAddress) {
+ echo $company->deliveryAddress->formatted;
+ }
+}
+```
+
+## Batch Operations
+
+### Validating Multiple ICs
+
+```php
+$ics = ['12345678', '87654321', '11223344'];
+
+$results = ares('validateMultipleIcs', $ics);
+
+foreach ($results as $ic => $company) {
+ if ($company) {
+ echo "IC {$ic}: {$company->name}";
+ } else {
+ echo "IC {$ic}: Not found or invalid";
+ }
+}
+```
+
+### Filtering Companies
+
+```php
+// Assume you have an array of companies
+$companies = [
+ Ares::findCompany('12345678'),
+ Ares::findCompany('87654321'),
+ // ... more companies
+];
+
+// Filter active companies only
+$activeCompanies = ares('filterActiveCompanies', $companies);
+
+// Filter by legal form
+$companiesWithSpecificForm = ares('filterByLegalForm', $companies, 's.r.o.');
+
+// Search companies by name
+$searchResults = ares('searchByName', $companies, 'Název firmy');
+```
+
+### Company Statistics
+
+```php
+$companies = [
+ Ares::findCompany('12345678'),
+ Ares::findCompany('87654321'),
+ // ... more companies
+];
+
+$stats = ares('getCompanyStatistics', $companies);
+
+echo "Total companies: " . $stats['total'];
+echo "Active companies: " . $stats['active'];
+echo "Inactive companies: " . $stats['inactive'];
+echo "With VAT: " . $stats['with_vat'];
+echo "Active percentage: " . $stats['active_percentage'] . '%';
+```
+
+## Display Formatting
+
+### Format Company for Display
+
+```php
+$company = Ares::findCompany('12345678');
+
+$displayData = ares('formatCompanyByIc', '12345678');
+
+// Or format existing company object
+$displayData = ares('formatCompanyForDisplay', $company);
+
+// $displayData contains:
+// [
+// 'IC' => '12345678',
+// 'Name' => 'Company Name',
+// 'DIC' => 'CZ12345678',
+// 'Status' => 'Active',
+// 'Legal Form' => 's.r.o.',
+// 'Establishment Date' => '2020-01-01',
+// 'Address' => 'Street 123, City, 123 45',
+// 'Financial Office' => 'Finanční úřad',
+// 'Primary Source' => 'RES'
+// ]
+```
+
+### Custom Date Formatting
+
+```php
+// Get establishment date in custom format
+$date = ares('getEstablishmentDateByIc', '12345678', 'd.m.Y');
+echo $date; // '01.01.2020'
+
+// Or from company object
+$date = ares('getEstablishmentDate', $company, 'F j, Y');
+echo $date; // 'January 1, 2020'
+```
+
+## Controller Examples
+
+### API Controller
+
+```php
+namespace App\Http\Controllers;
+
+use NyonCode\Ares\Facades\Ares;
+use Illuminate\Http\Request;
+use Illuminate\Http\JsonResponse;
+
+class CompanyApiController extends Controller
+{
+ public function show(string $ic): JsonResponse
+ {
+ try {
+ $company = Ares::findCompanyOrFail($ic);
+
+ return response()->json([
+ 'success' => true,
+ 'data' => ares('formatCompanyForDisplay', $company)
+ ]);
+ } catch (InvalidIcException $e) {
+ return response()->json([
+ 'success' => false,
+ 'error' => 'Invalid IC format'
+ ], 400);
+ } catch (CompanyNotFoundException $e) {
+ return response()->json([
+ 'success' => false,
+ 'error' => 'Company not found'
+ ], 404);
+ }
+ }
+
+ public function validate(Request $request): JsonResponse
+ {
+ $request->validate(['ic' => 'required|string']);
+
+ $isValid = ares_validate_ic($request->ic);
+ $normalized = ares_normalize_ic($request->ic);
+
+ return response()->json([
+ 'valid' => $isValid,
+ 'normalized' => $normalized
+ ]);
+ }
+}
+```
+
+### Web Controller
+
+```php
+namespace App\Http\Controllers;
+
+use NyonCode\Ares\Facades\Ares;
+use Illuminate\Http\Request;
+
+class CompanyController extends Controller
+{
+ public function search(Request $request)
+ {
+ $request->validate(['ic' => 'required|string']);
+
+ $company = Ares::findCompany($request->ic);
+
+ if (!$company) {
+ return back()->with('error', 'Company not found');
+ }
+
+ return view('company.show', [
+ 'company' => $company,
+ 'isActive' => ares_is_company_active($request->ic),
+ 'address' => ares_get_address($request->ic)
+ ]);
+ }
+}
+```
+
+## Artisan Command Usage
+
+### Testing ARES Connection
+
+```bash
+# Test with a specific IC
+php artisan ares:test 12345678
+
+# The command will display:
+# Company found:
+# +-------------------------+---------------------------+
+# | Property | Value |
+# +-------------------------+---------------------------+
+# | IC | 12345678 |
+# | Name | Company Name |
+# | DIC | CZ12345678 |
+# | Primary Source | RES |
+# | Date of Establishment | 2020-01-01 |
+# | Financial Office | Finanční úřad |
+# | Address | Street 123, City, 123 45 |
+# | Delivery Address | N/A |
+# | Legal Form | s.r.o. |
+# | Business Register File | N/A |
+# +-------------------------+---------------------------+
+```
+
+## Caching
+
+The package automatically caches results to reduce API calls. You can control caching behavior:
+
+### Clear Company Cache
+
+```php
+// Clear specific company from cache
+Ares::forgetCompany('12345678');
+
+// Or using helper
+ares()->forgetCompany('12345678');
+```
+
+### Cache TTL Configuration
+
+Set cache duration in your configuration:
+
+```php
+// config/ares.php
+return [
+ 'cache_ttl' => 3600, // 1 hour
+ // or
+ 'cache_ttl' => 86400, // 24 hours
+];
+```
+
+---
+
+*Previous: [Installation Guide](installation.md) | Next: [Helper Functions](helpers.md)*
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
index 2bd0059..5fa7f1e 100644
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -9,6 +9,9 @@
tests/Unit
+
+ tests/Integration
+
tests/Feature
diff --git a/src/Commands/TestAresCommand.php b/src/Commands/TestAresCommand.php
index 776c8ae..31e758c 100644
--- a/src/Commands/TestAresCommand.php
+++ b/src/Commands/TestAresCommand.php
@@ -13,6 +13,11 @@ final class TestAresCommand extends Command
protected $description = 'Test ARES API communication for the given IC';
+ /**
+ * Execute the console command.
+ *
+ * @return int Command exit code
+ */
public function handle(): int
{
$ic = $this->icArgument();
@@ -44,6 +49,11 @@ public function handle(): int
return self::SUCCESS;
}
+ /**
+ * Get the IC argument from the command.
+ *
+ * @return string The IC identification number
+ */
private function icArgument(): string
{
$value = $this->argument('ic');
diff --git a/src/Exceptions/CompanyNotFoundException.php b/src/Exceptions/CompanyNotFoundException.php
index 30929dd..0f0c92c 100644
--- a/src/Exceptions/CompanyNotFoundException.php
+++ b/src/Exceptions/CompanyNotFoundException.php
@@ -8,8 +8,13 @@
final class CompanyNotFoundException extends RuntimeException
{
+ /**
+ * Create a new exception for a company not found in ARES.
+ *
+ * @param string $ic The identification number of the company that was not found
+ */
public static function forIc(string $ic): self
{
- return new self("Company with ICO [{$ic}] was not found in ARES.");
+ return new self("Company with IC [{$ic}] was not found in ARES.");
}
}
diff --git a/src/Exceptions/InvalidApiResponseException.php b/src/Exceptions/InvalidApiResponseException.php
index b280437..3e657dc 100644
--- a/src/Exceptions/InvalidApiResponseException.php
+++ b/src/Exceptions/InvalidApiResponseException.php
@@ -8,11 +8,19 @@
final class InvalidApiResponseException extends RuntimeException
{
+ /**
+ * Create a new exception for a missing required field in ARES response.
+ *
+ * @param string $field The name of the missing field
+ */
public static function missingRequiredField(string $field): self
{
return new self("ARES response is missing required field [{$field}].");
}
+ /**
+ * Create a new exception for an invalid payload type in ARES response.
+ */
public static function invalidPayloadType(): self
{
return new self('ARES response payload must be an array.');
diff --git a/src/Exceptions/InvalidIcException.php b/src/Exceptions/InvalidIcException.php
index 2a7e967..4c90383 100644
--- a/src/Exceptions/InvalidIcException.php
+++ b/src/Exceptions/InvalidIcException.php
@@ -8,8 +8,13 @@
final class InvalidIcException extends InvalidArgumentException
{
+ /**
+ * Create a new exception for an invalid IC.
+ *
+ * @param string $ic The invalid identification number
+ */
public static function forIc(string $ic): self
{
- return new self("Invalid ICO [{$ic}].");
+ return new self("Invalid IC format: {$ic}");
}
}
diff --git a/src/Helpers/AresFluentBuilder.php b/src/Helpers/AresFluentBuilder.php
new file mode 100644
index 0000000..c2bf7d1
--- /dev/null
+++ b/src/Helpers/AresFluentBuilder.php
@@ -0,0 +1,454 @@
+|null
+ */
+ private ?array $companies = null;
+
+ /**
+ * @var array|null
+ */
+ private ?array $results = null;
+
+ public function __construct(private readonly AresClientInterface $client) {}
+
+ /**
+ * @param list $arguments
+ */
+ public function __call(string $method, array $arguments): mixed
+ {
+ if (method_exists($this->client, $method)) {
+ return $this->client->{$method}(...$arguments);
+ }
+
+ if (method_exists(AresHelper::class, $method)) {
+ return AresHelper::{$method}(...$arguments);
+ }
+
+ throw new BadMethodCallException(sprintf('Method [%s] does not exist on AresFluentBuilder.', $method));
+ }
+
+ /**
+ * Find a company by IC.
+ *
+ * @param string $ic The company identification number
+ */
+ public function find(string $ic): self
+ {
+ $this->reset();
+ $this->company = $this->client->findCompany($ic);
+
+ return $this;
+ }
+
+ /**
+ * Find multiple companies by ICs.
+ *
+ * @param array $ics Array of identification numbers
+ */
+ public function findMany(array $ics): self
+ {
+ $this->reset();
+ $this->results = [];
+
+ foreach ($ics as $ic) {
+ $this->results[$ic] = $this->client->findCompany($ic);
+ }
+
+ $this->companies = $this->resolvedCompaniesFromResults();
+
+ return $this;
+ }
+
+ /**
+ * Find company or throw exception.
+ *
+ * @param string $ic The company identification number
+ */
+ public function findOrFail(string $ic): self
+ {
+ $this->reset();
+ $this->company = $this->client->findCompanyOrFail($ic);
+
+ return $this;
+ }
+
+ /**
+ * Filter to active companies only.
+ */
+ public function active(): self
+ {
+ if ($this->company !== null) {
+ if (! AresHelper::isCompanyActive($this->company)) {
+ $this->company = null;
+ }
+ } elseif ($this->companies !== null) {
+ $this->companies = array_values(AresHelper::filterActiveCompanies($this->companies));
+ }
+
+ return $this;
+ }
+
+ /**
+ * Filter to inactive companies only.
+ */
+ public function inactive(): self
+ {
+ if ($this->company !== null) {
+ if (AresHelper::isCompanyActive($this->company)) {
+ $this->company = null;
+ }
+ } elseif ($this->companies !== null) {
+ $this->companies = array_values(array_filter(
+ $this->companies,
+ static fn (CompanyData $company): bool => ! AresHelper::isCompanyActive($company)
+ ));
+ }
+
+ return $this;
+ }
+
+ /**
+ * Filter by legal form.
+ *
+ * @param string $legalForm The legal form to filter by
+ */
+ public function legalForm(string $legalForm): self
+ {
+ if ($this->company !== null) {
+ if (AresHelper::getLegalForm($this->company) !== $legalForm) {
+ $this->company = null;
+ }
+ } elseif ($this->companies !== null) {
+ $this->companies = array_values(AresHelper::filterByLegalForm($this->companies, $legalForm));
+ }
+
+ return $this;
+ }
+
+ /**
+ * Filter to companies with VAT numbers only.
+ */
+ public function withVat(): self
+ {
+ if ($this->company !== null) {
+ if (! AresHelper::hasVatNumber($this->company)) {
+ $this->company = null;
+ }
+ } elseif ($this->companies !== null) {
+ $this->companies = array_values(array_filter(
+ $this->companies,
+ static fn (CompanyData $company): bool => AresHelper::hasVatNumber($company)
+ ));
+ }
+
+ return $this;
+ }
+
+ /**
+ * Filter to companies without VAT numbers only.
+ */
+ public function withoutVat(): self
+ {
+ if ($this->company !== null) {
+ if (AresHelper::hasVatNumber($this->company)) {
+ $this->company = null;
+ }
+ } elseif ($this->companies !== null) {
+ $this->companies = array_values(array_filter(
+ $this->companies,
+ static fn (CompanyData $company): bool => ! AresHelper::hasVatNumber($company)
+ ));
+ }
+
+ return $this;
+ }
+
+ /**
+ * Search companies by name.
+ *
+ * @param string $searchTerm The search term
+ * @param bool $caseSensitive Whether search should be case sensitive
+ */
+ public function search(string $searchTerm, bool $caseSensitive = false): self
+ {
+ if ($this->companies !== null) {
+ $this->companies = array_values(AresHelper::searchByName($this->companies, $searchTerm, $caseSensitive));
+ }
+
+ return $this;
+ }
+
+ /**
+ * Limit the number of results.
+ *
+ * @param int $limit Maximum number of results
+ */
+ public function limit(int $limit): self
+ {
+ if ($this->companies !== null) {
+ $this->companies = array_slice($this->companies, 0, $limit);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Skip a number of results.
+ *
+ * @param int $offset Number of results to skip
+ */
+ public function offset(int $offset): self
+ {
+ if ($this->companies !== null) {
+ $this->companies = array_slice($this->companies, $offset);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Get the first result.
+ */
+ public function first(): self
+ {
+ if ($this->companies !== null) {
+ $firstCompany = reset($this->companies);
+ $this->company = $firstCompany instanceof CompanyData ? $firstCompany : null;
+ $this->companies = null;
+ }
+
+ return $this;
+ }
+
+ /**
+ * Get results as array of CompanyData objects.
+ *
+ * @return array
+ */
+ public function get(): array
+ {
+ if ($this->company !== null) {
+ return [$this->company];
+ }
+
+ return $this->companies ?? [];
+ }
+
+ /**
+ * Get results as formatted display arrays.
+ *
+ * @return array>
+ */
+ public function getFormatted(): array
+ {
+ $companies = $this->get();
+
+ return array_map(function (CompanyData $company) {
+ return AresHelper::formatCompanyForDisplay($company);
+ }, $companies);
+ }
+
+ /**
+ * Get the first company or null.
+ */
+ public function firstOrFail(): ?CompanyData
+ {
+ $companies = $this->get();
+
+ return $companies[0] ?? null;
+ }
+
+ /**
+ * Get the company (for single company operations).
+ */
+ public function company(): ?CompanyData
+ {
+ return $this->company;
+ }
+
+ /**
+ * Check if any results exist.
+ */
+ public function exists(): bool
+ {
+ if ($this->company !== null) {
+ return true;
+ }
+
+ return ! empty($this->companies);
+ }
+
+ /**
+ * Check if no results exist.
+ */
+ public function isEmpty(): bool
+ {
+ return ! $this->exists();
+ }
+
+ /**
+ * Count the number of results.
+ */
+ public function count(): int
+ {
+ if ($this->company !== null) {
+ return 1;
+ }
+
+ return count($this->companies ?? []);
+ }
+
+ /**
+ * Get statistics for the current results.
+ *
+ * @return array
+ */
+ public function stats(): array
+ {
+ $companies = $this->get();
+
+ return AresHelper::getCompanyStatistics($companies);
+ }
+
+ /**
+ * Get company names as array.
+ *
+ * @return array
+ */
+ public function names(): array
+ {
+ $companies = $this->get();
+
+ return array_map(function (CompanyData $company) {
+ return $company->name;
+ }, $companies);
+ }
+
+ /**
+ * Get company ICs as array.
+ *
+ * @return array
+ */
+ public function ics(): array
+ {
+ $companies = $this->get();
+
+ return array_map(function (CompanyData $company) {
+ return $company->ic;
+ }, $companies);
+ }
+
+ /**
+ * Get company addresses as array.
+ *
+ * @return array
+ */
+ public function addresses(): array
+ {
+ $companies = $this->get();
+
+ return array_map(function (CompanyData $company) {
+ return AresHelper::getFullAddress($company);
+ }, $companies);
+ }
+
+ /**
+ * Get companies as key-value pairs (IC => CompanyData).
+ *
+ * @return array
+ */
+ public function keyByIc(): array
+ {
+ $companies = $this->get();
+
+ $result = [];
+ foreach ($companies as $company) {
+ $result[$company->ic] = $company;
+ }
+
+ return $result;
+ }
+
+ /**
+ * Get companies as key-value pairs (IC => formatted array).
+ *
+ * @return array>
+ */
+ public function keyByIcFormatted(): array
+ {
+ $companies = $this->get();
+
+ $result = [];
+ foreach ($companies as $company) {
+ $result[$company->ic] = AresHelper::formatCompanyForDisplay($company);
+ }
+
+ return $result;
+ }
+
+ /**
+ * Clear cache for current IC(s).
+ */
+ public function forget(): self
+ {
+ if ($this->company !== null) {
+ $this->client->forgetCompany($this->company->ic);
+ } elseif ($this->results !== null) {
+ foreach (array_keys($this->results) as $ic) {
+ $this->client->forgetCompany($ic);
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * Reset the builder state.
+ */
+ public function reset(): self
+ {
+ $this->company = null;
+ $this->companies = null;
+ $this->results = null;
+
+ return $this;
+ }
+
+ /**
+ * @return list
+ */
+ private function resolvedCompaniesFromResults(): array
+ {
+ if ($this->results === null) {
+ return [];
+ }
+
+ $companies = [];
+
+ foreach ($this->results as $company) {
+ if ($company instanceof CompanyData) {
+ $companies[] = $company;
+ }
+ }
+
+ return $companies;
+ }
+
+ public function client(): AresClientInterface
+ {
+ return $this->client;
+ }
+}
diff --git a/src/Helpers/AresHelper.php b/src/Helpers/AresHelper.php
new file mode 100644
index 0000000..be8ebdb
--- /dev/null
+++ b/src/Helpers/AresHelper.php
@@ -0,0 +1,335 @@
+registration->primarySource;
+
+ if ($primarySource !== null) {
+ $primaryStatus = $company->registration->sourceStatus($primarySource)?->status;
+
+ if ($primaryStatus !== null) {
+ return $primaryStatus === RegistrationSourceState::Active;
+ }
+ }
+
+ foreach ($company->registration->sourceStatuses as $sourceStatus) {
+ if ($sourceStatus->status === RegistrationSourceState::Active) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Get the full formatted address of a company.
+ *
+ * @param CompanyData $company The company data
+ * @return string The formatted address or 'N/A' if not available
+ */
+ public static function getFullAddress(CompanyData $company): string
+ {
+ if ($company->registeredOffice === null) {
+ return 'N/A';
+ }
+
+ return $company->registeredOffice->formatted ?? 'N/A';
+ }
+
+ /**
+ * Get the company's legal form in a readable format.
+ *
+ * @param CompanyData $company The company data
+ * @return string The legal form or 'N/A' if not available
+ */
+ public static function getLegalForm(CompanyData $company): string
+ {
+ return $company->registration->legalForm ?? 'N/A';
+ }
+
+ /**
+ * Check if a company has a valid VAT number (DIC).
+ *
+ * @param CompanyData $company The company data
+ * @return bool True if the company has a VAT number, false otherwise
+ */
+ public static function hasVatNumber(CompanyData $company): bool
+ {
+ return ! empty($company->dic);
+ }
+
+ /**
+ * Get the company's establishment date in a formatted way.
+ *
+ * @param CompanyData $company The company data
+ * @param string $format The date format (default: Y-m-d)
+ * @return string The formatted date or 'N/A' if not available
+ */
+ public static function getEstablishmentDate(CompanyData $company, string $format = 'Y-m-d'): string
+ {
+ if ($company->registration->dateOfEstablishment === null) {
+ return 'N/A';
+ }
+
+ try {
+ $date = new \DateTime($company->registration->dateOfEstablishment);
+
+ return $date->format($format);
+ } catch (\Exception) {
+ return 'N/A';
+ }
+ }
+
+ /**
+ * Validate multiple IC numbers and return the valid ones with their data.
+ *
+ * @param array $ics Array of identification numbers
+ * @return array Array with IC as key and CompanyData or null as value
+ */
+ public static function validateMultipleIcs(array $ics): array
+ {
+ $results = [];
+
+ foreach ($ics as $ic) {
+ $results[$ic] = Ares::findCompany($ic);
+ }
+
+ return $results;
+ }
+
+ /**
+ * Filter companies by legal form.
+ *
+ * @param array $companies Array of companies
+ * @param string $legalForm The legal form to filter by
+ * @return array Filtered companies
+ */
+ public static function filterByLegalForm(array $companies, string $legalForm): array
+ {
+ return array_values(array_filter($companies, function (CompanyData $company) use ($legalForm) {
+ return self::getLegalForm($company) === $legalForm;
+ }));
+ }
+
+ /**
+ * Filter active companies from an array.
+ *
+ * @param array $companies Array of companies
+ * @return array Active companies only
+ */
+ public static function filterActiveCompanies(array $companies): array
+ {
+ return array_values(array_filter($companies, function (CompanyData $company) {
+ return self::isCompanyActive($company);
+ }));
+ }
+
+ /**
+ * Get company statistics from an array of companies.
+ *
+ * @param array $companies Array of companies
+ * @return array Statistics about the companies
+ */
+ public static function getCompanyStatistics(array $companies): array
+ {
+ $total = count($companies);
+ $active = count(self::filterActiveCompanies($companies));
+ $withVat = count(array_filter($companies, function (CompanyData $company) {
+ return self::hasVatNumber($company);
+ }));
+
+ return [
+ 'total' => $total,
+ 'active' => $active,
+ 'inactive' => $total - $active,
+ 'with_vat' => $withVat,
+ 'without_vat' => $total - $withVat,
+ 'active_percentage' => $total > 0 ? round(($active / $total) * 100, 2) : 0,
+ 'vat_percentage' => $total > 0 ? round(($withVat / $total) * 100, 2) : 0,
+ ];
+ }
+
+ /**
+ * Format company information as an array for display purposes.
+ *
+ * @param CompanyData $company The company data
+ * @return array Formatted company information
+ */
+ public static function formatCompanyForDisplay(CompanyData $company): array
+ {
+ return [
+ 'IC' => $company->ic,
+ 'Name' => $company->name,
+ 'DIC' => $company->dic ?? 'N/A',
+ 'Status' => self::isCompanyActive($company) ? 'Active' : 'Inactive',
+ 'Legal Form' => self::getLegalForm($company),
+ 'Establishment Date' => self::getEstablishmentDate($company),
+ 'Address' => self::getFullAddress($company),
+ 'Financial Office' => $company->registration->financialOffice ?? 'N/A',
+ 'Primary Source' => $company->registration->primarySource ?? 'N/A',
+ ];
+ }
+
+ /**
+ * Search companies by name in an array.
+ *
+ * @param array $companies Array of companies
+ * @param string $searchTerm The search term
+ * @param bool $caseSensitive Whether the search should be case sensitive
+ * @return array Companies matching the search term
+ */
+ public static function searchByName(array $companies, string $searchTerm, bool $caseSensitive = false): array
+ {
+ $term = $caseSensitive ? $searchTerm : strtolower($searchTerm);
+
+ return array_values(array_filter($companies, function (CompanyData $company) use ($term, $caseSensitive) {
+ $name = $caseSensitive ? $company->name : strtolower($company->name);
+
+ return str_contains($name, $term);
+ }));
+ }
+
+ /**
+ * Resolve the configured ARES client instance.
+ */
+ public static function client(): AresClientInterface
+ {
+ return app(AresClientInterface::class);
+ }
+
+ /**
+ * Find company and check if it's active in one call.
+ *
+ * @param string $ic The company identification number
+ * @return bool True if company exists and is active, false otherwise
+ */
+ public static function isCompanyActiveByIc(string $ic): bool
+ {
+ $company = Ares::findCompany($ic);
+
+ return $company !== null && self::isCompanyActive($company);
+ }
+
+ /**
+ * Find company and get its formatted address in one call.
+ *
+ * @param string $ic The company identification number
+ * @return string The formatted address or 'N/A' if not found
+ */
+ public static function getAddressByIc(string $ic): string
+ {
+ $company = Ares::findCompany($ic);
+
+ return $company !== null ? self::getFullAddress($company) : 'N/A';
+ }
+
+ /**
+ * Find company and get its legal form in one call.
+ *
+ * @param string $ic The company identification number
+ * @return string The legal form or 'N/A' if not found
+ */
+ public static function getLegalFormByIc(string $ic): string
+ {
+ $company = Ares::findCompany($ic);
+
+ return $company !== null ? self::getLegalForm($company) : 'N/A';
+ }
+
+ /**
+ * Find company and check if it has VAT number in one call.
+ *
+ * @param string $ic The company identification number
+ * @return bool True if company exists and has VAT number, false otherwise
+ */
+ public static function hasVatNumberByIc(string $ic): bool
+ {
+ $company = Ares::findCompany($ic);
+
+ return $company !== null && self::hasVatNumber($company);
+ }
+
+ /**
+ * Find company and get its establishment date in one call.
+ *
+ * @param string $ic The company identification number
+ * @param string $format The date format (default: Y-m-d)
+ * @return string The formatted date or 'N/A' if not found
+ */
+ public static function getEstablishmentDateByIc(string $ic, string $format = 'Y-m-d'): string
+ {
+ $company = Ares::findCompany($ic);
+
+ return $company !== null ? self::getEstablishmentDate($company, $format) : 'N/A';
+ }
+
+ /**
+ * Find company and format it for display in one call.
+ *
+ * @param string $ic The company identification number
+ * @return array Formatted company information or empty array if not found
+ */
+ public static function formatCompanyByIc(string $ic): array
+ {
+ $company = Ares::findCompany($ic);
+
+ return $company !== null ? self::formatCompanyForDisplay($company) : [];
+ }
+
+ /**
+ * Validate IC format without making API call.
+ *
+ * @param string $ic The identification number to validate
+ * @return bool True if IC format is valid, false otherwise
+ */
+ public static function validateIcFormat(string $ic): bool
+ {
+ return Ares::isValidIc($ic);
+ }
+
+ /**
+ * Normalize IC format without making API call.
+ *
+ * @param string $ic The identification number to normalize
+ * @return string The normalized 8-digit identification number
+ */
+ public static function normalizeIcFormat(string $ic): string
+ {
+ return Ares::normalizeIc($ic);
+ }
+
+ /**
+ * Create a new fluent API builder instance.
+ */
+ public static function fluent(): AresFluentBuilder
+ {
+ return new AresFluentBuilder(self::client());
+ }
+
+ /**
+ * Create a new fluent API builder instance with custom client.
+ *
+ * @param AresClientInterface $client The ARES client
+ */
+ public static function fluentWithClient(AresClientInterface $client): AresFluentBuilder
+ {
+ return new AresFluentBuilder($client);
+ }
+}
diff --git a/src/Providers/AresServiceProvider.php b/src/Providers/AresServiceProvider.php
index e88f41f..ea1c03e 100644
--- a/src/Providers/AresServiceProvider.php
+++ b/src/Providers/AresServiceProvider.php
@@ -6,12 +6,11 @@
use Exception;
use Illuminate\Contracts\Cache\Factory as CacheFactory;
-use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Contracts\Foundation\Application;
-use Illuminate\Http\Client\Factory as Http;
use Illuminate\Log\LogManager;
use NyonCode\Ares\Commands\TestAresCommand;
use NyonCode\Ares\Contracts\AresClientInterface;
+use NyonCode\Ares\Helpers\AresHelper;
use NyonCode\Ares\Services\AresClient;
use NyonCode\LaravelPackageToolkit\Contracts\Packable;
use NyonCode\LaravelPackageToolkit\Exceptions\InvalidLanguageDirectoryException;
@@ -34,39 +33,46 @@ public function configure(Packager $packager): void
->hasCommands([
TestAresCommand::class,
])
- ->hasTranslations('resources/lang');
- }
-
- public function register(): void
- {
- parent::register();
+ ->hasTranslations('resources/lang')
+ ->registeredPackage(function ($packager) {
+ $this->app->bind(AresClientInterface::class, function (Application $app): AresClient {
+ return new AresClient(
+ baseUrl: $this->configString('ares.api_url'),
+ cacheTtl: $this->configInt('ares.cache_ttl'),
+ logger: $app->make(LogManager::class)->channel($this->configString('ares.log_channel')),
+ cache: $app->make(CacheFactory::class)->store(),
+ httpTimeout: $this->configFloat('ares.http_options.timeout'),
+ httpConnectTimeout: $this->configFloat('ares.http_options.connect_timeout'),
+ );
+ });
- $this->app->singleton(AresClientInterface::class, function (Application $app): AresClient {
- return new AresClient(
- baseUrl: $this->configString('ares.api_url'),
- cacheTtl: $this->configInt('ares.cache_ttl'),
- logger: $app->make(LogManager::class)->channel($this->configString('ares.log_channel')),
- events: $app->make(Dispatcher::class),
- cache: $app->make(CacheFactory::class)->store(),
- http: $app->make(Http::class),
- );
- });
-
- $this->app->bind('ares', fn (Application $app) => $app->make(AresClientInterface::class));
+ $this->app->bind('ares', fn (Application $app) => $app->make(AresClientInterface::class));
+ $this->app->singleton(AresHelper::class, fn () => new AresHelper);
+ $this->app->alias(AresHelper::class, 'ares.helper');
+ });
}
/**
+ * Get package information for the about command.
+ *
* @return array
*/
public function aboutData(): array
{
return [
+ 'Author' => 'Ondřej Nyklíček',
'Client contract' => AresClientInterface::class,
'Facade alias' => 'Ares',
'Cache support' => 'enabled',
];
}
+ /**
+ * Get a string value from configuration.
+ *
+ * @param string $key The configuration key
+ * @return string The configuration value or empty string if not found
+ */
private function configString(string $key): string
{
$value = config($key);
@@ -74,10 +80,29 @@ private function configString(string $key): string
return is_string($value) ? $value : '';
}
+ /**
+ * Get an integer value from configuration.
+ *
+ * @param string $key The configuration key
+ * @return int The configuration value or 0 if not found/invalid
+ */
private function configInt(string $key): int
{
$value = config($key);
return is_int($value) ? $value : (is_numeric($value) ? (int) $value : 0);
}
+
+ /**
+ * Get a float value from configuration.
+ *
+ * @param string $key The configuration key
+ * @return float The configuration value or 0.0 if not found/invalid
+ */
+ private function configFloat(string $key): float
+ {
+ $value = config($key);
+
+ return is_float($value) || is_int($value) ? (float) $value : (is_numeric($value) ? (float) $value : 0.0);
+ }
}
diff --git a/src/Services/AresClient.php b/src/Services/AresClient.php
index 3e0e15c..f0f59ae 100644
--- a/src/Services/AresClient.php
+++ b/src/Services/AresClient.php
@@ -5,8 +5,8 @@
namespace NyonCode\Ares\Services;
use Illuminate\Contracts\Cache\Repository as Cache;
-use Illuminate\Contracts\Events\Dispatcher;
-use Illuminate\Http\Client\Factory as Http;
+use Illuminate\Support\Facades\Event;
+use Illuminate\Support\Facades\Http;
use NyonCode\Ares\Contracts\AresClientInterface;
use NyonCode\Ares\Data\CompanyData;
use NyonCode\Ares\Events\CompanyLookupFailed;
@@ -19,79 +19,120 @@
final class AresClient implements AresClientInterface
{
- private string $baseUrl;
+ private const CACHE_PREFIX = 'ares:v1:company:';
- private int $cacheTtl;
+ private const DEFAULT_HTTP_TIMEOUT = 5.0;
- private LoggerInterface $logger;
+ private const DEFAULT_HTTP_CONNECT_TIMEOUT = 3.0;
- private Dispatcher $events;
-
- private Cache $cache;
+ private string $processedBaseUrl;
+ /**
+ * Create a new ARES client instance.
+ *
+ * @param string $baseUrl The base URL for the ARES API
+ * @param int $cacheTtl The cache time-to-live in seconds
+ * @param LoggerInterface $logger The logger instance
+ * @param Cache $cache The cache repository
+ * @param float $httpTimeout The HTTP request timeout in seconds
+ * @param float $httpConnectTimeout The HTTP connection timeout in seconds
+ */
public function __construct(
- string $baseUrl,
- int $cacheTtl,
- LoggerInterface $logger,
- Dispatcher $events,
- Cache $cache,
- private readonly Http $http,
+ private readonly string $baseUrl,
+ private readonly int $cacheTtl,
+ private readonly LoggerInterface $logger,
+ private readonly Cache $cache,
+ private readonly float $httpTimeout = self::DEFAULT_HTTP_TIMEOUT,
+ private readonly float $httpConnectTimeout = self::DEFAULT_HTTP_CONNECT_TIMEOUT,
) {
- $this->baseUrl = rtrim($baseUrl, '/');
- $this->cacheTtl = $cacheTtl;
- $this->logger = $logger;
- $this->events = $events;
- $this->cache = $cache;
+ $this->processedBaseUrl = rtrim($this->baseUrl, '/');
}
+ /**
+ * Find a company by its identification number.
+ *
+ * @param string $ic The company identification number
+ * @return CompanyData|null The company data or null if not found/invalid
+ */
public function findCompany(string $ic): ?CompanyData
{
- $ic = $this->normalizeIc($ic);
+ $normalizedIc = $this->normalizeIc($ic);
- if (! $this->isValidIc($ic)) {
- $this->logger->warning('Invalid IC format', ['ic' => $ic]);
+ if (! $this->isValidIc($normalizedIc)) {
+ $this->logger->warning('Invalid IC format', ['ic' => $normalizedIc]);
return null;
}
- $cacheKey = $this->cacheKey($ic);
+ $forceRefresh = false;
+
+ for ($attempt = 0; $attempt < 2; $attempt++) {
+ $payloadLookup = $this->findPayload($normalizedIc, $forceRefresh);
+
+ if ($payloadLookup === null) {
+ return null;
+ }
- return $this->cache->remember($cacheKey, $this->cacheTtl, function () use ($ic) {
try {
- $response = $this->http
- ->timeout($this->httpTimeout())
- ->connectTimeout($this->httpConnectTimeout())
- ->get("{$this->baseUrl}/ekonomicke-subjekty/{$ic}");
+ $company = CompanyData::fromApiResponse($payloadLookup['payload']);
+ } catch (Throwable $e) {
+ if ($payloadLookup['from_cache']) {
+ $this->logger->warning('Invalid cached company payload detected, flushing', [
+ 'ic' => $normalizedIc,
+ 'key' => $this->cacheKey($normalizedIc),
+ 'exception' => $e->getMessage(),
+ ]);
- if ($response->failed()) {
- $this->events->dispatch(new CompanyLookupFailed($ic, $response->status()));
+ $this->cache->forget($this->cacheKey($normalizedIc));
+ $forceRefresh = true;
- return null;
+ continue;
}
- $company = CompanyData::fromApiResponse(
- $this->payloadData($response->json())
- );
- $this->events->dispatch(new CompanyLookupSucceeded($company));
-
- return $company;
- } catch (Throwable $e) {
- $this->logger->error('ARES API error', [
- 'ic' => $ic,
- 'exception' => $e->getMessage(),
- ]);
- $this->events->dispatch(new CompanyLookupFailed($ic, 0, $e));
+ $this->reportLookupException($normalizedIc, $e);
+ $this->cache->forget($this->cacheKey($normalizedIc));
return null;
}
- });
+
+ Event::dispatch(new CompanyLookupSucceeded($company));
+
+ return $company;
+ }
+
+ return null;
}
+ /**
+ * Find a company by its identification number and return raw API data.
+ *
+ * @param string $ic The company identification number
+ * @return array|null The raw API response data or null if not found
+ */
public function findCompanyRaw(string $ic): ?array
{
- return $this->findCompany($ic)?->rawData;
+ $normalizedIc = $this->normalizeIc($ic);
+
+ if (! $this->isValidIc($normalizedIc)) {
+ $this->logger->warning('Invalid IC format', ['ic' => $normalizedIc]);
+
+ return null;
+ }
+
+ $payloadLookup = $this->findPayload($normalizedIc);
+
+ return $payloadLookup['payload'] ?? null;
}
+ /**
+ * Find a company by its identification number or throw an exception.
+ *
+ * @param string $ic The company identification number
+ * @return CompanyData The company data
+ *
+ * @throws InvalidIcException When the IC format is invalid
+ * @throws CompanyNotFoundException When the company is not found
+ */
public function findCompanyOrFail(string $ic): CompanyData
{
$normalizedIc = $this->normalizeIc($ic);
@@ -109,6 +150,12 @@ public function findCompanyOrFail(string $ic): CompanyData
return $company;
}
+ /**
+ * Remove a company from the cache.
+ *
+ * @param string $ic The company identification number
+ * @return bool True if the cache entry was removed, false otherwise
+ */
public function forgetCompany(string $ic): bool
{
return $this->cache->forget($this->cacheKey($this->normalizeIc($ic)));
@@ -137,6 +184,12 @@ public function isValidIc(string $ic): bool
return $checksum === (int) $ic[7];
}
+ /**
+ * Normalize the identification number by removing non-digit characters and padding.
+ *
+ * @param string $ic The identification number to normalize
+ * @return string The normalized 8-digit identification number
+ */
public function normalizeIc(string $ic): string
{
return str_pad(preg_replace('/\D/', '', $ic) ?? '', 8, '0', STR_PAD_LEFT);
@@ -144,21 +197,48 @@ public function normalizeIc(string $ic): string
private function cacheKey(string $ic): string
{
- return "ares:company:{$ic}";
+ return self::CACHE_PREFIX.$ic;
}
- private function httpTimeout(): float
+ /**
+ * @return array{payload: array, from_cache: bool}|null
+ */
+ private function findPayload(string $normalizedIc, bool $forceRefresh = false): ?array
{
- $timeout = config('ares.http_options.timeout');
+ $cacheKey = $this->cacheKey($normalizedIc);
- return is_numeric($timeout) ? (float) $timeout : 5.0;
- }
+ if (! $forceRefresh && $this->cache->has($cacheKey)) {
+ $payload = $this->cache->get($cacheKey);
- private function httpConnectTimeout(): float
- {
- $timeout = config('ares.http_options.connect_timeout');
+ if (is_array($payload)) {
+ return [
+ 'payload' => $this->payloadData($payload),
+ 'from_cache' => true,
+ ];
+ }
+
+ $this->logger->warning('Invalid cache payload detected, flushing', [
+ 'key' => $cacheKey,
+ 'type' => gettype($payload),
+ ]);
+
+ $this->cache->forget($cacheKey);
+ }
+
+ $payload = $this->requestPayload($normalizedIc);
- return is_numeric($timeout) ? (float) $timeout : 3.0;
+ if ($payload === null) {
+ return null;
+ }
+
+ if ($this->cacheTtl > 0) {
+ $this->cache->put($cacheKey, $payload, $this->cacheTtl);
+ }
+
+ return [
+ 'payload' => $payload,
+ 'from_cache' => false,
+ ];
}
/**
@@ -170,14 +250,61 @@ private function payloadData(mixed $payload): array
throw InvalidApiResponseException::invalidPayloadType();
}
- $normalized = [];
+ $normalizedPayload = [];
foreach ($payload as $key => $value) {
if (is_string($key)) {
- $normalized[$key] = $value;
+ $normalizedPayload[$key] = $value;
}
}
- return $normalized;
+ return $normalizedPayload;
+ }
+
+ /**
+ * @return array|null
+ */
+ private function requestPayload(string $normalizedIc): ?array
+ {
+ try {
+ $response = Http::withOptions([
+ 'timeout' => $this->httpTimeout,
+ 'connect_timeout' => $this->httpConnectTimeout,
+ ])
+ ->acceptJson()
+ ->get($this->companyUrl($normalizedIc));
+
+ if ($response->failed()) {
+ $this->logger->warning('ARES lookup failed with HTTP status', [
+ 'ic' => $normalizedIc,
+ 'status' => $response->status(),
+ ]);
+
+ Event::dispatch(new CompanyLookupFailed($normalizedIc, $response->status()));
+
+ return null;
+ }
+
+ return $this->payloadData($response->json());
+ } catch (Throwable $e) {
+ $this->reportLookupException($normalizedIc, $e);
+
+ return null;
+ }
+ }
+
+ private function companyUrl(string $normalizedIc): string
+ {
+ return "{$this->processedBaseUrl}/ekonomicke-subjekty/{$normalizedIc}";
+ }
+
+ private function reportLookupException(string $normalizedIc, Throwable $exception): void
+ {
+ $this->logger->error('ARES API error', [
+ 'ic' => $normalizedIc,
+ 'exception' => $exception->getMessage(),
+ ]);
+
+ Event::dispatch(new CompanyLookupFailed($normalizedIc, 0, $exception));
}
}
diff --git a/src/helpers.php b/src/helpers.php
new file mode 100644
index 0000000..efa7647
--- /dev/null
+++ b/src/helpers.php
@@ -0,0 +1,157 @@
+{$method}(...$args);
+ }
+
+ throw new InvalidArgumentException(sprintf('Method [%s] does not exist on AresHelper.', $method));
+ }
+}
+
+if (! function_exists('ares_is_company_active')) {
+ /**
+ * Check if a company is active by its IC.
+ *
+ * @param string $ic The company identification number
+ * @return bool True if company exists and is active, false otherwise
+ */
+ function ares_is_company_active(string $ic): bool
+ {
+ return AresHelper::isCompanyActiveByIc($ic);
+ }
+}
+
+if (! function_exists('ares_get_address')) {
+ /**
+ * Get company address by IC.
+ *
+ * @param string $ic The company identification number
+ * @return string The formatted address or 'N/A' if not found
+ */
+ function ares_get_address(string $ic): string
+ {
+ return AresHelper::getAddressByIc($ic);
+ }
+}
+
+if (! function_exists('ares_has_vat')) {
+ /**
+ * Check if company has VAT number by IC.
+ *
+ * @param string $ic The company identification number
+ * @return bool True if company exists and has VAT number, false otherwise
+ */
+ function ares_has_vat(string $ic): bool
+ {
+ return AresHelper::hasVatNumberByIc($ic);
+ }
+}
+
+if (! function_exists('ares_get_legal_form')) {
+ /**
+ * Get company legal form by IC.
+ *
+ * @param string $ic The company identification number
+ * @return string The legal form or 'N/A' if not found
+ */
+ function ares_get_legal_form(string $ic): string
+ {
+ return AresHelper::getLegalFormByIc($ic);
+ }
+}
+
+if (! function_exists('ares_get_establishment_date')) {
+ /**
+ * Get company establishment date by IC.
+ *
+ * @param string $ic The company identification number
+ * @param string $format The date format
+ * @return string The formatted date or 'N/A' if not found
+ */
+ function ares_get_establishment_date(string $ic, string $format = 'Y-m-d'): string
+ {
+ return AresHelper::getEstablishmentDateByIc($ic, $format);
+ }
+}
+
+if (! function_exists('ares_format_company')) {
+ /**
+ * Get formatted company data by IC.
+ *
+ * @param string $ic The company identification number
+ * @return array Formatted company data
+ */
+ function ares_format_company(string $ic): array
+ {
+ return AresHelper::formatCompanyByIc($ic);
+ }
+}
+
+if (! function_exists('ares_get_company_statistics')) {
+ /**
+ * Get company statistics for a list of IC values.
+ *
+ * @param array $ics The company identification numbers
+ * @return array Statistics for found companies
+ */
+ function ares_get_company_statistics(array $ics): array
+ {
+ $results = AresHelper::validateMultipleIcs($ics);
+ $companies = array_values(array_filter(
+ $results,
+ static fn (mixed $company): bool => $company instanceof CompanyData
+ ));
+
+ return AresHelper::getCompanyStatistics($companies);
+ }
+}
+
+if (! function_exists('ares_validate_ic')) {
+ /**
+ * Validate IC format.
+ *
+ * @param string $ic The identification number to validate
+ * @return bool True if IC format is valid, false otherwise
+ */
+ function ares_validate_ic(string $ic): bool
+ {
+ return AresHelper::validateIcFormat($ic);
+ }
+}
+
+if (! function_exists('ares_normalize_ic')) {
+ /**
+ * Normalize IC format.
+ *
+ * @param string $ic The identification number to normalize
+ * @return string The normalized 8-digit identification number
+ */
+ function ares_normalize_ic(string $ic): string
+ {
+ return AresHelper::normalizeIcFormat($ic);
+ }
+}
diff --git a/tests/Fakes/FakeAresClient.php b/tests/Fakes/FakeAresClient.php
new file mode 100644
index 0000000..c4ae829
--- /dev/null
+++ b/tests/Fakes/FakeAresClient.php
@@ -0,0 +1,80 @@
+
+ */
+ public array $companiesByIc = [];
+
+ /**
+ * @var list
+ */
+ public array $findCalls = [];
+
+ /**
+ * @var list
+ */
+ public array $normalizeCalls = [];
+
+ /**
+ * @var array
+ */
+ public array $normalizeMap = [];
+
+ /**
+ * @var list
+ */
+ public array $forgottenIcs = [];
+
+ public function findCompany(string $ic): ?CompanyData
+ {
+ $this->findCalls[] = $ic;
+
+ return $this->companiesByIc[$ic] ?? null;
+ }
+
+ public function findCompanyRaw(string $ic): ?array
+ {
+ return null;
+ }
+
+ public function findCompanyOrFail(string $ic): CompanyData
+ {
+ $company = $this->findCompany($ic);
+
+ if ($company === null) {
+ throw new RuntimeException("Company [$ic] not found.");
+ }
+
+ return $company;
+ }
+
+ public function forgetCompany(string $ic): bool
+ {
+ $this->forgottenIcs[] = $ic;
+ unset($this->companiesByIc[$ic]);
+
+ return true;
+ }
+
+ public function isValidIc(string $ic): bool
+ {
+ return $ic !== '';
+ }
+
+ public function normalizeIc(string $ic): string
+ {
+ $this->normalizeCalls[] = $ic;
+
+ return $this->normalizeMap[$ic] ?? preg_replace('/\s+/', '', $ic) ?? $ic;
+ }
+}
diff --git a/tests/Feature/AresClientTest.php b/tests/Feature/AresClientTest.php
index e74a6d0..43a3ee9 100644
--- a/tests/Feature/AresClientTest.php
+++ b/tests/Feature/AresClientTest.php
@@ -3,6 +3,7 @@
declare(strict_types=1);
use Illuminate\Http\Client\ConnectionException;
+use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Http;
use NyonCode\Ares\Contracts\AresClientInterface;
@@ -169,7 +170,7 @@
it('throws a domain exception for invalid ic in the fail-fast method', function () {
expect(fn () => app(AresClientInterface::class)->findCompanyOrFail('123'))
- ->toThrow(InvalidIcException::class, 'Invalid ICO [00000123].');
+ ->toThrow(InvalidIcException::class, 'Invalid IC format: 00000123');
});
it('throws a domain exception when a company is not found in the fail-fast method', function () {
@@ -178,7 +179,7 @@
]);
expect(fn () => app(AresClientInterface::class)->findCompanyOrFail('27074358'))
- ->toThrow(CompanyNotFoundException::class, 'Company with ICO [27074358] was not found in ARES.');
+ ->toThrow(CompanyNotFoundException::class, 'Company with IC [27074358] was not found in ARES.');
});
it('handles transport exceptions and dispatches a failed event', function () {
@@ -241,3 +242,82 @@
expect($client->normalizeIc('27 074 358'))->toBe('27074358')
->and($client->normalizeIc('123'))->toBe('00000123');
});
+
+it('recovers from a corrupted cached payload by flushing and refetching it', function () {
+ $requestCount = 0;
+
+ Cache::put('ares:v1:company:27074358', 'broken-payload', 3600);
+
+ Http::fake(function () use (&$requestCount) {
+ $requestCount++;
+
+ return Http::response([
+ 'ico' => '27074358',
+ 'obchodniJmeno' => 'Asseco Central Europe, a.s.',
+ 'sidlo' => [
+ 'nazevObce' => 'Praha',
+ ],
+ ]);
+ });
+
+ $company = app(AresClientInterface::class)->findCompany('27074358');
+
+ expect($company?->ic)->toBe('27074358')
+ ->and($requestCount)->toBe(1)
+ ->and(Cache::get('ares:v1:company:27074358'))->toBeArray();
+});
+
+it('recovers from a malformed cached array payload by refetching a fresh response', function () {
+ $requestCount = 0;
+
+ Cache::put('ares:v1:company:27074358', [
+ 'ico' => '27074358',
+ ], 3600);
+
+ Http::fake(function () use (&$requestCount) {
+ $requestCount++;
+
+ return Http::response([
+ 'ico' => '27074358',
+ 'obchodniJmeno' => 'Asseco Central Europe, a.s.',
+ 'sidlo' => [
+ 'nazevObce' => 'Praha',
+ ],
+ ]);
+ });
+
+ $company = app(AresClientInterface::class)->findCompany('27074358');
+
+ expect($company?->ic)->toBe('27074358')
+ ->and($company?->name)->toBe('Asseco Central Europe, a.s.')
+ ->and($requestCount)->toBe(1)
+ ->and(Cache::get('ares:v1:company:27074358'))->toMatchArray([
+ 'ico' => '27074358',
+ 'obchodniJmeno' => 'Asseco Central Europe, a.s.',
+ ]);
+});
+
+it('serves raw payload lookups from cache without making an extra request', function () {
+ $requestCount = 0;
+
+ Http::fake(function () use (&$requestCount) {
+ $requestCount++;
+
+ return Http::response([
+ 'ico' => '27074358',
+ 'obchodniJmeno' => 'Asseco Central Europe, a.s.',
+ 'dic' => 'CZ27074358',
+ ]);
+ });
+
+ $client = app(AresClientInterface::class);
+
+ $client->findCompany('27074358');
+ $raw = $client->findCompanyRaw('27074358');
+
+ expect($raw)->toMatchArray([
+ 'ico' => '27074358',
+ 'obchodniJmeno' => 'Asseco Central Europe, a.s.',
+ 'dic' => 'CZ27074358',
+ ])->and($requestCount)->toBe(1);
+});
diff --git a/tests/Integration/HelperFunctionTest.php b/tests/Integration/HelperFunctionTest.php
new file mode 100644
index 0000000..1023263
--- /dev/null
+++ b/tests/Integration/HelperFunctionTest.php
@@ -0,0 +1,270 @@
+fakeAresClient = new FakeAresClient;
+ $this->activeCompany = makeCompany();
+
+ app()->instance(AresClientInterface::class, $this->fakeAresClient);
+});
+
+it('returns a fluent builder when ares is called without arguments', function () {
+ expect(ares())->toBeInstanceOf(AresFluentBuilder::class);
+});
+
+it('returns the configured client when ares client is requested', function () {
+ expect(ares('client'))->toBe($this->fakeAresClient);
+});
+
+it('dispatches helper methods through the ares helper', function () {
+ expect(ares('isCompanyActive', $this->activeCompany))->toBeTrue()
+ ->and(ares('getLegalForm', $this->activeCompany))->toBe('s.r.o.');
+});
+
+it('dispatches client methods through the ares helper', function () {
+ $this->fakeAresClient->normalizeMap['123 456 78'] = '12345678';
+
+ expect(ares('normalizeIc', '123 456 78'))->toBe('12345678')
+ ->and($this->fakeAresClient->normalizeCalls)->toBe(['123 456 78']);
+});
+
+it('throws for unknown helper methods', function () {
+ expect(fn () => ares('invalidMethod'))
+ ->toThrow(InvalidArgumentException::class, 'Method [invalidMethod] does not exist on AresHelper.');
+});
+
+it('proxies direct client calls from the fluent builder', function () {
+ $this->fakeAresClient->companiesByIc['12345678'] = $this->activeCompany;
+
+ expect(ares()->findCompany('12345678'))->toBe($this->activeCompany)
+ ->and($this->fakeAresClient->findCalls)->toBe(['12345678']);
+});
+
+it('supports fluent single company lookups', function () {
+ $this->fakeAresClient->companiesByIc['12345678'] = $this->activeCompany;
+
+ $companies = ares()
+ ->find('12345678')
+ ->active()
+ ->get();
+
+ expect($companies)->toHaveCount(1)
+ ->and($companies[0])->toBe($this->activeCompany)
+ ->and($this->fakeAresClient->findCalls)->toBe(['12345678']);
+});
+
+it('supports fluent multi company filtering and formatting', function () {
+ $this->fakeAresClient->companiesByIc = [
+ '12345678' => $this->activeCompany,
+ '87654321' => makeCompany(
+ ic: '87654321',
+ name: 'Tech Solutions s.r.o.',
+ dic: 'CZ87654321',
+ legalForm: 's.r.o.',
+ ),
+ '11223344' => makeCompany(
+ ic: '11223344',
+ name: 'Legacy Services a.s.',
+ dic: null,
+ active: false,
+ legalForm: 'a.s.',
+ ),
+ ];
+
+ $companies = ares()
+ ->findMany(['12345678', '87654321', '11223344'])
+ ->active()
+ ->withVat()
+ ->legalForm('s.r.o.')
+ ->search('Tech')
+ ->limit(10)
+ ->getFormatted();
+
+ expect($companies)->toHaveCount(1)
+ ->and($companies[0]['Name'])->toBe('Tech Solutions s.r.o.')
+ ->and($this->fakeAresClient->findCalls)->toBe(['12345678', '87654321', '11223344']);
+});
+
+it('supports fluent stats, key extraction and reset', function () {
+ $this->fakeAresClient->companiesByIc = [
+ '12345678' => $this->activeCompany,
+ '87654321' => makeCompany(
+ ic: '87654321',
+ name: 'Inactive Company',
+ active: false,
+ dic: null,
+ ),
+ ];
+
+ $builder = ares()->findMany(['12345678', '87654321']);
+
+ expect($builder->stats())->toMatchArray([
+ 'total' => 2,
+ 'active' => 1,
+ 'inactive' => 1,
+ 'with_vat' => 1,
+ 'without_vat' => 1,
+ ])->and($builder->names())->toBe(['Test Company', 'Inactive Company'])
+ ->and($builder->ics())->toBe(['12345678', '87654321'])
+ ->and($builder->keyByIc())->toHaveKeys(['12345678', '87654321']);
+
+ $builder->reset();
+
+ expect($builder->count())->toBe(0)
+ ->and($builder->isEmpty())->toBeTrue();
+});
+
+it('can forget cached companies through the builder', function () {
+ $this->fakeAresClient->companiesByIc['12345678'] = $this->activeCompany;
+
+ ares()->find('12345678')->forget();
+
+ expect($this->fakeAresClient->forgottenIcs)->toBe(['12345678'])
+ ->and($this->fakeAresClient->companiesByIc)->not->toHaveKey('12345678');
+});
+
+it('exposes helper convenience functions for found companies', function () {
+ $this->fakeAresClient->companiesByIc['12345678'] = $this->activeCompany;
+
+ expect(ares_is_company_active('12345678'))->toBeTrue()
+ ->and(ares_get_address('12345678'))->toBe('Test Street 123, Test District, 12345 Test City')
+ ->and(ares_get_legal_form('12345678'))->toBe('s.r.o.')
+ ->and(ares_has_vat('12345678'))->toBeTrue()
+ ->and(ares_get_establishment_date('12345678'))->toBe('2020-01-01')
+ ->and(ares_format_company('12345678'))->toMatchArray([
+ 'IC' => '12345678',
+ 'Name' => 'Test Company',
+ 'Status' => 'Active',
+ ]);
+});
+
+it('exposes helper convenience functions with safe defaults for missing companies', function () {
+ expect(ares_is_company_active('12345678'))->toBeFalse()
+ ->and(ares_get_address('12345678'))->toBe('N/A')
+ ->and(ares_get_legal_form('12345678'))->toBe('N/A')
+ ->and(ares_has_vat('12345678'))->toBeFalse()
+ ->and(ares_get_establishment_date('12345678'))->toBe('N/A')
+ ->and(ares_format_company('12345678'))->toBe([]);
+});
+
+it('aggregates statistics through the global helper', function () {
+ $this->fakeAresClient->companiesByIc = [
+ '12345678' => $this->activeCompany,
+ '87654321' => makeCompany(
+ ic: '87654321',
+ name: 'Inactive Company',
+ dic: null,
+ active: false,
+ ),
+ ];
+
+ expect(ares_get_company_statistics(['12345678', '87654321']))->toMatchArray([
+ 'total' => 2,
+ 'active' => 1,
+ 'inactive' => 1,
+ 'with_vat' => 1,
+ 'without_vat' => 1,
+ ]);
+});
+
+it('detects active companies from the primary registration source when available', function () {
+ $company = makeCompany(sourceStatuses: [
+ new RegistrationStatusData(
+ source: 'ros',
+ rawStatus: 'AKTIVNI',
+ status: RegistrationSourceState::Active,
+ ),
+ new RegistrationStatusData(
+ source: 'vr',
+ rawStatus: 'HISTORICKY',
+ status: RegistrationSourceState::Historical,
+ ),
+ ]);
+
+ expect(AresHelper::isCompanyActive($company))->toBeTrue();
+});
+
+it('falls back to any active source status when primary status is missing', function () {
+ $company = makeCompany(
+ primarySource: 'missing',
+ sourceStatuses: [
+ new RegistrationStatusData(
+ source: 'vr',
+ rawStatus: 'AKTIVNI',
+ status: RegistrationSourceState::Active,
+ ),
+ ],
+ );
+
+ expect(AresHelper::isCompanyActive($company))->toBeTrue();
+});
+
+it('treats companies as inactive when no source status is active', function () {
+ $company = makeCompany(active: false);
+
+ expect(AresHelper::isCompanyActive($company))->toBeFalse();
+});
+
+it('resolves the helper from the service container alias', function () {
+ expect(app('ares.helper'))->toBeInstanceOf(AresHelper::class)
+ ->and(app(AresHelper::class))->toBeInstanceOf(AresHelper::class);
+});
+
+function makeCompany(
+ string $ic = '12345678',
+ string $name = 'Test Company',
+ ?string $dic = 'CZ12345678',
+ bool $active = true,
+ string $legalForm = 's.r.o.',
+ ?string $primarySource = 'ros',
+ ?array $sourceStatuses = null,
+): CompanyData {
+ return new CompanyData(
+ ic: $ic,
+ name: $name,
+ dic: $dic,
+ dicSkDph: null,
+ registeredOffice: new AddressData(
+ formatted: 'Test Street 123, Test District, 12345 Test City',
+ street: 'Test Street',
+ houseNumber: '123',
+ district: 'Test District',
+ city: 'Test City',
+ postalCode: '12345',
+ countryCode: 'CZ',
+ country: 'Czech Republic',
+ ),
+ deliveryAddress: null,
+ registration: new RegistrationData(
+ legalForm: $legalForm,
+ financialOffice: 'Financni urad pro Test City',
+ dateOfEstablishment: '2020-01-01',
+ dateOfLastUpdate: '2026-04-26',
+ primarySource: $primarySource,
+ businessRegisterFileMark: 'C 12345/MSPH',
+ naceCodes: ['62'],
+ nace2008Codes: ['620'],
+ sourceStatuses: $sourceStatuses ?? [
+ new RegistrationStatusData(
+ source: 'ros',
+ rawStatus: $active ? 'AKTIVNI' : 'HISTORICKY',
+ status: $active ? RegistrationSourceState::Active : RegistrationSourceState::Historical,
+ ),
+ ],
+ ),
+ rawData: [],
+ );
+}
diff --git a/tests/Pest.php b/tests/Pest.php
index a38d9d3..6b80984 100644
--- a/tests/Pest.php
+++ b/tests/Pest.php
@@ -4,4 +4,4 @@
use NyonCode\Ares\Tests\TestCase;
-uses(TestCase::class)->in('Feature', 'Unit');
+uses(TestCase::class)->in('Feature', 'Integration', 'Unit');
diff --git a/tests/Unit/ServiceProviderTest.php b/tests/Unit/ServiceProviderTest.php
index 28e972b..ad1bd44 100644
--- a/tests/Unit/ServiceProviderTest.php
+++ b/tests/Unit/ServiceProviderTest.php
@@ -3,9 +3,12 @@
declare(strict_types=1);
use NyonCode\Ares\Contracts\AresClientInterface;
+use NyonCode\Ares\Helpers\AresHelper;
use NyonCode\Ares\Services\AresClient;
it('registers the client contract and facade binding', function () {
expect(app(AresClientInterface::class))->toBeInstanceOf(AresClient::class)
- ->and(app('ares'))->toBeInstanceOf(AresClientInterface::class);
+ ->and(app('ares'))->toBeInstanceOf(AresClientInterface::class)
+ ->and(app(AresHelper::class))->toBeInstanceOf(AresHelper::class)
+ ->and(app('ares.helper'))->toBeInstanceOf(AresHelper::class);
});