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); });