diff --git a/.gitignore b/.gitignore
index d5673e3..b34d766 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,6 +2,10 @@
node_modules/
npm-debug.log
yarn-error.log
+/composer.lock
+/.phpstan/
+/.phpunit.cache/
+/.idea/
# Laravel 4 specific
bootstrap/compiled.php
diff --git a/.idea/.gitignore b/.idea/.gitignore
deleted file mode 100644
index ab1f416..0000000
--- a/.idea/.gitignore
+++ /dev/null
@@ -1,10 +0,0 @@
-# Default ignored files
-/shelf/
-/workspace.xml
-# Ignored default folder with query files
-/queries/
-# Datasource local storage ignored files
-/dataSources/
-/dataSources.local.xml
-# Editor-based HTTP Client requests
-/httpRequests/
diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml
deleted file mode 100644
index 83879ce..0000000
--- a/.idea/inspectionProfiles/Project_Default.xml
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/laravel-ares.iml b/.idea/laravel-ares.iml
deleted file mode 100644
index 07b238f..0000000
--- a/.idea/laravel-ares.iml
+++ /dev/null
@@ -1,157 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/laravel-idea-personal.xml b/.idea/laravel-idea-personal.xml
deleted file mode 100644
index 425e121..0000000
--- a/.idea/laravel-idea-personal.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/laravel-idea.xml b/.idea/laravel-idea.xml
deleted file mode 100644
index d8e17dd..0000000
--- a/.idea/laravel-idea.xml
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
deleted file mode 100644
index 3ce3588..0000000
--- a/.idea/misc.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
deleted file mode 100644
index f00f124..0000000
--- a/.idea/modules.xml
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/php-docker-settings.xml b/.idea/php-docker-settings.xml
deleted file mode 100644
index c3b1223..0000000
--- a/.idea/php-docker-settings.xml
+++ /dev/null
@@ -1,23 +0,0 @@
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/php-test-framework.xml b/.idea/php-test-framework.xml
deleted file mode 100644
index b97138c..0000000
--- a/.idea/php-test-framework.xml
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/php.xml b/.idea/php.xml
deleted file mode 100644
index 3ff3c62..0000000
--- a/.idea/php.xml
+++ /dev/null
@@ -1,188 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/stat.log b/.idea/stat.log
deleted file mode 100644
index fd45870..0000000
--- a/.idea/stat.log
+++ /dev/null
@@ -1,447 +0,0 @@
-{"ts":1777018029320,"action":"PROJECT_OPENED","tags":{"PROJECT_NAME":"laravel-ares"}}
-{"ts":1777018032010,"action":"IDE_DEACTIVATED"}
-{"ts":1777018032185,"action":"IDE_ACTIVATED"}
-{"ts":1777018034155,"action":"FILE_OPENED","file":"/README.md","tags":{"FILE_LINE_OF_CODE":"3"}}
-{"ts":1777018039572,"action":"IDE_DEACTIVATED"}
-{"ts":1777018044489,"action":"IDE_ACTIVATED"}
-{"ts":1777018044830,"action":"IDE_DEACTIVATED"}
-{"ts":1777018048478,"action":"IDE_ACTIVATED"}
-{"ts":1777018055159,"action":"FILE_CLOSED","file":"/README.md","tags":{"FILE_LINE_OF_CODE":"3"}}
-{"ts":1777018055159,"action":"FILE_OPENED","file":"/composer.json","tags":{"FILE_LINE_OF_CODE":"0"}}
-{"ts":1777018055597,"action":"IDE_DEACTIVATED"}
-{"ts":1777018056271,"action":"IDE_ACTIVATED"}
-{"ts":1777018057784,"action":"IDE_DEACTIVATED"}
-{"ts":1777018059693,"action":"IDE_ACTIVATED"}
-{"ts":1777018062595,"action":"IDE_DEACTIVATED"}
-{"ts":1777018063341,"action":"IDE_ACTIVATED"}
-{"ts":1777018064035,"action":"IDE_DEACTIVATED"}
-{"ts":1777018066682,"action":"IDE_ACTIVATED"}
-{"ts":1777018072099,"action":"IDE_DEACTIVATED"}
-{"ts":1777018075130,"action":"IDE_ACTIVATED"}
-{"ts":1777018087547,"action":"FILE_CLOSED","file":"/composer.json","tags":{"FILE_LINE_OF_CODE":"34"}}
-{"ts":1777018087547,"action":"FILE_OPENED","file":"/src/AresServiceProvider.php","tags":{"FILE_LINE_OF_CODE":"0"}}
-{"ts":1777018089595,"action":"IDE_DEACTIVATED"}
-{"ts":1777018091194,"action":"IDE_ACTIVATED"}
-{"ts":1777018103743,"action":"FILE_CLOSED","file":"/src/Provider/AresServiceProvider.php","tags":{"FILE_LINE_OF_CODE":"0"}}
-{"ts":1777018103743,"action":"FILE_OPENED","file":"/composer.json","tags":{"FILE_LINE_OF_CODE":"34"}}
-{"ts":1777018108811,"action":"IDE_DEACTIVATED"}
-{"ts":1777018112010,"action":"IDE_ACTIVATED"}
-{"ts":1777018112973,"action":"FILE_CLOSED","file":"/composer.json","tags":{"FILE_LINE_OF_CODE":"34"}}
-{"ts":1777018112973,"action":"FILE_OPENED","file":"/src/Provider/AresServiceProvider.php","tags":{"FILE_LINE_OF_CODE":"0"}}
-{"ts":1777018133981,"action":"FILE_CLOSED","file":"/src/Provider/AresServiceProvider.php","tags":{"FILE_LINE_OF_CODE":"44"}}
-{"ts":1777018133981,"action":"FILE_OPENED","file":"/composer.json","tags":{"FILE_LINE_OF_CODE":"34"}}
-{"ts":1777018230070,"action":"IDE_DEACTIVATED"}
-{"ts":1777018234449,"action":"IDE_ACTIVATED"}
-{"ts":1777018238691,"action":"IDE_DEACTIVATED"}
-{"ts":1777018277380,"action":"IDE_ACTIVATED"}
-{"ts":1777018313501,"action":"IDE_DEACTIVATED"}
-{"ts":1777018316719,"action":"IDE_ACTIVATED"}
-{"ts":1777018321911,"action":"IDE_DEACTIVATED"}
-{"ts":1777018324995,"action":"IDE_ACTIVATED"}
-{"ts":1777018326792,"action":"FILE_CLOSED","file":"/composer.json","tags":{"FILE_LINE_OF_CODE":"41"}}
-{"ts":1777018326792,"action":"FILE_OPENED","file":"/src/config/ares.php","tags":{"FILE_LINE_OF_CODE":"0"}}
-{"ts":1777018327278,"action":"IDE_DEACTIVATED"}
-{"ts":1777018327283,"action":"IDE_ACTIVATED"}
-{"ts":1777018332045,"action":"IDE_DEACTIVATED"}
-{"ts":1777018332654,"action":"IDE_ACTIVATED"}
-{"ts":1777018333695,"action":"IDE_DEACTIVATED"}
-{"ts":1777018353049,"action":"IDE_ACTIVATED"}
-{"ts":1777018354886,"action":"FILE_CLOSED","file":"/src/config/ares.php","tags":{"FILE_LINE_OF_CODE":"11"}}
-{"ts":1777018354886,"action":"FILE_OPENED","file":"/src/Provider/AresServiceProvider.php","tags":{"FILE_LINE_OF_CODE":"44"}}
-{"ts":1777018373610,"action":"IDE_DEACTIVATED"}
-{"ts":1777018398175,"action":"IDE_ACTIVATED"}
-{"ts":1777018402045,"action":"IDE_DEACTIVATED"}
-{"ts":1777018468113,"action":"IDE_ACTIVATED"}
-{"ts":1777018493116,"action":"FILE_CLOSED","file":"/src/Provider/AresServiceProvider.php","tags":{"FILE_LINE_OF_CODE":"42"}}
-{"ts":1777018493116,"action":"FILE_OPENED","file":"/vendor/fakerphp/faker/src/Faker/Factory.php","tags":{"FILE_LINE_OF_CODE":"72"}}
-{"ts":1777018510219,"action":"FILE_CLOSED","file":"/vendor/fakerphp/faker/src/Faker/Factory.php","tags":{"FILE_LINE_OF_CODE":"72"}}
-{"ts":1777018510219,"action":"FILE_OPENED","file":"/src/Providers/AresServiceProvider.php","tags":{"FILE_LINE_OF_CODE":"42"}}
-{"ts":1777018528844,"action":"IDE_DEACTIVATED"}
-{"ts":1777018537441,"action":"IDE_ACTIVATED"}
-{"ts":1777018544284,"action":"IDE_DEACTIVATED"}
-{"ts":1777018547850,"action":"IDE_ACTIVATED"}
-{"ts":1777018575642,"action":"IDE_DEACTIVATED"}
-{"ts":1777018577349,"action":"IDE_ACTIVATED"}
-{"ts":1777018580554,"action":"FILE_CLOSED","file":"/src/Providers/AresServiceProvider.php","tags":{"FILE_LINE_OF_CODE":"53"}}
-{"ts":1777018580554,"action":"FILE_OPENED","file":"/src/Data/CompanyData.php","tags":{"FILE_LINE_OF_CODE":"0"}}
-{"ts":1777018581057,"action":"IDE_DEACTIVATED"}
-{"ts":1777018581843,"action":"IDE_ACTIVATED"}
-{"ts":1777018583075,"action":"IDE_DEACTIVATED"}
-{"ts":1777018584377,"action":"IDE_ACTIVATED"}
-{"ts":1777018585475,"action":"IDE_DEACTIVATED"}
-{"ts":1777018590399,"action":"IDE_ACTIVATED"}
-{"ts":1777018593800,"action":"FILE_CLOSED","file":"/src/Data/CompanyData.php","tags":{"FILE_LINE_OF_CODE":"33"}}
-{"ts":1777018593801,"action":"FILE_OPENED","file":"/src/Services/AresClient.php","tags":{"FILE_LINE_OF_CODE":"0"}}
-{"ts":1777018594502,"action":"IDE_DEACTIVATED"}
-{"ts":1777018595187,"action":"IDE_ACTIVATED"}
-{"ts":1777018595942,"action":"IDE_DEACTIVATED"}
-{"ts":1777018597489,"action":"IDE_ACTIVATED"}
-{"ts":1777018598855,"action":"IDE_DEACTIVATED"}
-{"ts":1777018603497,"action":"IDE_ACTIVATED"}
-{"ts":1777018607693,"action":"FILE_CLOSED","file":"/src/Services/AresClient.php","tags":{"FILE_LINE_OF_CODE":"101"}}
-{"ts":1777018607693,"action":"FILE_OPENED","file":"/src/Events/CompanyLookupSucceeded.php","tags":{"FILE_LINE_OF_CODE":"0"}}
-{"ts":1777018608467,"action":"IDE_DEACTIVATED"}
-{"ts":1777018609274,"action":"IDE_ACTIVATED"}
-{"ts":1777018609915,"action":"IDE_DEACTIVATED"}
-{"ts":1777018611296,"action":"IDE_ACTIVATED"}
-{"ts":1777018613568,"action":"IDE_DEACTIVATED"}
-{"ts":1777018617224,"action":"IDE_ACTIVATED"}
-{"ts":1777018621609,"action":"FILE_CLOSED","file":"/src/Events/CompanyLookupSucceeded.php","tags":{"FILE_LINE_OF_CODE":"12"}}
-{"ts":1777018621609,"action":"FILE_OPENED","file":"/src/Events/CompanyLookupFailed.php","tags":{"FILE_LINE_OF_CODE":"0"}}
-{"ts":1777018624343,"action":"IDE_DEACTIVATED"}
-{"ts":1777018626174,"action":"IDE_ACTIVATED"}
-{"ts":1777018627715,"action":"IDE_DEACTIVATED"}
-{"ts":1777018631477,"action":"IDE_ACTIVATED"}
-{"ts":1777018635080,"action":"FILE_CLOSED","file":"/src/Events/CompanyLookupFailed.php","tags":{"FILE_LINE_OF_CODE":"16"}}
-{"ts":1777018635080,"action":"FILE_OPENED","file":"/src/Facades/Ares.php","tags":{"FILE_LINE_OF_CODE":"0"}}
-{"ts":1777018637769,"action":"IDE_DEACTIVATED"}
-{"ts":1777018639204,"action":"IDE_ACTIVATED"}
-{"ts":1777018639484,"action":"IDE_DEACTIVATED"}
-{"ts":1777018640805,"action":"IDE_ACTIVATED"}
-{"ts":1777018644186,"action":"IDE_DEACTIVATED"}
-{"ts":1777018644855,"action":"IDE_ACTIVATED"}
-{"ts":1777018649634,"action":"FILE_CLOSED","file":"/src/Facades/Ares.php","tags":{"FILE_LINE_OF_CODE":"20"}}
-{"ts":1777018649634,"action":"FILE_OPENED","file":"/src/Events/CompanyLookupFailed.php","tags":{"FILE_LINE_OF_CODE":"16"}}
-{"ts":1777018651060,"action":"FILE_CLOSED","file":"/src/Events/CompanyLookupFailed.php","tags":{"FILE_LINE_OF_CODE":"16"}}
-{"ts":1777018651060,"action":"FILE_OPENED","file":"/src/Events/CompanyLookupSucceeded.php","tags":{"FILE_LINE_OF_CODE":"12"}}
-{"ts":1777018654780,"action":"FILE_CLOSED","file":"/src/Events/CompanyLookupSucceeded.php","tags":{"FILE_LINE_OF_CODE":"12"}}
-{"ts":1777018654780,"action":"FILE_OPENED","file":"/composer.json","tags":{"FILE_LINE_OF_CODE":"41"}}
-{"ts":1777018662227,"action":"FILE_CLOSED","file":"/composer.json","tags":{"FILE_LINE_OF_CODE":"41"}}
-{"ts":1777018662227,"action":"FILE_OPENED","file":"/src/Providers/AresServiceProvider.php","tags":{"FILE_LINE_OF_CODE":"53"}}
-{"ts":1777018675160,"action":"FILE_CLOSED","file":"/src/Providers/AresServiceProvider.php","tags":{"FILE_LINE_OF_CODE":"53"}}
-{"ts":1777018675160,"action":"FILE_OPENED","file":"/src/Data/CompanyData.php","tags":{"FILE_LINE_OF_CODE":"33"}}
-{"ts":1777018677271,"action":"FILE_CLOSED","file":"/src/Data/CompanyData.php","tags":{"FILE_LINE_OF_CODE":"33"}}
-{"ts":1777018677271,"action":"FILE_OPENED","file":"/src/Events/CompanyLookupFailed.php","tags":{"FILE_LINE_OF_CODE":"16"}}
-{"ts":1777018678077,"action":"FILE_CLOSED","file":"/src/Events/CompanyLookupFailed.php","tags":{"FILE_LINE_OF_CODE":"16"}}
-{"ts":1777018678077,"action":"FILE_OPENED","file":"/src/Events/CompanyLookupSucceeded.php","tags":{"FILE_LINE_OF_CODE":"12"}}
-{"ts":1777018678813,"action":"IDE_DEACTIVATED"}
-{"ts":1777018689917,"action":"IDE_ACTIVATED"}
-{"ts":1777018695169,"action":"FILE_CLOSED","file":"/src/Events/CompanyLookupSucceeded.php","tags":{"FILE_LINE_OF_CODE":"12"}}
-{"ts":1777018695169,"action":"FILE_OPENED","file":"/src/Commands/TestAresCommand.php","tags":{"FILE_LINE_OF_CODE":"0"}}
-{"ts":1777018695743,"action":"IDE_DEACTIVATED"}
-{"ts":1777018696614,"action":"IDE_ACTIVATED"}
-{"ts":1777018697630,"action":"IDE_DEACTIVATED"}
-{"ts":1777018701656,"action":"IDE_ACTIVATED"}
-{"ts":1777018703356,"action":"IDE_DEACTIVATED"}
-{"ts":1777018711636,"action":"IDE_ACTIVATED"}
-{"ts":1777018715930,"action":"FILE_CLOSED","file":"/src/Commands/TestAresCommand.php","tags":{"FILE_LINE_OF_CODE":"38"}}
-{"ts":1777018715930,"action":"FILE_OPENED","file":"/resources/lang/cs/ares.php ","tags":{"FILE_LINE_OF_CODE":"0"}}
-{"ts":1777018717197,"action":"IDE_DEACTIVATED"}
-{"ts":1777018718847,"action":"IDE_ACTIVATED"}
-{"ts":1777018719253,"action":"FILE_OPENED","file":"/src/Commands/TestAresCommand.php","tags":{"FILE_LINE_OF_CODE":"38"}}
-{"ts":1777018725076,"action":"FILE_CLOSED","file":"/src/Commands/TestAresCommand.php","tags":{"FILE_LINE_OF_CODE":"46"}}
-{"ts":1777018725076,"action":"FILE_OPENED","file":"/resources/lang/cs/ares.php","tags":{"FILE_LINE_OF_CODE":"0"}}
-{"ts":1777018726256,"action":"IDE_DEACTIVATED"}
-{"ts":1777018728892,"action":"IDE_ACTIVATED"}
-{"ts":1777018731951,"action":"FILE_CLOSED","file":"/resources/lang/cs/ares.php","tags":{"FILE_LINE_OF_CODE":"9"}}
-{"ts":1777018731951,"action":"FILE_OPENED","file":"/resources/lang/en/ares.php ","tags":{"FILE_LINE_OF_CODE":"0"}}
-{"ts":1777018732580,"action":"IDE_DEACTIVATED"}
-{"ts":1777018734048,"action":"IDE_ACTIVATED"}
-{"ts":1777018734426,"action":"FILE_OPENED","file":"/resources/lang/cs/ares.php","tags":{"FILE_LINE_OF_CODE":"9"}}
-{"ts":1777018736787,"action":"IDE_DEACTIVATED"}
-{"ts":1777018743456,"action":"IDE_ACTIVATED"}
-{"ts":1777018746932,"action":"FILE_CLOSED","file":"/resources/lang/cs/ares.php","tags":{"FILE_LINE_OF_CODE":"9"}}
-{"ts":1777018746932,"action":"FILE_OPENED","file":"/example/blade-usage.blade.php ","tags":{"FILE_LINE_OF_CODE":"0"}}
-{"ts":1777018747587,"action":"IDE_DEACTIVATED"}
-{"ts":1777018749041,"action":"IDE_ACTIVATED"}
-{"ts":1777018749485,"action":"FILE_OPENED","file":"/resources/lang/cs/ares.php","tags":{"FILE_LINE_OF_CODE":"41"}}
-{"ts":1777018752139,"action":"IDE_DEACTIVATED"}
-{"ts":1777018756329,"action":"IDE_ACTIVATED"}
-{"ts":1777018759544,"action":"FILE_CLOSED","file":"/resources/lang/cs/ares.php","tags":{"FILE_LINE_OF_CODE":"33"}}
-{"ts":1777018759544,"action":"FILE_OPENED","file":"/example/example/livewire-usage.php ","tags":{"FILE_LINE_OF_CODE":"0"}}
-{"ts":1777018760352,"action":"IDE_DEACTIVATED"}
-{"ts":1777018761965,"action":"IDE_ACTIVATED"}
-{"ts":1777018762428,"action":"FILE_OPENED","file":"/resources/lang/cs/ares.php","tags":{"FILE_LINE_OF_CODE":"33"}}
-{"ts":1777018765012,"action":"FILE_CLOSED","file":"/resources/lang/cs/ares.php","tags":{"FILE_LINE_OF_CODE":"33"}}
-{"ts":1777018765012,"action":"FILE_OPENED","file":"/example/blade-usage.blade.php","tags":{"FILE_LINE_OF_CODE":"0"}}
-{"ts":1777018767029,"action":"FILE_CLOSED","file":"/example/blade-usage.blade.php","tags":{"FILE_LINE_OF_CODE":"0"}}
-{"ts":1777018767029,"action":"FILE_OPENED","file":"/example/example/livewire-usage.php","tags":{"FILE_LINE_OF_CODE":"0"}}
-{"ts":1777018774372,"action":"FILE_CLOSED","file":"/example/livewire-usage.php","tags":{"FILE_LINE_OF_CODE":"41"}}
-{"ts":1777018774372,"action":"FILE_OPENED","file":"/example/blade-usage.blade.php","tags":{"FILE_LINE_OF_CODE":"0"}}
-{"ts":1777018777450,"action":"IDE_DEACTIVATED"}
-{"ts":1777018783268,"action":"IDE_ACTIVATED"}
-{"ts":1777018788882,"action":"IDE_DEACTIVATED"}
-{"ts":1777018885223,"action":"IDE_ACTIVATED"}
-{"ts":1777018887078,"action":"FILE_CLOSED","file":"/example/blade-usage.blade.php","tags":{"FILE_LINE_OF_CODE":"33"}}
-{"ts":1777018887078,"action":"FILE_OPENED","file":"/resources/lang/en/ares.php","tags":{"FILE_LINE_OF_CODE":"0"}}
-{"ts":1777018892017,"action":"FILE_CLOSED","file":"/resources/lang/en/ares.php","tags":{"FILE_LINE_OF_CODE":"0"}}
-{"ts":1777018892017,"action":"FILE_OPENED","file":"/src/Providers/AresServiceProvider.php","tags":{"FILE_LINE_OF_CODE":"53"}}
-{"ts":1777018895811,"action":"IDE_DEACTIVATED"}
-{"ts":1777018905336,"action":"IDE_ACTIVATED"}
-{"ts":1777018911802,"action":"FILE_CLOSED","file":"/src/Providers/AresServiceProvider.php","tags":{"FILE_LINE_OF_CODE":"53"}}
-{"ts":1777018911802,"action":"FILE_OPENED","file":"/config/ares.php","tags":{"FILE_LINE_OF_CODE":"11"}}
-{"ts":1777018913619,"action":"IDE_DEACTIVATED"}
-{"ts":1777018928890,"action":"IDE_ACTIVATED"}
-{"ts":1777018934962,"action":"IDE_DEACTIVATED"}
-{"ts":1777018936602,"action":"IDE_ACTIVATED"}
-{"ts":1777018940235,"action":"FILE_CLOSED","file":"/config/ares.php","tags":{"FILE_LINE_OF_CODE":"34"}}
-{"ts":1777018940235,"action":"FILE_OPENED","file":"/src/Contracts/AresClientInterface.php","tags":{"FILE_LINE_OF_CODE":"0"}}
-{"ts":1777018944442,"action":"IDE_DEACTIVATED"}
-{"ts":1777018946405,"action":"IDE_ACTIVATED"}
-{"ts":1777018949743,"action":"IDE_DEACTIVATED"}
-{"ts":1777018952199,"action":"IDE_ACTIVATED"}
-{"ts":1777018953302,"action":"FILE_CLOSED","file":"/src/Contracts/AresClientInterface.php","tags":{"FILE_LINE_OF_CODE":"13"}}
-{"ts":1777018953302,"action":"FILE_OPENED","file":"/src/Data/CompanyData.php","tags":{"FILE_LINE_OF_CODE":"33"}}
-{"ts":1777018955196,"action":"IDE_DEACTIVATED"}
-{"ts":1777018957889,"action":"IDE_ACTIVATED"}
-{"ts":1777018959648,"action":"IDE_DEACTIVATED"}
-{"ts":1777018962837,"action":"IDE_ACTIVATED"}
-{"ts":1777018968102,"action":"IDE_DEACTIVATED"}
-{"ts":1777018972680,"action":"IDE_ACTIVATED"}
-{"ts":1777018978538,"action":"FILE_CLOSED","file":"/src/Data/CompanyData.php","tags":{"FILE_LINE_OF_CODE":"33"}}
-{"ts":1777018978538,"action":"FILE_OPENED","file":"/src/Services/AresClient.php","tags":{"FILE_LINE_OF_CODE":"101"}}
-{"ts":1777018979598,"action":"IDE_DEACTIVATED"}
-{"ts":1777018981508,"action":"IDE_ACTIVATED"}
-{"ts":1777018987646,"action":"IDE_DEACTIVATED"}
-{"ts":1777018994079,"action":"IDE_ACTIVATED"}
-{"ts":1777019000135,"action":"FILE_CLOSED","file":"/src/Services/AresClient.php","tags":{"FILE_LINE_OF_CODE":"100"}}
-{"ts":1777019000135,"action":"FILE_OPENED","file":"/src/Events/CompanyLookupFailed.php","tags":{"FILE_LINE_OF_CODE":"16"}}
-{"ts":1777019000660,"action":"IDE_DEACTIVATED"}
-{"ts":1777019002610,"action":"IDE_ACTIVATED"}
-{"ts":1777019008266,"action":"IDE_DEACTIVATED"}
-{"ts":1777019009959,"action":"IDE_ACTIVATED"}
-{"ts":1777019012717,"action":"FILE_CLOSED","file":"/src/Events/CompanyLookupFailed.php","tags":{"FILE_LINE_OF_CODE":"12"}}
-{"ts":1777019012717,"action":"FILE_OPENED","file":"/src/Events/CompanyLookupSucceeded.php","tags":{"FILE_LINE_OF_CODE":"12"}}
-{"ts":1777019014821,"action":"IDE_DEACTIVATED"}
-{"ts":1777019015849,"action":"IDE_ACTIVATED"}
-{"ts":1777019019095,"action":"IDE_DEACTIVATED"}
-{"ts":1777019031131,"action":"IDE_ACTIVATED"}
-{"ts":1777019033645,"action":"IDE_DEACTIVATED"}
-{"ts":1777019036567,"action":"IDE_ACTIVATED"}
-{"ts":1777019037786,"action":"FILE_CLOSED","file":"/src/Events/CompanyLookupSucceeded.php","tags":{"FILE_LINE_OF_CODE":"12"}}
-{"ts":1777019037786,"action":"FILE_OPENED","file":"/src/Events/CompanyLookupFailed.php","tags":{"FILE_LINE_OF_CODE":"12"}}
-{"ts":1777019041693,"action":"IDE_DEACTIVATED"}
-{"ts":1777019045798,"action":"IDE_ACTIVATED"}
-{"ts":1777019047669,"action":"FILE_CLOSED","file":"/src/Events/CompanyLookupFailed.php","tags":{"FILE_LINE_OF_CODE":"16"}}
-{"ts":1777019047669,"action":"FILE_OPENED","file":"/src/Facades/Ares.php","tags":{"FILE_LINE_OF_CODE":"20"}}
-{"ts":1777019051702,"action":"IDE_DEACTIVATED"}
-{"ts":1777019056534,"action":"IDE_ACTIVATED"}
-{"ts":1777019057843,"action":"FILE_CLOSED","file":"/src/Facades/Ares.php","tags":{"FILE_LINE_OF_CODE":"20"}}
-{"ts":1777019057843,"action":"FILE_OPENED","file":"/src/Commands/TestAresCommand.php","tags":{"FILE_LINE_OF_CODE":"46"}}
-{"ts":1777019058363,"action":"IDE_DEACTIVATED"}
-{"ts":1777019060044,"action":"IDE_ACTIVATED"}
-{"ts":1777019062135,"action":"IDE_DEACTIVATED"}
-{"ts":1777019074832,"action":"IDE_ACTIVATED"}
-{"ts":1777019076210,"action":"IDE_DEACTIVATED"}
-{"ts":1777019077884,"action":"IDE_ACTIVATED"}
-{"ts":1777019079969,"action":"FILE_CLOSED","file":"/src/Commands/TestAresCommand.php","tags":{"FILE_LINE_OF_CODE":"38"}}
-{"ts":1777019079969,"action":"FILE_OPENED","file":"/src/Contracts/AresClientInterface.php","tags":{"FILE_LINE_OF_CODE":"13"}}
-{"ts":1777019081165,"action":"FILE_CLOSED","file":"/src/Contracts/AresClientInterface.php","tags":{"FILE_LINE_OF_CODE":"13"}}
-{"ts":1777019081165,"action":"FILE_OPENED","file":"/src/Data/CompanyData.php","tags":{"FILE_LINE_OF_CODE":"33"}}
-{"ts":1777019081955,"action":"FILE_CLOSED","file":"/src/Data/CompanyData.php","tags":{"FILE_LINE_OF_CODE":"33"}}
-{"ts":1777019081955,"action":"FILE_OPENED","file":"/src/Events/CompanyLookupFailed.php","tags":{"FILE_LINE_OF_CODE":"16"}}
-{"ts":1777019082461,"action":"FILE_CLOSED","file":"/src/Events/CompanyLookupFailed.php","tags":{"FILE_LINE_OF_CODE":"16"}}
-{"ts":1777019082461,"action":"FILE_OPENED","file":"/src/Events/CompanyLookupSucceeded.php","tags":{"FILE_LINE_OF_CODE":"12"}}
-{"ts":1777019083105,"action":"FILE_CLOSED","file":"/src/Events/CompanyLookupSucceeded.php","tags":{"FILE_LINE_OF_CODE":"12"}}
-{"ts":1777019083105,"action":"FILE_OPENED","file":"/src/Facades/Ares.php","tags":{"FILE_LINE_OF_CODE":"20"}}
-{"ts":1777019090412,"action":"FILE_CLOSED","file":"/src/Facades/Ares.php","tags":{"FILE_LINE_OF_CODE":"20"}}
-{"ts":1777019090412,"action":"FILE_OPENED","file":"/src/Contracts/AresClientInterface.php","tags":{"FILE_LINE_OF_CODE":"13"}}
-{"ts":1777019092258,"action":"FILE_CLOSED","file":"/src/Contracts/AresClientInterface.php","tags":{"FILE_LINE_OF_CODE":"13"}}
-{"ts":1777019092258,"action":"FILE_OPENED","file":"/src/Providers/AresServiceProvider.php","tags":{"FILE_LINE_OF_CODE":"53"}}
-{"ts":1777019108994,"action":"FILE_CLOSED","file":"/src/Providers/AresServiceProvider.php","tags":{"FILE_LINE_OF_CODE":"54"}}
-{"ts":1777019108994,"action":"FILE_OPENED","file":"/src/Services/AresClient.php","tags":{"FILE_LINE_OF_CODE":"100"}}
-{"ts":1777019170014,"action":"FILE_CLOSED","file":"/src/Services/AresClient.php","tags":{"FILE_LINE_OF_CODE":"101"}}
-{"ts":1777019170014,"action":"FILE_OPENED","file":"/src/Data/CompanyData.php","tags":{"FILE_LINE_OF_CODE":"33"}}
-{"ts":1777019171494,"action":"FILE_CLOSED","file":"/src/Data/CompanyData.php","tags":{"FILE_LINE_OF_CODE":"33"}}
-{"ts":1777019171494,"action":"FILE_OPENED","file":"/src/Services/AresClient.php","tags":{"FILE_LINE_OF_CODE":"101"}}
-{"ts":1777019196556,"action":"FILE_CLOSED","file":"/src/Services/AresClient.php","tags":{"FILE_LINE_OF_CODE":"101"}}
-{"ts":1777019196556,"action":"FILE_OPENED","file":"/src/Providers/AresServiceProvider.php","tags":{"FILE_LINE_OF_CODE":"54"}}
-{"ts":1777019197206,"action":"FILE_CLOSED","file":"/src/Providers/AresServiceProvider.php","tags":{"FILE_LINE_OF_CODE":"54"}}
-{"ts":1777019197206,"action":"FILE_OPENED","file":"/src/Facades/Ares.php","tags":{"FILE_LINE_OF_CODE":"20"}}
-{"ts":1777019214401,"action":"FILE_CLOSED","file":"/src/Facades/Ares.php","tags":{"FILE_LINE_OF_CODE":"20"}}
-{"ts":1777019214401,"action":"FILE_OPENED","file":"/example/blade-usage.blade.php","tags":{"FILE_LINE_OF_CODE":"33"}}
-{"ts":1777019216811,"action":"FILE_OPENED","file":"/resources/lang/cs/ares.php","tags":{"FILE_LINE_OF_CODE":"33"}}
-{"ts":1777019232776,"action":"FILE_CLOSED","file":"/resources/lang/cs/ares.php","tags":{"FILE_LINE_OF_CODE":"33"}}
-{"ts":1777019232776,"action":"FILE_OPENED","file":"/src/Commands/TestAresCommand.php","tags":{"FILE_LINE_OF_CODE":"38"}}
-{"ts":1777019234725,"action":"FILE_CLOSED","file":"/src/Commands/TestAresCommand.php","tags":{"FILE_LINE_OF_CODE":"38"}}
-{"ts":1777019234725,"action":"FILE_OPENED","file":"/resources/lang/en/ares.php","tags":{"FILE_LINE_OF_CODE":"0"}}
-{"ts":1777019236095,"action":"FILE_CLOSED","file":"/resources/lang/en/ares.php","tags":{"FILE_LINE_OF_CODE":"0"}}
-{"ts":1777019236095,"action":"FILE_OPENED","file":"/resources/lang/cs/ares.php","tags":{"FILE_LINE_OF_CODE":"33"}}
-{"ts":1777019236789,"action":"IDE_DEACTIVATED"}
-{"ts":1777019244155,"action":"IDE_ACTIVATED"}
-{"ts":1777019247907,"action":"IDE_DEACTIVATED"}
-{"ts":1777019250012,"action":"IDE_ACTIVATED"}
-{"ts":1777019251287,"action":"FILE_CLOSED","file":"/resources/lang/cs/ares.php","tags":{"FILE_LINE_OF_CODE":"9"}}
-{"ts":1777019251287,"action":"FILE_OPENED","file":"/resources/lang/en/ares.php","tags":{"FILE_LINE_OF_CODE":"0"}}
-{"ts":1777019254475,"action":"FILE_CLOSED","file":"/resources/lang/en/ares.php","tags":{"FILE_LINE_OF_CODE":"9"}}
-{"ts":1777019254475,"action":"FILE_OPENED","file":"/src/Commands/TestAresCommand.php","tags":{"FILE_LINE_OF_CODE":"38"}}
-{"ts":1777019256444,"action":"FILE_CLOSED","file":"/src/Commands/TestAresCommand.php","tags":{"FILE_LINE_OF_CODE":"38"}}
-{"ts":1777019256444,"action":"FILE_OPENED","file":"/src/Events/CompanyLookupSucceeded.php","tags":{"FILE_LINE_OF_CODE":"12"}}
-{"ts":1777019262571,"action":"FILE_CLOSED","file":"/src/Events/CompanyLookupSucceeded.php","tags":{"FILE_LINE_OF_CODE":"12"}}
-{"ts":1777019262571,"action":"FILE_OPENED","file":"/composer.json","tags":{"FILE_LINE_OF_CODE":"41"}}
-{"ts":1777019269464,"action":"IDE_DEACTIVATED"}
-{"ts":1777019283530,"action":"IDE_ACTIVATED"}
-{"ts":1777019293237,"action":"PROJECT_CLOSED","tags":{"PROJECT_NAME":"laravel-ares"}}
-{"ts":1777019384663,"action":"FILE_OPENED","file":"/composer.json","tags":{"FILE_LINE_OF_CODE":"41"}}
-{"ts":1777019387912,"action":"PROJECT_OPENED","tags":{"PROJECT_NAME":"laravel-ares"}}
-{"ts":1777019487056,"action":"FILE_CLOSED","file":"/composer.json","tags":{"FILE_LINE_OF_CODE":"41"}}
-{"ts":1777019487056,"action":"FILE_OPENED","file":"/src/Services/AresClient.php","tags":{"FILE_LINE_OF_CODE":"101"}}
-{"ts":1777019536838,"action":"FILE_CLOSED","file":"/src/Services/AresClient.php","tags":{"FILE_LINE_OF_CODE":"101"}}
-{"ts":1777019536838,"action":"FILE_OPENED","file":"/composer.json","tags":{"FILE_LINE_OF_CODE":"41"}}
-{"ts":1777019565536,"action":"IDE_DEACTIVATED"}
-{"ts":1777019565624,"action":"IDE_ACTIVATED"}
-{"ts":1777019612835,"action":"IDE_DEACTIVATED"}
-{"ts":1777020270882,"action":"IDE_ACTIVATED"}
-{"ts":1777020278745,"action":"FILE_CLOSED","file":"/composer.json","tags":{"FILE_LINE_OF_CODE":"56"}}
-{"ts":1777020332358,"action":"FILE_OPENED","file":"/README.md","tags":{"FILE_LINE_OF_CODE":"175"}}
-{"ts":1777020368122,"action":"FILE_CLOSED","file":"/README.md","tags":{"FILE_LINE_OF_CODE":"175"}}
-{"ts":1777020368122,"action":"FILE_OPENED","file":"/tests/Pest.php","tags":{"FILE_LINE_OF_CODE":"8"}}
-{"ts":1777020373748,"action":"FILE_CLOSED","file":"/tests/Pest.php","tags":{"FILE_LINE_OF_CODE":"8"}}
-{"ts":1777020373748,"action":"FILE_OPENED","file":"/tests/Unit/CompanyDataTest.php","tags":{"FILE_LINE_OF_CODE":"36"}}
-{"ts":1777020380970,"action":"FILE_CLOSED","file":"/tests/Unit/CompanyDataTest.php","tags":{"FILE_LINE_OF_CODE":"36"}}
-{"ts":1777020380970,"action":"FILE_OPENED","file":"/tests/Feature/AresClientTest.php","tags":{"FILE_LINE_OF_CODE":"100"}}
-{"ts":1777020419518,"action":"FILE_CLOSED","file":"/tests/Feature/AresClientTest.php","tags":{"FILE_LINE_OF_CODE":"100"}}
-{"ts":1777020419518,"action":"FILE_OPENED","file":"/src/Services/AresClient.php","tags":{"FILE_LINE_OF_CODE":"114"}}
-{"ts":1777020421766,"action":"FILE_CLOSED","file":"/src/Services/AresClient.php","tags":{"FILE_LINE_OF_CODE":"114"}}
-{"ts":1777020421766,"action":"FILE_OPENED","file":"/src/Commands/TestAresCommand.php","tags":{"FILE_LINE_OF_CODE":"41"}}
-{"ts":1777020442659,"action":"FILE_CLOSED","file":"/src/Commands/TestAresCommand.php","tags":{"FILE_LINE_OF_CODE":"41"}}
-{"ts":1777020442659,"action":"FILE_OPENED","file":"/src/Contracts/AresClientInterface.php","tags":{"FILE_LINE_OF_CODE":"15"}}
-{"ts":1777020444334,"action":"FILE_CLOSED","file":"/src/Contracts/AresClientInterface.php","tags":{"FILE_LINE_OF_CODE":"15"}}
-{"ts":1777020444334,"action":"FILE_OPENED","file":"/src/Data/CompanyData.php","tags":{"FILE_LINE_OF_CODE":"34"}}
-{"ts":1777020445520,"action":"FILE_CLOSED","file":"/src/Data/CompanyData.php","tags":{"FILE_LINE_OF_CODE":"34"}}
-{"ts":1777020445520,"action":"FILE_OPENED","file":"/resources/lang/cs/ares.php","tags":{"FILE_LINE_OF_CODE":"10"}}
-{"ts":1777020457495,"action":"FILE_CLOSED","file":"/resources/lang/cs/ares.php","tags":{"FILE_LINE_OF_CODE":"10"}}
-{"ts":1777020457495,"action":"FILE_OPENED","file":"/resources/lang/en/ares.php","tags":{"FILE_LINE_OF_CODE":"10"}}
-{"ts":1777020463463,"action":"FILE_CLOSED","file":"/resources/lang/en/ares.php","tags":{"FILE_LINE_OF_CODE":"10"}}
-{"ts":1777020463463,"action":"FILE_OPENED","file":"/resources/lang/cs/ares.php","tags":{"FILE_LINE_OF_CODE":"10"}}
-{"ts":1777020470814,"action":"FILE_CLOSED","file":"/resources/lang/cs/ares.php","tags":{"FILE_LINE_OF_CODE":"10"}}
-{"ts":1777020470814,"action":"FILE_OPENED","file":"/src/Commands/TestAresCommand.php","tags":{"FILE_LINE_OF_CODE":"41"}}
-{"ts":1777020475221,"action":"FILE_CLOSED","file":"/src/Commands/TestAresCommand.php","tags":{"FILE_LINE_OF_CODE":"41"}}
-{"ts":1777020475221,"action":"FILE_OPENED","file":"/src/Providers/AresServiceProvider.php","tags":{"FILE_LINE_OF_CODE":"61"}}
-{"ts":1777020533935,"action":"IDE_DEACTIVATED"}
-{"ts":1777020640646,"action":"IDE_ACTIVATED"}
-{"ts":1777020667642,"action":"IDE_DEACTIVATED"}
-{"ts":1777020669050,"action":"IDE_ACTIVATED"}
-{"ts":1777020670702,"action":"IDE_DEACTIVATED"}
-{"ts":1777020673142,"action":"IDE_ACTIVATED"}
-{"ts":1777020675969,"action":"IDE_DEACTIVATED"}
-{"ts":1777020677239,"action":"IDE_ACTIVATED"}
-{"ts":1777020736932,"action":"IDE_DEACTIVATED"}
-{"ts":1777020866482,"action":"IDE_ACTIVATED"}
-{"ts":1777020869630,"action":"FILE_CLOSED","file":"/src/Providers/AresServiceProvider.php","tags":{"FILE_LINE_OF_CODE":"63"}}
-{"ts":1777020902352,"action":"FILE_OPENED","file":"/tests/Pest.php","tags":{"FILE_LINE_OF_CODE":"8"}}
-{"ts":1777020903836,"action":"FILE_CLOSED","file":"/tests/Pest.php","tags":{"FILE_LINE_OF_CODE":"8"}}
-{"ts":1777020903836,"action":"FILE_OPENED","file":"/resources/lang/en/ares.php","tags":{"FILE_LINE_OF_CODE":"10"}}
-{"ts":1777020904961,"action":"FILE_CLOSED","file":"/resources/lang/en/ares.php","tags":{"FILE_LINE_OF_CODE":"10"}}
-{"ts":1777020904961,"action":"FILE_OPENED","file":"/resources/lang/cs/ares.php","tags":{"FILE_LINE_OF_CODE":"10"}}
-{"ts":1777020908392,"action":"FILE_CLOSED","file":"/resources/lang/cs/ares.php","tags":{"FILE_LINE_OF_CODE":"10"}}
-{"ts":1777020908392,"action":"FILE_OPENED","file":"/src/Commands/TestAresCommand.php","tags":{"FILE_LINE_OF_CODE":"47"}}
-{"ts":1777020933493,"action":"FILE_CLOSED","file":"/src/Commands/TestAresCommand.php","tags":{"FILE_LINE_OF_CODE":"47"}}
-{"ts":1777020943119,"action":"IDE_DEACTIVATED"}
-{"ts":1777020944410,"action":"IDE_ACTIVATED"}
-{"ts":1777020950020,"action":"FILE_OPENED","file":"/composer.json","tags":{"FILE_LINE_OF_CODE":"56"}}
-{"ts":1777020961763,"action":"FILE_CLOSED","file":"/composer.json","tags":{"FILE_LINE_OF_CODE":"56"}}
-{"ts":1777020961763,"action":"FILE_OPENED","file":"/src/Facades/Ares.php","tags":{"FILE_LINE_OF_CODE":"21"}}
-{"ts":1777021032651,"action":"IDE_DEACTIVATED"}
-{"ts":1777021267247,"action":"IDE_ACTIVATED"}
-{"ts":1777021271933,"action":"FILE_CLOSED","file":"/src/Facades/Ares.php","tags":{"FILE_LINE_OF_CODE":"27"}}
-{"ts":1777021332150,"action":"IDE_DEACTIVATED"}
-{"ts":1777021859583,"action":"IDE_ACTIVATED"}
-{"ts":1777021930823,"action":"IDE_DEACTIVATED"}
-{"ts":1777021968339,"action":"IDE_ACTIVATED"}
-{"ts":1777022056835,"action":"IDE_DEACTIVATED"}
-{"ts":1777022165233,"action":"IDE_ACTIVATED"}
-{"ts":1777022201040,"action":"FILE_OPENED","file":"/src/Facades/Ares.php","tags":{"FILE_LINE_OF_CODE":"27"}}
-{"ts":1777022240302,"action":"IDE_DEACTIVATED"}
-{"ts":1777022495899,"action":"IDE_ACTIVATED"}
-{"ts":1777022501167,"action":"FILE_CLOSED","file":"/src/Facades/Ares.php","tags":{"FILE_LINE_OF_CODE":"27"}}
-{"ts":1777022501167,"action":"FILE_OPENED","file":"/src/Data/AddressData.php","tags":{"FILE_LINE_OF_CODE":"138"}}
-{"ts":1777022502238,"action":"FILE_CLOSED","file":"/src/Data/AddressData.php","tags":{"FILE_LINE_OF_CODE":"138"}}
-{"ts":1777022502238,"action":"FILE_OPENED","file":"/src/Data/CompanyData.php","tags":{"FILE_LINE_OF_CODE":"75"}}
-{"ts":1777022514264,"action":"FILE_CLOSED","file":"/src/Data/CompanyData.php","tags":{"FILE_LINE_OF_CODE":"75"}}
-{"ts":1777022514264,"action":"FILE_OPENED","file":"/src/Data/DeliveryAddressData.php","tags":{"FILE_LINE_OF_CODE":"49"}}
-{"ts":1777022518905,"action":"FILE_CLOSED","file":"/src/Data/DeliveryAddressData.php","tags":{"FILE_LINE_OF_CODE":"49"}}
-{"ts":1777022518905,"action":"FILE_OPENED","file":"/src/Data/AddressData.php","tags":{"FILE_LINE_OF_CODE":"138"}}
-{"ts":1777022524333,"action":"FILE_CLOSED","file":"/src/Data/AddressData.php","tags":{"FILE_LINE_OF_CODE":"138"}}
-{"ts":1777022524333,"action":"FILE_OPENED","file":"/phpstan.neon.dist","tags":{"FILE_LINE_OF_CODE":"19"}}
-{"ts":1777022558823,"action":"IDE_DEACTIVATED"}
-{"ts":1777022578297,"action":"IDE_ACTIVATED"}
-{"ts":1777022595706,"action":"IDE_DEACTIVATED"}
-{"ts":1777022643025,"action":"IDE_ACTIVATED"}
-{"ts":1777022694803,"action":"IDE_DEACTIVATED"}
-{"ts":1777022798895,"action":"IDE_ACTIVATED"}
-{"ts":1777023046053,"action":"IDE_DEACTIVATED"}
-{"ts":1777023264942,"action":"IDE_ACTIVATED"}
-{"ts":1777023305628,"action":"FILE_OPENED","file":"/src/Facades/Ares.php","tags":{"FILE_LINE_OF_CODE":"27"}}
-{"ts":1777023576283,"action":"IDE_DEACTIVATED"}
-{"ts":1777023576357,"action":"IDE_ACTIVATED"}
-{"ts":1777023600030,"action":"FILE_CLOSED","file":"/src/Facades/Ares.php","tags":{"FILE_LINE_OF_CODE":"27"}}
-{"ts":1777023600030,"action":"FILE_OPENED","file":"/composer.json","tags":{"FILE_LINE_OF_CODE":"61"}}
-{"ts":1777023637317,"action":"IDE_DEACTIVATED"}
-{"ts":1777023650285,"action":"IDE_ACTIVATED"}
-{"ts":1777023656612,"action":"IDE_DEACTIVATED"}
-{"ts":1777023660580,"action":"IDE_ACTIVATED"}
-{"ts":1777023666262,"action":"IDE_DEACTIVATED"}
-{"ts":1777023667798,"action":"IDE_ACTIVATED"}
-{"ts":1777023699997,"action":"FILE_CLOSED","file":"/composer.json","tags":{"FILE_LINE_OF_CODE":"61"}}
-{"ts":1777023705701,"action":"FILE_OPENED","file":"/composer.json","tags":{"FILE_LINE_OF_CODE":"61"}}
-{"ts":1777023780977,"action":"IDE_DEACTIVATED"}
-{"ts":1777023783317,"action":"IDE_ACTIVATED"}
-{"ts":1777023789748,"action":"IDE_DEACTIVATED"}
-{"ts":1777023789826,"action":"IDE_ACTIVATED"}
-{"ts":1777023813462,"action":"FILE_CLOSED","file":"/composer.json","tags":{"FILE_LINE_OF_CODE":"61"}}
-{"ts":1777023813462,"action":"FILE_OPENED","file":"/phpstan.neon.dist","tags":{"FILE_LINE_OF_CODE":"13"}}
-{"ts":1777023825527,"action":"FILE_CLOSED","file":"/phpstan.neon.dist","tags":{"FILE_LINE_OF_CODE":"13"}}
-{"ts":1777023825527,"action":"FILE_OPENED","file":"/.phpstan/resultCache.php","tags":{"FILE_LINE_OF_CODE":"4584"}}
-{"ts":1777023830247,"action":"FILE_CLOSED","file":"/.phpstan/resultCache.php","tags":{"FILE_LINE_OF_CODE":"4584"}}
-{"ts":1777023830247,"action":"FILE_OPENED","file":"/resources/lang/en/ares.php","tags":{"FILE_LINE_OF_CODE":"10"}}
-{"ts":1777023832022,"action":"FILE_CLOSED","file":"/resources/lang/en/ares.php","tags":{"FILE_LINE_OF_CODE":"10"}}
-{"ts":1777023832022,"action":"FILE_OPENED","file":"/src/Commands/TestAresCommand.php","tags":{"FILE_LINE_OF_CODE":"54"}}
-{"ts":1777023833639,"action":"FILE_CLOSED","file":"/src/Commands/TestAresCommand.php","tags":{"FILE_LINE_OF_CODE":"54"}}
-{"ts":1777023833639,"action":"FILE_OPENED","file":"/src/Contracts/AresClientInterface.php","tags":{"FILE_LINE_OF_CODE":"26"}}
-{"ts":1777023834669,"action":"FILE_CLOSED","file":"/src/Contracts/AresClientInterface.php","tags":{"FILE_LINE_OF_CODE":"26"}}
-{"ts":1777023834669,"action":"FILE_OPENED","file":"/src/Data/AddressData.php","tags":{"FILE_LINE_OF_CODE":"139"}}
-{"ts":1777023835307,"action":"FILE_CLOSED","file":"/src/Data/AddressData.php","tags":{"FILE_LINE_OF_CODE":"139"}}
-{"ts":1777023835307,"action":"FILE_OPENED","file":"/src/Contracts/AresClientInterface.php","tags":{"FILE_LINE_OF_CODE":"26"}}
-{"ts":1777023836084,"action":"FILE_CLOSED","file":"/src/Contracts/AresClientInterface.php","tags":{"FILE_LINE_OF_CODE":"26"}}
-{"ts":1777023836084,"action":"FILE_OPENED","file":"/src/Commands/TestAresCommand.php","tags":{"FILE_LINE_OF_CODE":"54"}}
-{"ts":1777023837237,"action":"FILE_CLOSED","file":"/src/Commands/TestAresCommand.php","tags":{"FILE_LINE_OF_CODE":"54"}}
-{"ts":1777023837237,"action":"FILE_OPENED","file":"/src/Contracts/AresClientInterface.php","tags":{"FILE_LINE_OF_CODE":"26"}}
-{"ts":1777023837800,"action":"FILE_CLOSED","file":"/src/Contracts/AresClientInterface.php","tags":{"FILE_LINE_OF_CODE":"26"}}
-{"ts":1777023837800,"action":"FILE_OPENED","file":"/src/Data/AddressData.php","tags":{"FILE_LINE_OF_CODE":"139"}}
-{"ts":1777023846277,"action":"FILE_CLOSED","file":"/src/Data/AddressData.php","tags":{"FILE_LINE_OF_CODE":"139"}}
-{"ts":1777023846278,"action":"FILE_OPENED","file":"/src/Enums/RegistrationSourceState.php","tags":{"FILE_LINE_OF_CODE":"18"}}
-{"ts":1777023847379,"action":"FILE_CLOSED","file":"/src/Enums/RegistrationSourceState.php","tags":{"FILE_LINE_OF_CODE":"18"}}
-{"ts":1777023847379,"action":"FILE_OPENED","file":"/src/Events/CompanyLookupFailed.php","tags":{"FILE_LINE_OF_CODE":"17"}}
-{"ts":1777023847694,"action":"FILE_CLOSED","file":"/src/Events/CompanyLookupFailed.php","tags":{"FILE_LINE_OF_CODE":"17"}}
-{"ts":1777023847694,"action":"FILE_OPENED","file":"/src/Events/CompanyLookupSucceeded.php","tags":{"FILE_LINE_OF_CODE":"13"}}
-{"ts":1777023848765,"action":"FILE_CLOSED","file":"/src/Events/CompanyLookupSucceeded.php","tags":{"FILE_LINE_OF_CODE":"13"}}
-{"ts":1777023848765,"action":"FILE_OPENED","file":"/src/Exceptions/CompanyNotFoundException.php","tags":{"FILE_LINE_OF_CODE":"16"}}
-{"ts":1777023849079,"action":"FILE_CLOSED","file":"/src/Exceptions/CompanyNotFoundException.php","tags":{"FILE_LINE_OF_CODE":"16"}}
-{"ts":1777023849079,"action":"FILE_OPENED","file":"/src/Exceptions/InvalidApiResponseException.php","tags":{"FILE_LINE_OF_CODE":"21"}}
-{"ts":1777023849314,"action":"FILE_CLOSED","file":"/src/Exceptions/InvalidApiResponseException.php","tags":{"FILE_LINE_OF_CODE":"21"}}
-{"ts":1777023849314,"action":"FILE_OPENED","file":"/src/Exceptions/InvalidIcException.php","tags":{"FILE_LINE_OF_CODE":"16"}}
-{"ts":1777023850081,"action":"FILE_CLOSED","file":"/src/Exceptions/InvalidIcException.php","tags":{"FILE_LINE_OF_CODE":"16"}}
-{"ts":1777023850081,"action":"FILE_OPENED","file":"/src/Facades/Ares.php","tags":{"FILE_LINE_OF_CODE":"27"}}
-{"ts":1777023850796,"action":"FILE_CLOSED","file":"/src/Facades/Ares.php","tags":{"FILE_LINE_OF_CODE":"27"}}
-{"ts":1777023850796,"action":"FILE_OPENED","file":"/src/Providers/AresServiceProvider.php","tags":{"FILE_LINE_OF_CODE":"84"}}
-{"ts":1777023852737,"action":"FILE_CLOSED","file":"/src/Providers/AresServiceProvider.php","tags":{"FILE_LINE_OF_CODE":"84"}}
-{"ts":1777023852737,"action":"FILE_OPENED","file":"/src/Exceptions/InvalidIcException.php","tags":{"FILE_LINE_OF_CODE":"16"}}
-{"ts":1777023860757,"action":"FILE_CLOSED","file":"/src/Exceptions/InvalidIcException.php","tags":{"FILE_LINE_OF_CODE":"16"}}
-{"ts":1777023860757,"action":"FILE_OPENED","file":"/tests/Feature/AresClientTest.php","tags":{"FILE_LINE_OF_CODE":"244"}}
-{"ts":1777023861028,"action":"FILE_CLOSED","file":"/tests/Feature/AresClientTest.php","tags":{"FILE_LINE_OF_CODE":"244"}}
-{"ts":1777023861028,"action":"FILE_OPENED","file":"/tests/Feature/TestAresCommandTest.php","tags":{"FILE_LINE_OF_CODE":"140"}}
-{"ts":1777023861371,"action":"FILE_CLOSED","file":"/tests/Feature/TestAresCommandTest.php","tags":{"FILE_LINE_OF_CODE":"140"}}
-{"ts":1777023861371,"action":"FILE_OPENED","file":"/tests/Pest.php","tags":{"FILE_LINE_OF_CODE":"8"}}
-{"ts":1777023862214,"action":"FILE_CLOSED","file":"/tests/Pest.php","tags":{"FILE_LINE_OF_CODE":"8"}}
-{"ts":1777023862214,"action":"FILE_OPENED","file":"/tests/Unit/CompanyDataTest.php","tags":{"FILE_LINE_OF_CODE":"116"}}
-{"ts":1777023879329,"action":"FILE_CLOSED","file":"/tests/Unit/CompanyDataTest.php","tags":{"FILE_LINE_OF_CODE":"116"}}
-{"ts":1777023879329,"action":"FILE_OPENED","file":"/README.md","tags":{"FILE_LINE_OF_CODE":"177"}}
-{"ts":1777023893214,"action":"IDE_DEACTIVATED"}
-{"ts":1777023893283,"action":"IDE_ACTIVATED"}
-{"ts":1777023912506,"action":"IDE_DEACTIVATED"}
-{"ts":1777023959432,"action":"IDE_ACTIVATED"}
-{"ts":1777023964248,"action":"IDE_DEACTIVATED"}
-{"ts":1777023977962,"action":"IDE_ACTIVATED"}
-{"ts":1777023990966,"action":"IDE_DEACTIVATED"}
-{"ts":1777024003962,"action":"IDE_ACTIVATED"}
-{"ts":1777024031574,"action":"IDE_DEACTIVATED"}
-{"ts":1777024031653,"action":"IDE_ACTIVATED"}
-{"ts":1777024244369,"action":"IDE_DEACTIVATED"}
-{"ts":1777024244737,"action":"IDE_ACTIVATED"}
-{"ts":1777024281303,"action":"FILE_CLOSED","file":"/README.md","tags":{"FILE_LINE_OF_CODE":"177"}}
-{"ts":1777024281303,"action":"FILE_OPENED","file":"/tests/Unit/ServiceProviderTest.php","tags":{"FILE_LINE_OF_CODE":"12"}}
-{"ts":1777024281827,"action":"FILE_CLOSED","file":"/tests/Unit/ServiceProviderTest.php","tags":{"FILE_LINE_OF_CODE":"12"}}
-{"ts":1777024281827,"action":"FILE_OPENED","file":"/tests/Unit/CompanyDataTest.php","tags":{"FILE_LINE_OF_CODE":"116"}}
-{"ts":1777024292789,"action":"FILE_CLOSED","file":"/tests/Unit/CompanyDataTest.php","tags":{"FILE_LINE_OF_CODE":"116"}}
-{"ts":1777024292789,"action":"FILE_OPENED","file":"/.github/workflows/ci.yml","tags":{"FILE_LINE_OF_CODE":"88"}}
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
deleted file mode 100644
index 35eb1dd..0000000
--- a/.idea/vcs.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..a278acf
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,28 @@
+# Changelog
+
+All notable changes to `laravel-ares` will be documented in this file.
+
+The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
+and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+
+## [0.0.6]
+
+### Removed
+- Removed the experimental Livewire search/lookup components (`AresSearch`,
+ `AresLookup`) and the `livewire/livewire` runtime dependency. The package is
+ now a lean API/indexing library; UI can be built on top of `Ares::search()`.
+
+### Fixed
+- Subject name search now performs a true substring match (`LIKE '%term%'`) on
+ every database driver. Previously the MySQL branch used an invalid
+ `MATCH … AGAINST('*term*' IN BOOLEAN MODE)` expression whose leading `*` was
+ ignored, silently degrading to prefix matching and diverging from the
+ documented behaviour.
+- Fixed a broken PHPStan configuration (`excludePaths` referenced a removed
+ directory) and applied Pint formatting, restoring a green CI pipeline.
+
+### Changed
+- The `ares_subjects` migration no longer creates an unused MySQL `FULLTEXT`
+ index (it does not accelerate `LIKE` substring queries). PostgreSQL keeps its
+ trigram GIN index; other drivers use a plain index on `name`.
+- Stopped tracking IDE (`.idea/`) and tooling cache directories in git.
diff --git a/README.md b/README.md
index aa0f9ea..9180824 100644
--- a/README.md
+++ b/README.md
@@ -11,6 +11,7 @@
- Events for successful and failed lookups
- ICO normalization and checksum validation
- Explicit exceptions for invalid ICO and missing companies
+- Subject indexing with database-backed autocomplete search
- Pest test suite, PHPStan configuration, and GitHub Actions CI
## Requirements
@@ -41,6 +42,9 @@ php artisan vendor:publish --tag=laravel-ares::config
| `log_channel` | `stack` | Laravel log channel used for client errors |
| `http_options.timeout` | `5.0` | Request timeout in seconds |
| `http_options.connect_timeout` | `3.0` | Connection timeout in seconds |
+| `indexing.enabled` | `true` | Enable subject indexing and search |
+| `indexing.auto_index` | `true` | Automatically index subjects on successful lookup |
+| `indexing.stale_days` | `30` | Number of days before a record is considered stale |
Environment overrides:
@@ -49,6 +53,9 @@ Environment overrides:
- `ARES_LOG_CHANNEL`
- `ARES_HTTP_TIMEOUT`
- `ARES_HTTP_CONNECT_TIMEOUT`
+- `ARES_INDEXING_ENABLED`
+- `ARES_AUTO_INDEX`
+- `ARES_STALE_DAYS`
## Usage
@@ -91,6 +98,7 @@ Public API:
- `forgetCompany(string $ic): bool`
- `isValidIc(string $ic): bool`
- `normalizeIc(string $ic): string`
+- `search(string $query, int $limit = 10): Collection`
## Domain Model
@@ -117,6 +125,7 @@ Related DTOs:
- `RegistrationData` groups legal form, dates, source, file mark, NACE codes, and source statuses
- `RegistrationStatusData` represents one registry source status
- `RegistrationSourceState` is a typed enum for known ARES status values
+- `SubjectData` is a lightweight DTO for autocomplete search results (`ic`, `name`, `city`)
`rawData` remains available as an escape hatch for fields the package does not map yet.
@@ -136,15 +145,73 @@ The package dispatches:
- `NyonCode\Ares\Events\CompanyLookupSucceeded`
- `NyonCode\Ares\Events\CompanyLookupFailed`
-## Artisan Command
+## Subject Indexing and Autocomplete
-The package includes an artisan helper for manual verification:
+The package can index looked-up subjects into a local database table for fast autocomplete search.
+
+Run the migration after installing:
+
+```bash
+php artisan migrate
+```
+
+Search indexed subjects by name or IC:
+
+```php
+// Search by company name
+$results = Ares::search('Asseco');
+
+// Search by IC prefix
+$results = Ares::search('2707', 5);
+
+// Using the global helper
+$results = ares_search('Skoda');
+```
+
+Each result is a `SubjectData` with `ic`, `name`, and `city` properties.
+
+### Auto-indexing
+
+When `indexing.auto_index` is enabled (default), every successful `findCompany()` call dispatches a queued job that indexes the subject automatically. No extra code needed.
+
+### Manual Indexing
```bash
+# Index specific subjects
+php artisan ares:index 27074358 25596641
+
+# Refresh stale records (older than configured stale_days)
+php artisan ares:index --refresh-stale
+
+# Custom stale threshold and limit
+php artisan ares:index --refresh-stale --stale-days=14 --limit=200
+```
+
+Schedule the refresh in your application's scheduler for automatic maintenance:
+
+```php
+$schedule->command('ares:index --refresh-stale')->daily();
+```
+
+## Artisan Commands
+
+The package includes artisan commands for diagnostics and indexing:
+
+```bash
+# Test ARES API connectivity
php artisan ares:test 27074358
+
+# Index subjects
+php artisan ares:index 27074358
+
+# Show indexing statistics
+php artisan ares:index
+
+# Refresh stale records
+php artisan ares:index --refresh-stale
```
-The command renders a compact company summary including DIC, source, dates, registered office, delivery address, and register metadata.
+`ares:test` renders a compact company summary including DIC, source, dates, registered office, delivery address, and register metadata.
## Quality Gates
@@ -174,10 +241,12 @@ The repository includes a GitHub Actions workflow for:
## Development Notes
-- Successful lookups are cached under the `ares:company:{ic}` key format.
+- Successful lookups are cached under the `ares:v1:company:{ic}` key format.
- Invalid ICO values are rejected before any HTTP request is sent.
- `forgetCompany()` invalidates cache entries using normalized ICO values.
- Failed HTTP responses, malformed payloads, and transport exceptions all dispatch `CompanyLookupFailed`.
+- Auto-indexed subjects are stored in the `ares_subjects` table with a minimal footprint (`ic`, `name`, `city`, `indexed_at`).
+- Search uses `LIKE` queries with database indexes for fast prefix/substring matching.
## License
diff --git a/config/ares.php b/config/ares.php
index 0b21d09..25d31f4 100644
--- a/config/ares.php
+++ b/config/ares.php
@@ -31,4 +31,17 @@
'timeout' => env('ARES_HTTP_TIMEOUT', 5.0),
'connect_timeout' => env('ARES_HTTP_CONNECT_TIMEOUT', 3.0),
],
+
+ /*
+ |--------------------------------------------------------------------------
+ | Subject Indexing
+ |--------------------------------------------------------------------------
+ */
+ 'indexing' => [
+ 'enabled' => env('ARES_INDEXING_ENABLED', true),
+ 'auto_index' => env('ARES_AUTO_INDEX', true),
+ 'stale_days' => env('ARES_STALE_DAYS', 30),
+ 'queue' => env('ARES_INDEX_QUEUE'),
+ 'connection' => env('ARES_INDEX_CONNECTION'),
+ ],
];
diff --git a/database/migrations/create_ares_subjects_table.php b/database/migrations/create_ares_subjects_table.php
new file mode 100644
index 0000000..74d893a
--- /dev/null
+++ b/database/migrations/create_ares_subjects_table.php
@@ -0,0 +1,38 @@
+char('ic', 8)->primary();
+ $table->string('name');
+ $table->string('city', 100)->nullable();
+ $table->timestamp('indexed_at')->useCurrent();
+ });
+
+ // Name search uses substring matching (LIKE '%term%'). On PostgreSQL a
+ // trigram GIN index accelerates that; other drivers fall back to a plain
+ // index, which still helps ordering and prefix lookups.
+ if (Schema::getConnection()->getDriverName() === 'pgsql') {
+ Schema::getConnection()->statement(
+ 'CREATE INDEX ares_subjects_name_trgm ON ares_subjects USING GIN (name gin_trgm_ops)'
+ );
+ } else {
+ Schema::table('ares_subjects', function (Blueprint $table) {
+ $table->index('name');
+ });
+ }
+ }
+
+ public function down(): void
+ {
+ Schema::dropIfExists('ares_subjects');
+ }
+};
diff --git a/docs/README.md b/docs/README.md
index c6cf296..646b817 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -7,6 +7,7 @@ A comprehensive Laravel package for interacting with the Czech ARES (Administrat
- [Installation](installation.md)
- [Configuration](configuration.md)
- [Usage Examples](usage.md)
+- [Subject Indexing & Autocomplete](indexing.md)
- [Helper Functions](helpers.md)
- [API Reference](api.md)
- [Events](events.md)
@@ -19,8 +20,9 @@ The Laravel ARES package provides a simple and elegant way to interact with the
- **Caching**: Built-in caching support to reduce API calls
- **Events**: Laravel events for successful and failed lookups
- **Validation**: IC (identification number) format validation
+- **Subject Indexing**: Database-backed indexing with autocomplete search
- **Helper Functions**: Global helper functions for common operations
-- **Artisan Commands**: Command-line tools for testing and debugging
+- **Artisan Commands**: Command-line tools for testing, debugging, and indexing
- **Type Safety**: Full PHP 8.2+ type safety and strict typing
## Quick Start
@@ -41,6 +43,10 @@ if (ares_is_company_active('12345678')) {
// Using facade
$company = Ares::findCompanyOrFail('12345678');
+// Autocomplete search from indexed subjects
+$results = Ares::search('Asseco'); // search by name
+$results = Ares::search('2707', 5); // search by IC prefix
+
// Using fluent API - most elegant way
$companies = ares()
->findMany(['12345678', '87654321'])
@@ -77,6 +83,12 @@ $stats = ares()
- Filtering and searching capabilities
- Formatted display data
+### 🔎 Subject Indexing & Autocomplete
+- Database-backed subject index for fast local search
+- Automatic indexing on successful lookups (queued job)
+- Artisan command for manual and scheduled indexing
+- Stale record detection and refresh
+
### 🎯 Helper Functions
- Global helper functions like `ares()`, `ares_is_company_active()`
- Facade-based methods
diff --git a/docs/api.md b/docs/api.md
index 16a75fd..4b93897 100644
--- a/docs/api.md
+++ b/docs/api.md
@@ -19,6 +19,7 @@ interface AresClientInterface
public function forgetCompany(string $ic): bool;
public function isValidIc(string $ic): bool;
public function normalizeIc(string $ic): string;
+ public function search(string $query, int $limit = 10): Collection;
}
```
@@ -150,8 +151,40 @@ $normalized = $ares->normalizeIc('123 456 78');
echo $normalized; // '12345678'
```
+##### search(string $query, int $limit = 10): Collection
+
+Search indexed subjects by name or IC for autocomplete.
+
+**Parameters:**
+- `$query` (string) - Search query (digits search by IC prefix, text searches by name)
+- `$limit` (int) - Maximum number of results (default: 10)
+
+**Returns:**
+- `Collection` - Collection of matching subjects
+
+**Example:**
+```php
+// Search by name
+$results = $ares->search('Asseco');
+
+// Search by IC prefix
+$results = $ares->search('2707', 5);
+```
+
## Data Classes
+### SubjectData
+
+Lightweight DTO for autocomplete search results.
+
+#### Properties
+
+```php
+public readonly string $ic;
+public readonly string $name;
+public readonly ?string $city;
+```
+
### CompanyData
Represents a company with all its information.
@@ -297,8 +330,57 @@ Ares::findCompanyOrFail($ic);
Ares::forgetCompany($ic);
Ares::isValidIc($ic);
Ares::normalizeIc($ic);
+Ares::search($query, $limit);
+```
+
+## Jobs
+
+### IndexAresSubject
+
+Queued job for indexing a subject into the `ares_subjects` table.
+
+#### Static Factory
+
+```php
+IndexAresSubject::fromCompanyData(CompanyData $company): self
```
+Creates an `IndexAresSubject` job from a `CompanyData` object. Extracts `ic`, `name`, and `city` automatically.
+
+#### Behavior
+
+- Uses `updateOrCreate` to insert or update the subject
+- Sets `indexed_at` to the current timestamp
+- Runs on the default queue (or synchronously with `sync` driver)
+
+## Services
+
+### SubjectSearchService
+
+Service for searching and managing indexed subjects.
+
+#### Methods
+
+##### search(string $query, int $limit = 10): Collection
+
+Search indexed subjects. Digits search by IC prefix, text by name substring.
+
+##### indexSubject(string $ic, string $name, ?string $city): void
+
+Index a single subject directly (without a queued job).
+
+##### subjectCount(): int
+
+Return the total number of indexed subjects.
+
+##### staleCount(int $days): int
+
+Return the number of records older than the given number of days.
+
+##### staleSubjects(int $days, int $limit = 100): Collection
+
+Return stale subject records for re-indexing.
+
## Artisan Commands
### TestAresCommand
@@ -319,6 +401,36 @@ php artisan ares:test {ic}
php artisan ares:test 12345678
```
+### IndexAresCommand
+
+Index ARES subjects for autocomplete search.
+
+#### Usage
+
+```bash
+php artisan ares:index {ics?*} {--refresh-stale} {--stale-days=} {--limit=100}
+```
+
+**Parameters:**
+- `ics` (optional) - One or more IC numbers to index
+
+**Options:**
+- `--refresh-stale` - Re-index stale records
+- `--stale-days=N` - Override configured stale days threshold
+- `--limit=N` - Maximum number of records to refresh (default: 100)
+
+**Examples:**
+```bash
+# Index specific subjects
+php artisan ares:index 27074358 25596641
+
+# Show statistics
+php artisan ares:index
+
+# Refresh stale records
+php artisan ares:index --refresh-stale --stale-days=14 --limit=200
+```
+
## Events
### CompanyLookupSucceeded
diff --git a/docs/configuration.md b/docs/configuration.md
index dde7e63..afc5614 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -7,7 +7,7 @@ This guide covers all configuration options available in the Laravel ARES packag
Publish the configuration file to customize package settings:
```bash
-php artisan vendor:publish --tag="ares-config"
+php artisan vendor:publish --tag=laravel-ares::config
```
This creates `config/ares.php` with the following structure:
@@ -23,6 +23,11 @@ return [
'timeout' => env('ARES_HTTP_TIMEOUT', 5.0),
'connect_timeout' => env('ARES_HTTP_CONNECT_TIMEOUT', 3.0),
],
+ 'indexing' => [
+ 'enabled' => env('ARES_INDEXING_ENABLED', true),
+ 'auto_index' => env('ARES_AUTO_INDEX', true),
+ 'stale_days' => env('ARES_STALE_DAYS', 30),
+ ],
];
```
@@ -131,6 +136,56 @@ ARES_LOG_CHANNEL=stack
'log_channel' => 'ares',
```
+### indexing
+
+Configuration for subject indexing and autocomplete search.
+
+#### indexing.enabled
+
+Enable or disable the indexing feature.
+
+**Type:** `bool`
+**Default:** `true`
+**Environment Variable:** `ARES_INDEXING_ENABLED`
+
+When disabled, `search()` returns an empty collection and no `IndexAresSubject` jobs are dispatched.
+
+#### indexing.auto_index
+
+Automatically index subjects on successful `findCompany()` calls.
+
+**Type:** `bool`
+**Default:** `true`
+**Environment Variable:** `ARES_AUTO_INDEX`
+
+When enabled, a queued `IndexAresSubject` job is dispatched after each successful lookup.
+
+#### indexing.stale_days
+
+Number of days before an indexed record is considered stale.
+
+**Type:** `int`
+**Default:** `30`
+**Environment Variable:** `ARES_STALE_DAYS`
+
+Used by `php artisan ares:index --refresh-stale` to determine which records need refreshing.
+
+#### Examples
+
+```env
+# Enable auto-indexing (default)
+ARES_INDEXING_ENABLED=true
+ARES_AUTO_INDEX=true
+ARES_STALE_DAYS=30
+
+# Disable auto-indexing but keep search enabled
+ARES_INDEXING_ENABLED=true
+ARES_AUTO_INDEX=false
+
+# Disable indexing entirely
+ARES_INDEXING_ENABLED=false
+```
+
### http_options
HTTP client configuration for API requests.
@@ -195,6 +250,11 @@ ARES_CACHE_TTL=86400
ARES_LOG_CHANNEL=stack
ARES_HTTP_TIMEOUT=5.0
ARES_HTTP_CONNECT_TIMEOUT=3.0
+
+# Indexing
+ARES_INDEXING_ENABLED=true
+ARES_AUTO_INDEX=true
+ARES_STALE_DAYS=30
```
### Environment-Specific Configurations
diff --git a/docs/faq.md b/docs/faq.md
index d4f14cf..e459b1f 100644
--- a/docs/faq.md
+++ b/docs/faq.md
@@ -33,7 +33,7 @@ composer require nyoncode/laravel-ares
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"
+php artisan vendor:publish --tag=laravel-ares::config
```
### The helper functions are not available. What should I do?
@@ -340,6 +340,50 @@ class CompanyTest extends TestCase
No, use mock data or test IC numbers provided in the documentation. Avoid using real company data in automated tests.
+## Subject Indexing
+
+### What is subject indexing?
+
+Subject indexing stores basic company data (IC, name, city) in a local database table for fast autocomplete search. Instead of querying the ARES API for every search keystroke, you search your local index.
+
+### How do I set up indexing?
+
+Run the migration and enable indexing in your config (enabled by default):
+
+```bash
+php artisan migrate
+```
+
+### How does auto-indexing work?
+
+When `indexing.auto_index` is enabled, every successful `findCompany()` call dispatches a queued `IndexAresSubject` job. The index grows organically as your application looks up companies.
+
+### How do I search the index?
+
+```php
+use NyonCode\Ares\Facades\Ares;
+
+$results = Ares::search('Asseco'); // search by name
+$results = Ares::search('2707', 5); // search by IC prefix
+$results = ares_search('Skoda'); // global helper
+```
+
+### Can I disable indexing?
+
+Yes, set `ARES_INDEXING_ENABLED=false` in your `.env` file. When disabled, `search()` returns an empty collection and no jobs are dispatched.
+
+### How do I keep the index fresh?
+
+Schedule the refresh command in your scheduler:
+
+```php
+$schedule->command('ares:index --refresh-stale')->daily();
+```
+
+### Can I use the search without auto-indexing?
+
+Yes. Set `ARES_AUTO_INDEX=false` and use `php artisan ares:index` to manually index subjects.
+
## Security and Privacy
### Is the data from ARES public?
@@ -377,7 +421,7 @@ The package only accesses public company information. No sensitive personal data
### What's the roadmap?
-- Enhanced filtering and search capabilities
+- Full-text search support for indexed subjects
- Additional data sources
- Performance optimizations
- More helper functions
diff --git a/docs/helpers.md b/docs/helpers.md
index 09a2a5a..b960baf 100644
--- a/docs/helpers.md
+++ b/docs/helpers.md
@@ -142,6 +142,23 @@ if (ares_validate_ic('12345678')) {
}
```
+#### ares_search(string $query, int $limit = 10): Collection
+
+Search indexed subjects for autocomplete.
+
+```php
+// Search by name
+$results = ares_search('Asseco');
+
+// Search by IC prefix with limit
+$results = ares_search('2707', 5);
+
+// Each result is a SubjectData(ic, name, city)
+foreach ($results as $subject) {
+ echo "{$subject->name} ({$subject->ic})";
+}
+```
+
#### ares_normalize_ic(string $ic): string
Normalize IC to 8-digit format.
diff --git a/docs/indexing.md b/docs/indexing.md
new file mode 100644
index 0000000..0c28935
--- /dev/null
+++ b/docs/indexing.md
@@ -0,0 +1,218 @@
+# Subject Indexing & Autocomplete
+
+The package can index looked-up subjects into a local database table (`ares_subjects`) for fast autocomplete search. This is useful for building typeahead/autocomplete inputs in your application.
+
+## How It Works
+
+1. Subjects are stored in a minimal `ares_subjects` table with only the fields needed for autocomplete: `ic`, `name`, `city`, and `indexed_at`
+2. Subjects can be indexed automatically (on every successful `findCompany()` call) or manually via artisan command
+3. Search queries run against the local database, not the ARES API, making them fast and reliable
+
+## Setup
+
+Run the migration to create the `ares_subjects` table:
+
+```bash
+php artisan migrate
+```
+
+The table schema:
+
+| Column | Type | Description |
+| --- | --- | --- |
+| `ic` | `CHAR(8)` PRIMARY KEY | Company identification number |
+| `name` | `VARCHAR(255)` | Company name |
+| `city` | `VARCHAR(100)` nullable | City (for disambiguation in autocomplete) |
+| `indexed_at` | `TIMESTAMP` | When the record was last indexed |
+
+## Configuration
+
+```php
+// config/ares.php
+'indexing' => [
+ 'enabled' => env('ARES_INDEXING_ENABLED', true),
+ 'auto_index' => env('ARES_AUTO_INDEX', true),
+ 'stale_days' => env('ARES_STALE_DAYS', 30),
+],
+```
+
+| Key | Default | Description |
+| --- | --- | --- |
+| `indexing.enabled` | `true` | Enable the indexing feature and search |
+| `indexing.auto_index` | `true` | Automatically index subjects on successful `findCompany()` calls |
+| `indexing.stale_days` | `30` | Number of days before a record is considered stale |
+
+To disable indexing entirely:
+
+```env
+ARES_INDEXING_ENABLED=false
+```
+
+## Searching
+
+### Using the Facade
+
+```php
+use NyonCode\Ares\Facades\Ares;
+
+// Search by company name (substring match)
+$results = Ares::search('Asseco');
+
+// Search by IC prefix
+$results = Ares::search('2707');
+
+// Limit the number of results
+$results = Ares::search('Skoda', 5);
+```
+
+### Using the Helper Function
+
+```php
+$results = ares_search('Asseco');
+$results = ares_search('2707', 5);
+```
+
+### Using Dependency Injection
+
+```php
+use NyonCode\Ares\Contracts\AresClientInterface;
+
+class AutocompleteController
+{
+ public function __construct(
+ private readonly AresClientInterface $ares,
+ ) {}
+
+ public function __invoke(Request $request): JsonResponse
+ {
+ $results = $this->ares->search(
+ $request->string('q'),
+ 10,
+ );
+
+ return response()->json($results);
+ }
+}
+```
+
+### Search Result Format
+
+Each result is a `SubjectData` DTO:
+
+```php
+NyonCode\Ares\Data\SubjectData {
+ public readonly string $ic; // '27074358'
+ public readonly string $name; // 'Asseco Central Europe, a.s.'
+ public readonly ?string $city; // 'Praha'
+}
+```
+
+### Search Behavior
+
+- If the query contains only digits, it searches by IC prefix (`LIKE '2707%'`)
+- Otherwise, it searches by name substring (`LIKE '%Asseco%'`)
+- Results are ordered by `ic` (for IC search) or `name` (for name search)
+- Empty queries return an empty collection
+
+## Auto-indexing
+
+When `indexing.auto_index` is enabled (default), every successful `findCompany()` call dispatches a queued `IndexAresSubject` job. This means your index grows organically as your application looks up companies.
+
+The job runs on your default queue. To process it:
+
+```bash
+php artisan queue:work
+```
+
+If you use the `sync` queue driver, indexing happens synchronously within the same request.
+
+## Manual Indexing via Artisan
+
+### Index Specific Subjects
+
+```bash
+php artisan ares:index 27074358 25596641
+```
+
+Each IC is looked up via the ARES API and indexed. The command reports how many were indexed and how many failed.
+
+### Show Indexing Statistics
+
+```bash
+php artisan ares:index
+```
+
+Displays the total number of indexed subjects and stale record count.
+
+### Refresh Stale Records
+
+```bash
+# Use configured stale_days (default: 30)
+php artisan ares:index --refresh-stale
+
+# Custom stale threshold
+php artisan ares:index --refresh-stale --stale-days=14
+
+# Limit the number of records to refresh per run
+php artisan ares:index --refresh-stale --limit=200
+```
+
+### Scheduling
+
+Add the refresh command to your application's scheduler for automatic maintenance:
+
+```php
+// app/Console/Kernel.php or routes/console.php
+$schedule->command('ares:index --refresh-stale')->daily();
+```
+
+## Using in API Endpoints
+
+A typical autocomplete endpoint:
+
+```php
+use Illuminate\Http\Request;
+use Illuminate\Http\JsonResponse;
+use NyonCode\Ares\Facades\Ares;
+
+Route::get('/api/companies/search', function (Request $request): JsonResponse {
+ $request->validate([
+ 'q' => 'required|string|min:2',
+ 'limit' => 'integer|min:1|max:50',
+ ]);
+
+ $results = Ares::search(
+ $request->string('q'),
+ $request->integer('limit', 10),
+ );
+
+ return response()->json($results);
+});
+```
+
+Example response:
+
+```json
+[
+ {"ic": "27074358", "name": "Asseco Central Europe, a.s.", "city": "Praha"},
+ {"ic": "27082440", "name": "Asseco Solutions, a.s.", "city": "Praha"}
+]
+```
+
+## Disabling Indexing
+
+If you don't need autocomplete, disable indexing entirely:
+
+```env
+ARES_INDEXING_ENABLED=false
+```
+
+When disabled:
+- No `IndexAresSubject` jobs are dispatched
+- `search()` returns an empty collection
+- The `ares:index` command still works for manual operations
+- The migration can be skipped
+
+---
+
+*Previous: [Usage Examples](usage.md) | Next: [Helper Functions](helpers.md)*
diff --git a/docs/installation.md b/docs/installation.md
index 8b69e14..a070320 100644
--- a/docs/installation.md
+++ b/docs/installation.md
@@ -21,17 +21,27 @@ Install the package using Composer:
composer require nyoncode/laravel-ares
```
-### 2. Publish Configuration (Optional)
+### 2. Run Migrations
+
+The package includes a migration for the subject indexing table. Run it with:
+
+```bash
+php artisan migrate
+```
+
+This creates the `ares_subjects` table used for autocomplete search. If you don't need indexing, you can skip this step and set `ARES_INDEXING_ENABLED=false`.
+
+### 3. Publish Configuration (Optional)
Publish the configuration file to customize the package settings:
```bash
-php artisan vendor:publish --tag="ares-config"
+php artisan vendor:publish --tag=laravel-ares::config
```
This will create a `config/ares.php` file in your application.
-### 3. Register Service Provider
+### 4. 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`:
@@ -42,7 +52,7 @@ The package uses Laravel's package discovery, so the service provider is automat
],
```
-### 4. Register Facade (Optional)
+### 5. Register Facade (Optional)
If you want to use the Ares facade, add it to your `config/app.php` aliases:
@@ -62,12 +72,17 @@ After publishing the configuration file, you can customize the settings in `conf
return [
'api_url' => env('ARES_API_URL', 'https://ares.gov.cz/ekonomicke-subjekty-v-be/rest'),
- 'cache_ttl' => env('ARES_CACHE_TTL', 86400), // 24 hours
+ '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),
],
+ 'indexing' => [
+ 'enabled' => env('ARES_INDEXING_ENABLED', true),
+ 'auto_index' => env('ARES_AUTO_INDEX', true),
+ 'stale_days' => env('ARES_STALE_DAYS', 30),
+ ],
];
```
@@ -82,6 +97,11 @@ ARES_CACHE_TTL=86400
ARES_LOG_CHANNEL=stack
ARES_HTTP_TIMEOUT=5.0
ARES_HTTP_CONNECT_TIMEOUT=3.0
+
+# Indexing
+ARES_INDEXING_ENABLED=true
+ARES_AUTO_INDEX=true
+ARES_STALE_DAYS=30
```
## Verification
@@ -136,7 +156,7 @@ This ensures the helper files are properly autoloaded.
If you get configuration errors, make sure to publish the config file:
```bash
-php artisan vendor:publish --tag="ares-config"
+php artisan vendor:publish --tag=laravel-ares::config
```
#### 3. Cache Issues
diff --git a/docs/usage.md b/docs/usage.md
index 169d44a..3317b1e 100644
--- a/docs/usage.md
+++ b/docs/usage.md
@@ -324,6 +324,44 @@ class CompanyController extends Controller
}
```
+## Autocomplete Search
+
+The package indexes subjects into a local database for fast autocomplete search. See the [Subject Indexing & Autocomplete](indexing.md) guide for full details.
+
+### Quick Example
+
+```php
+use NyonCode\Ares\Facades\Ares;
+
+// Search by name
+$results = Ares::search('Asseco');
+
+// Search by IC prefix
+$results = Ares::search('2707', 5);
+
+// Using global helper
+$results = ares_search('Skoda');
+
+// Each result is a SubjectData(ic, name, city)
+foreach ($results as $subject) {
+ echo "{$subject->name} ({$subject->ic}) - {$subject->city}";
+}
+```
+
+### Autocomplete API Endpoint
+
+```php
+use NyonCode\Ares\Facades\Ares;
+
+Route::get('/api/companies/search', function (Request $request): JsonResponse {
+ $request->validate(['q' => 'required|string|min:2']);
+
+ return response()->json(
+ Ares::search($request->string('q'), 10)
+ );
+});
+```
+
## Artisan Command Usage
### Testing ARES Connection
@@ -350,6 +388,22 @@ php artisan ares:test 12345678
# +-------------------------+---------------------------+
```
+### Indexing Subjects
+
+```bash
+# Index specific subjects
+php artisan ares:index 27074358 25596641
+
+# Show indexing statistics
+php artisan ares:index
+
+# Refresh stale records
+php artisan ares:index --refresh-stale
+
+# With custom options
+php artisan ares:index --refresh-stale --stale-days=14 --limit=200
+```
+
## Caching
The package automatically caches results to reduce API calls. You can control caching behavior:
diff --git a/src/Commands/IndexAresCommand.php b/src/Commands/IndexAresCommand.php
new file mode 100644
index 0000000..41811b5
--- /dev/null
+++ b/src/Commands/IndexAresCommand.php
@@ -0,0 +1,133 @@
+ $ics */
+ $ics = $this->argument('ics');
+
+ if ($this->option('refresh-stale')) {
+ return $this->refreshStale($client, $search);
+ }
+
+ if ($ics === []) {
+ $this->components->info("Celkem indexovano: {$search->subjectCount()} subjektu.");
+
+ $staleDays = $this->configStaleDays();
+ $staleCount = $search->staleCount($staleDays);
+
+ if ($staleCount > 0) {
+ $this->components->warn("Zastaralych zaznamu (starsi nez {$staleDays} dni): {$staleCount}");
+ }
+
+ return self::SUCCESS;
+ }
+
+ return $this->indexIcs($client, $ics);
+ }
+
+ /**
+ * @param array $ics
+ */
+ private function indexIcs(AresClientInterface $client, array $ics): int
+ {
+ $indexed = 0;
+ $failed = 0;
+
+ $this->components->task('Indexovani subjektu', function () use ($client, $ics, &$indexed, &$failed) {
+ foreach ($ics as $ic) {
+ $company = $client->findCompany($ic);
+
+ if ($company === null) {
+ $failed++;
+
+ continue;
+ }
+
+ IndexAresSubject::dispatchSync(
+ ic: $company->ic,
+ name: $company->name,
+ city: $company->registeredOffice?->city,
+ );
+
+ $indexed++;
+ }
+ });
+
+ $this->newLine();
+ $this->components->info("Indexovano: {$indexed}, Neuspesnych: {$failed}");
+
+ return $failed > 0 ? self::FAILURE : self::SUCCESS;
+ }
+
+ private function refreshStale(AresClientInterface $client, SubjectSearchService $search): int
+ {
+ $staleDays = $this->configStaleDays();
+ $limit = (int) $this->option('limit');
+ $staleSubjects = $search->staleSubjects($staleDays, $limit);
+
+ if ($staleSubjects->isEmpty()) {
+ $this->components->info('Zadne zastarale zaznamy k preindexovani.');
+
+ return self::SUCCESS;
+ }
+
+ $this->components->info("Preindexovani {$staleSubjects->count()} zastaralych zaznamu...");
+
+ $refreshed = 0;
+ $failed = 0;
+
+ foreach ($staleSubjects as $subject) {
+ $company = $client->findCompany($subject->ic);
+
+ if ($company === null) {
+ $failed++;
+
+ continue;
+ }
+
+ IndexAresSubject::dispatchSync(
+ ic: $company->ic,
+ name: $company->name,
+ city: $company->registeredOffice?->city,
+ );
+
+ $refreshed++;
+ }
+
+ $this->components->info("Obnoveno: {$refreshed}, Neuspesnych: {$failed}");
+
+ return $failed > 0 ? self::FAILURE : self::SUCCESS;
+ }
+
+ private function configStaleDays(): int
+ {
+ $option = $this->option('stale-days');
+
+ if ($option !== null) {
+ return (int) $option;
+ }
+
+ $configValue = config('ares.indexing.stale_days');
+
+ return is_numeric($configValue) ? (int) $configValue : 30;
+ }
+}
diff --git a/src/Contracts/AresClientInterface.php b/src/Contracts/AresClientInterface.php
index ed1d04a..4a876f1 100644
--- a/src/Contracts/AresClientInterface.php
+++ b/src/Contracts/AresClientInterface.php
@@ -4,7 +4,9 @@
namespace NyonCode\Ares\Contracts;
+use Illuminate\Support\Collection;
use NyonCode\Ares\Data\CompanyData;
+use NyonCode\Ares\Data\SubjectData;
interface AresClientInterface
{
@@ -22,4 +24,11 @@ public function forgetCompany(string $ic): bool;
public function isValidIc(string $ic): bool;
public function normalizeIc(string $ic): string;
+
+ /**
+ * Search indexed subjects by name or IC for autocomplete.
+ *
+ * @return Collection
+ */
+ public function search(string $query, int $limit = 10): Collection;
}
diff --git a/src/Data/SubjectData.php b/src/Data/SubjectData.php
new file mode 100644
index 0000000..6c43388
--- /dev/null
+++ b/src/Data/SubjectData.php
@@ -0,0 +1,28 @@
+ $this->ic,
+ 'name' => $this->name,
+ 'city' => $this->city,
+ ];
+ }
+}
diff --git a/src/Facades/Ares.php b/src/Facades/Ares.php
index e330a61..fa97520 100644
--- a/src/Facades/Ares.php
+++ b/src/Facades/Ares.php
@@ -14,6 +14,7 @@
* @method static bool forgetCompany(string $ic)
* @method static bool isValidIc(string $ic)
* @method static string normalizeIc(string $ic)
+ * @method static \Illuminate\Support\Collection search(string $query, int $limit = 10)
*
* @see AresClientInterface
*/
diff --git a/src/Jobs/IndexAresSubject.php b/src/Jobs/IndexAresSubject.php
new file mode 100644
index 0000000..87711cf
--- /dev/null
+++ b/src/Jobs/IndexAresSubject.php
@@ -0,0 +1,62 @@
+onQueue($queue);
+ }
+
+ if (is_string($connection) && $connection !== '') {
+ $this->onConnection($connection);
+ }
+ }
+
+ public static function fromCompanyData(CompanyData $company): self
+ {
+ return new self(
+ ic: $company->ic,
+ name: $company->name,
+ city: $company->registeredOffice?->city,
+ );
+ }
+
+ public function uniqueId(): string
+ {
+ return $this->ic;
+ }
+
+ public function handle(): void
+ {
+ AresSubject::query()->updateOrCreate(
+ ['ic' => $this->ic],
+ [
+ 'name' => $this->name,
+ 'city' => $this->city,
+ 'indexed_at' => now(),
+ ],
+ );
+ }
+}
diff --git a/src/Models/AresSubject.php b/src/Models/AresSubject.php
new file mode 100644
index 0000000..2e9de3a
--- /dev/null
+++ b/src/Models/AresSubject.php
@@ -0,0 +1,54 @@
+
+ */
+ protected function casts(): array
+ {
+ return [
+ 'indexed_at' => 'datetime',
+ ];
+ }
+
+ public function toSubjectData(): SubjectData
+ {
+ return new SubjectData(
+ ic: $this->ic,
+ name: $this->name,
+ city: $this->city,
+ );
+ }
+}
diff --git a/src/Providers/AresServiceProvider.php b/src/Providers/AresServiceProvider.php
index ea1c03e..0266426 100644
--- a/src/Providers/AresServiceProvider.php
+++ b/src/Providers/AresServiceProvider.php
@@ -8,10 +8,12 @@
use Illuminate\Contracts\Cache\Factory as CacheFactory;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Log\LogManager;
+use NyonCode\Ares\Commands\IndexAresCommand;
use NyonCode\Ares\Commands\TestAresCommand;
use NyonCode\Ares\Contracts\AresClientInterface;
use NyonCode\Ares\Helpers\AresHelper;
use NyonCode\Ares\Services\AresClient;
+use NyonCode\Ares\Services\SubjectSearchService;
use NyonCode\LaravelPackageToolkit\Contracts\Packable;
use NyonCode\LaravelPackageToolkit\Exceptions\InvalidLanguageDirectoryException;
use NyonCode\LaravelPackageToolkit\Packager;
@@ -30,12 +32,18 @@ public function configure(Packager $packager): void
$packager
->name('laravel-ares')
->hasConfig()
+ ->hasMigrations()
->hasCommands([
TestAresCommand::class,
+ IndexAresCommand::class,
])
->hasTranslations('resources/lang')
->registeredPackage(function ($packager) {
+ $this->app->singleton(SubjectSearchService::class, fn () => new SubjectSearchService);
+
$this->app->bind(AresClientInterface::class, function (Application $app): AresClient {
+ $indexingEnabled = $this->configBool('ares.indexing.enabled');
+
return new AresClient(
baseUrl: $this->configString('ares.api_url'),
cacheTtl: $this->configInt('ares.cache_ttl'),
@@ -43,6 +51,8 @@ public function configure(Packager $packager): void
cache: $app->make(CacheFactory::class)->store(),
httpTimeout: $this->configFloat('ares.http_options.timeout'),
httpConnectTimeout: $this->configFloat('ares.http_options.connect_timeout'),
+ autoIndex: $indexingEnabled && $this->configBool('ares.indexing.auto_index'),
+ searchService: $indexingEnabled ? $app->make(SubjectSearchService::class) : null,
);
});
@@ -93,6 +103,19 @@ private function configInt(string $key): int
return is_int($value) ? $value : (is_numeric($value) ? (int) $value : 0);
}
+ /**
+ * Get a boolean value from configuration.
+ *
+ * @param string $key The configuration key
+ * @return bool The configuration value
+ */
+ private function configBool(string $key): bool
+ {
+ $value = config($key);
+
+ return filter_var($value, FILTER_VALIDATE_BOOLEAN);
+ }
+
/**
* Get a float value from configuration.
*
diff --git a/src/Services/AresClient.php b/src/Services/AresClient.php
index f0f59ae..c84390a 100644
--- a/src/Services/AresClient.php
+++ b/src/Services/AresClient.php
@@ -5,15 +5,18 @@
namespace NyonCode\Ares\Services;
use Illuminate\Contracts\Cache\Repository as Cache;
+use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Http;
use NyonCode\Ares\Contracts\AresClientInterface;
use NyonCode\Ares\Data\CompanyData;
+use NyonCode\Ares\Data\SubjectData;
use NyonCode\Ares\Events\CompanyLookupFailed;
use NyonCode\Ares\Events\CompanyLookupSucceeded;
use NyonCode\Ares\Exceptions\CompanyNotFoundException;
use NyonCode\Ares\Exceptions\InvalidApiResponseException;
use NyonCode\Ares\Exceptions\InvalidIcException;
+use NyonCode\Ares\Jobs\IndexAresSubject;
use Psr\Log\LoggerInterface;
use Throwable;
@@ -44,6 +47,8 @@ public function __construct(
private readonly Cache $cache,
private readonly float $httpTimeout = self::DEFAULT_HTTP_TIMEOUT,
private readonly float $httpConnectTimeout = self::DEFAULT_HTTP_CONNECT_TIMEOUT,
+ private readonly bool $autoIndex = false,
+ private readonly ?SubjectSearchService $searchService = null,
) {
$this->processedBaseUrl = rtrim($this->baseUrl, '/');
}
@@ -97,6 +102,14 @@ public function findCompany(string $ic): ?CompanyData
Event::dispatch(new CompanyLookupSucceeded($company));
+ if ($this->autoIndex) {
+ IndexAresSubject::dispatch(
+ ic: $company->ic,
+ name: $company->name,
+ city: $company->registeredOffice?->city,
+ );
+ }
+
return $company;
}
@@ -298,6 +311,19 @@ private function companyUrl(string $normalizedIc): string
return "{$this->processedBaseUrl}/ekonomicke-subjekty/{$normalizedIc}";
}
+ /**
+ * @return Collection
+ */
+ public function search(string $query, int $limit = 10): Collection
+ {
+ if ($this->searchService === null) {
+ /** @var Collection */
+ return collect();
+ }
+
+ return $this->searchService->search($query, $limit);
+ }
+
private function reportLookupException(string $normalizedIc, Throwable $exception): void
{
$this->logger->error('ARES API error', [
diff --git a/src/Services/SubjectSearchService.php b/src/Services/SubjectSearchService.php
new file mode 100644
index 0000000..f83856e
--- /dev/null
+++ b/src/Services/SubjectSearchService.php
@@ -0,0 +1,115 @@
+
+ */
+ public function search(string $query, int $limit = 10): Collection
+ {
+ $query = trim($query);
+
+ if ($query === '') {
+ /** @var Collection */
+ return collect();
+ }
+
+ if (ctype_digit($query)) {
+ return $this->searchByIc($query, $limit);
+ }
+
+ return $this->searchByName($query, $limit);
+ }
+
+ /**
+ * @return Collection
+ */
+ private function searchByIc(string $query, int $limit): Collection
+ {
+ return AresSubject::query()
+ ->whereRaw('ic LIKE ? ESCAPE ?', [self::escapeLike($query).'%', '\\'])
+ ->orderBy('ic')
+ ->limit($limit)
+ ->get()
+ ->map(fn (AresSubject $subject): SubjectData => $subject->toSubjectData());
+ }
+
+ /**
+ * @return Collection
+ */
+ private function searchByName(string $query, int $limit): Collection
+ {
+ $builder = AresSubject::query();
+
+ $this->applyNameSearch($builder, $query);
+
+ return $builder
+ ->limit($limit)
+ ->get()
+ ->map(fn (AresSubject $subject): SubjectData => $subject->toSubjectData());
+ }
+
+ /**
+ * @param Builder $builder
+ */
+ private function applyNameSearch(Builder $builder, string $query): void
+ {
+ $escaped = self::escapeLike($query);
+
+ $builder
+ ->whereRaw('name LIKE ? ESCAPE ?', ['%'.$escaped.'%', '\\'])
+ ->orderBy('name');
+ }
+
+ private static function escapeLike(string $value): string
+ {
+ return str_replace(['\\', '%', '_'], ['\\\\', '\\%', '\\_'], $value);
+ }
+
+ public function indexSubject(string $ic, string $name, ?string $city): void
+ {
+ AresSubject::query()->updateOrCreate(
+ ['ic' => $ic],
+ [
+ 'name' => $name,
+ 'city' => $city,
+ 'indexed_at' => now(),
+ ],
+ );
+ }
+
+ public function subjectCount(): int
+ {
+ return AresSubject::query()->count();
+ }
+
+ public function staleCount(int $days): int
+ {
+ return AresSubject::query()
+ ->where('indexed_at', '<', now()->subDays($days))
+ ->count();
+ }
+
+ /**
+ * @return Collection
+ */
+ public function staleSubjects(int $days, int $limit = 100): Collection
+ {
+ return AresSubject::query()
+ ->where('indexed_at', '<', now()->subDays($days))
+ ->orderBy('indexed_at')
+ ->limit($limit)
+ ->get();
+ }
+}
diff --git a/src/helpers.php b/src/helpers.php
index efa7647..9255e35 100644
--- a/src/helpers.php
+++ b/src/helpers.php
@@ -2,7 +2,9 @@
declare(strict_types=1);
+use Illuminate\Support\Collection;
use NyonCode\Ares\Data\CompanyData;
+use NyonCode\Ares\Data\SubjectData;
use NyonCode\Ares\Helpers\AresHelper;
if (! function_exists('ares')) {
@@ -143,6 +145,20 @@ function ares_validate_ic(string $ic): bool
}
}
+if (! function_exists('ares_search')) {
+ /**
+ * Search indexed ARES subjects for autocomplete.
+ *
+ * @param string $query Search query (name or IC)
+ * @param int $limit Maximum number of results
+ * @return Collection
+ */
+ function ares_search(string $query, int $limit = 10): Collection
+ {
+ return AresHelper::client()->search($query, $limit);
+ }
+}
+
if (! function_exists('ares_normalize_ic')) {
/**
* Normalize IC format.
diff --git a/tests/Fakes/FakeAresClient.php b/tests/Fakes/FakeAresClient.php
index c4ae829..a401593 100644
--- a/tests/Fakes/FakeAresClient.php
+++ b/tests/Fakes/FakeAresClient.php
@@ -4,6 +4,7 @@
namespace NyonCode\Ares\Tests\Fakes;
+use Illuminate\Support\Collection;
use NyonCode\Ares\Contracts\AresClientInterface;
use NyonCode\Ares\Data\CompanyData;
use RuntimeException;
@@ -77,4 +78,9 @@ public function normalizeIc(string $ic): string
return $this->normalizeMap[$ic] ?? preg_replace('/\s+/', '', $ic) ?? $ic;
}
+
+ public function search(string $query, int $limit = 10): Collection
+ {
+ return collect();
+ }
}
diff --git a/tests/Feature/SubjectIndexingTest.php b/tests/Feature/SubjectIndexingTest.php
new file mode 100644
index 0000000..c72f158
--- /dev/null
+++ b/tests/Feature/SubjectIndexingTest.php
@@ -0,0 +1,220 @@
+app['config']->set('database.default', 'testing');
+ $this->app['config']->set('database.connections.testing', [
+ 'driver' => 'sqlite',
+ 'database' => ':memory:',
+ 'prefix' => '',
+ ]);
+
+ $this->loadMigrationsFrom(__DIR__.'/../../database/migrations');
+});
+
+it('indexes a subject when findCompany succeeds and auto_index is enabled', function () {
+ Http::fake([
+ 'https://ares.gov.cz/*' => Http::response([
+ 'ico' => '27074358',
+ 'obchodniJmeno' => 'Asseco Central Europe, a.s.',
+ 'sidlo' => [
+ 'nazevObce' => 'Praha',
+ ],
+ ]),
+ ]);
+
+ $client = app(AresClientInterface::class);
+ $client->findCompany('27074358');
+
+ IndexAresSubject::dispatchSync(
+ ic: '27074358',
+ name: 'Asseco Central Europe, a.s.',
+ city: 'Praha',
+ );
+
+ expect(AresSubject::find('27074358'))
+ ->not->toBeNull()
+ ->name->toBe('Asseco Central Europe, a.s.')
+ ->city->toBe('Praha');
+});
+
+it('searches subjects by name', function () {
+ AresSubject::query()->create(['ic' => '27074358', 'name' => 'Asseco Central Europe, a.s.', 'city' => 'Praha']);
+ AresSubject::query()->create(['ic' => '25596641', 'name' => 'Skoda Auto a.s.', 'city' => 'Mlada Boleslav']);
+ AresSubject::query()->create(['ic' => '00177041', 'name' => 'Skoda Investment a.s.', 'city' => 'Praha']);
+
+ $search = app(SubjectSearchService::class);
+ $results = $search->search('Skoda');
+
+ expect($results)->toHaveCount(2)
+ ->and($results->first()->name)->toContain('Skoda');
+});
+
+it('searches subjects by ic prefix', function () {
+ AresSubject::query()->create(['ic' => '27074358', 'name' => 'Asseco Central Europe, a.s.', 'city' => 'Praha']);
+ AresSubject::query()->create(['ic' => '27082440', 'name' => 'Another Company s.r.o.', 'city' => 'Brno']);
+ AresSubject::query()->create(['ic' => '25596641', 'name' => 'Skoda Auto a.s.', 'city' => 'Mlada Boleslav']);
+
+ $search = app(SubjectSearchService::class);
+ $results = $search->search('2707');
+
+ expect($results)->toHaveCount(1)
+ ->and($results->first()->ic)->toBe('27074358');
+});
+
+it('respects the search limit', function () {
+ for ($i = 1; $i <= 5; $i++) {
+ AresSubject::query()->create([
+ 'ic' => str_pad((string) $i, 8, '0', STR_PAD_LEFT),
+ 'name' => "Test Company {$i}",
+ 'city' => 'Praha',
+ ]);
+ }
+
+ $search = app(SubjectSearchService::class);
+ $results = $search->search('Test', 3);
+
+ expect($results)->toHaveCount(3);
+});
+
+it('returns empty collection for empty query', function () {
+ $search = app(SubjectSearchService::class);
+ $results = $search->search('');
+
+ expect($results)->toBeEmpty();
+});
+
+it('returns SubjectData DTOs from search', function () {
+ AresSubject::query()->create(['ic' => '27074358', 'name' => 'Asseco Central Europe, a.s.', 'city' => 'Praha']);
+
+ $search = app(SubjectSearchService::class);
+ $results = $search->search('Asseco');
+
+ expect($results->first())
+ ->toBeInstanceOf(SubjectData::class)
+ ->ic->toBe('27074358')
+ ->name->toBe('Asseco Central Europe, a.s.')
+ ->city->toBe('Praha');
+});
+
+it('indexes subject via job', function () {
+ IndexAresSubject::dispatchSync(
+ ic: '27074358',
+ name: 'Asseco Central Europe, a.s.',
+ city: 'Praha',
+ );
+
+ $subject = AresSubject::find('27074358');
+
+ expect($subject)
+ ->not->toBeNull()
+ ->name->toBe('Asseco Central Europe, a.s.')
+ ->city->toBe('Praha');
+});
+
+it('updates existing subject via job', function () {
+ AresSubject::query()->create([
+ 'ic' => '27074358',
+ 'name' => 'Old Name',
+ 'city' => 'Brno',
+ ]);
+
+ IndexAresSubject::dispatchSync(
+ ic: '27074358',
+ name: 'New Name',
+ city: 'Praha',
+ );
+
+ $subject = AresSubject::find('27074358');
+
+ expect($subject)
+ ->name->toBe('New Name')
+ ->city->toBe('Praha');
+});
+
+it('searches via the client facade', function () {
+ AresSubject::query()->create(['ic' => '27074358', 'name' => 'Asseco Central Europe, a.s.', 'city' => 'Praha']);
+
+ $results = app(AresClientInterface::class)->search('Asseco');
+
+ expect($results)->toHaveCount(1)
+ ->and($results->first()->ic)->toBe('27074358');
+});
+
+it('escapes LIKE wildcards in name search', function () {
+ AresSubject::query()->create(['ic' => '27074358', 'name' => 'Test 100% Company', 'city' => 'Praha']);
+ AresSubject::query()->create(['ic' => '25596641', 'name' => 'Test Normal Company', 'city' => 'Brno']);
+
+ $search = app(SubjectSearchService::class);
+
+ $results = $search->search('100%');
+
+ expect($results)->toHaveCount(1)
+ ->and($results->first()->name)->toBe('Test 100% Company');
+});
+
+it('escapes LIKE wildcards in ic search', function () {
+ AresSubject::query()->create(['ic' => '27074358', 'name' => 'Company A', 'city' => 'Praha']);
+ AresSubject::query()->create(['ic' => '25596641', 'name' => 'Company B', 'city' => 'Brno']);
+
+ $search = app(SubjectSearchService::class);
+
+ $results = $search->search('2707_358');
+
+ expect($results)->toBeEmpty();
+});
+
+it('serializes SubjectData to JSON', function () {
+ $subject = new SubjectData(
+ ic: '27074358',
+ name: 'Asseco Central Europe, a.s.',
+ city: 'Praha',
+ );
+
+ $json = json_encode($subject, JSON_THROW_ON_ERROR);
+
+ expect(json_decode($json, true))->toBe([
+ 'ic' => '27074358',
+ 'name' => 'Asseco Central Europe, a.s.',
+ 'city' => 'Praha',
+ ]);
+});
+
+it('returns unique job id based on ic', function () {
+ $job = new IndexAresSubject(
+ ic: '27074358',
+ name: 'Test',
+ city: 'Praha',
+ );
+
+ expect($job->uniqueId())->toBe('27074358');
+});
+
+it('reports stale subjects count', function () {
+ AresSubject::query()->create([
+ 'ic' => '27074358',
+ 'name' => 'Old Company',
+ 'city' => 'Praha',
+ 'indexed_at' => now()->subDays(60),
+ ]);
+
+ AresSubject::query()->create([
+ 'ic' => '25596641',
+ 'name' => 'Fresh Company',
+ 'city' => 'Brno',
+ 'indexed_at' => now(),
+ ]);
+
+ $search = app(SubjectSearchService::class);
+
+ expect($search->staleCount(30))->toBe(1)
+ ->and($search->subjectCount())->toBe(2);
+});
diff --git a/tests/Feature/TestAresCommandTest.php b/tests/Feature/TestAresCommandTest.php
index 926c24a..8444a2c 100644
--- a/tests/Feature/TestAresCommandTest.php
+++ b/tests/Feature/TestAresCommandTest.php
@@ -2,6 +2,7 @@
declare(strict_types=1);
+use Illuminate\Support\Collection;
use NyonCode\Ares\Contracts\AresClientInterface;
use NyonCode\Ares\Data\AddressData;
use NyonCode\Ares\Data\CompanyData;
@@ -77,6 +78,11 @@ public function normalizeIc(string $ic): string
{
return $ic;
}
+
+ public function search(string $query, int $limit = 10): Collection
+ {
+ return collect();
+ }
});
$this->artisan('ares:test', ['ic' => '27074358'])
@@ -131,6 +137,11 @@ public function normalizeIc(string $ic): string
{
return $ic;
}
+
+ public function search(string $query, int $limit = 10): Collection
+ {
+ return collect();
+ }
});
$this->artisan('ares:test', ['ic' => '00000000'])
diff --git a/tests/TestCase.php b/tests/TestCase.php
index 9d850a9..e7ac1d9 100644
--- a/tests/TestCase.php
+++ b/tests/TestCase.php
@@ -25,5 +25,8 @@ protected function defineEnvironment($app): void
$app['config']->set('ares.log_channel', 'stack');
$app['config']->set('ares.http_options.timeout', 5.0);
$app['config']->set('ares.http_options.connect_timeout', 3.0);
+ $app['config']->set('ares.indexing.enabled', true);
+ $app['config']->set('ares.indexing.auto_index', false);
+ $app['config']->set('ares.indexing.stale_days', 30);
}
}