Integrate Virkdata module with new routes, helpers, and configurations

- Added `virkdata` class to handle API operations for company information retrieval and config validation.
- Introduced `virkdata_i` interface and supporting helpers: `virkdata_request_parameters`, `virkdata_response`, and enums for formats, countries, and error codes.
- Implemented new routes: `/worker/debug`, `/worker/licenseplates`, `/cvr/lookup`, and `/economic/doesCustomerExist`.
- Added module `Virkdata` route for searching company data and fetching/updating configurations (`moduleVirkDataRoute`).
- Created `virkdata` module configurations: `enabled`, `secret_key`, and `monthly_limit`.
- Integrated virkdata initialization in `index.php`.
This commit is contained in:
Jeppe Bundgaard
2025-10-12 12:50:57 +02:00
parent 6f68713c7a
commit ca29ded6be
16 changed files with 841 additions and 0 deletions
+208
View File
@@ -0,0 +1,208 @@
<?php
namespace classes;
require_once WD . '/modules/virkdata/virkdata_c.php';
/** Actions */
require_once WD . '/modules/virkdata/actions/virkdata_search_a.php';
use Exception;
use modules\virkdata\helpers\virkdata_response;
use virkdata\actions\virkdata_search_a;
use virkdata\virkdata_c;
use interfaces\virkdata_i;
class virkdata implements virkdata_i
{
/**
* Configuration of the virkdata module
* @var virkdata_c
*/
public virkdata_c $config;
/**
* API URL
* @var string
*/
private string $api_url = 'https://api.virkdata.com/';
/**
* ACTION: VIRKDATA_SEARCH
* @see virkdata_search_a
* @notation This action is when a company search request is made, and logs the request in the database
* @var virkdata_search_a $virkdata_search
*/
private virkdata_search_a $virkdata_search;
public function __construct()
{
$this->config = new virkdata_c();
/** Actions */
$this->virkdata_search = new virkdata_search_a();
}
/**
* @inheritDoc
* @throws Exception If the base or target currency is invalid
* @throws Exception If the endpoint is invalid
* @throws Exception If the response is invalid
* @throws Exception If the response is not valid JSON
* @throws Exception If the request fails
*/
function getCompanyInformation(string $search, string $endpoint, array $data): \modules\virkdata\helpers\virkdata_response
{
// Send the request
// Return the conversion rate
return self::sendRequest($search, $endpoint, $data);
}
/**
* @inheritDoc
* @throws Exception If the module is not enabled, the license plate is invalid, the daily limit is exceeded, or the secret key is invalid
*/
function sendRequest(
string $search,
string $endpoint,
array $data = [],
string $method = 'GET'
): object
{
// Validate the module is enabled
self::requireModuleEnabled();
// Validate the secret key
self::requireValidSecretKey();
// Send the request
$response = match ($method) {
'GET' => self::sendGetRequest($search, $endpoint, $data),
//'POST' => self::sendPostRequest($search, $endpoint, $data),
//'PUT' => self::sendPutRequest($search, $endpoint, $data),
//'DELETE' => self::sendDeleteRequest($search, $endpoint, $data),
default => self::exception(
$search,
[
'method' => $method,
'endpoint' => $endpoint,
'data' => $data,
'search' => $search,
'response' => null,
'status_code' => 400,
'error' => 'Invalid request method',
],
500
),
};
// Add the usage to the request log
$this->virkdata_search->virkdata_search($search, $response, 200);
// Return the response
return $response;
}
/**
* @inheritDoc
*/
function requireModuleEnabled(): void
{
// Check if the module is enabled
if (!$this->config->enabled->isTrue()) {
throw new Exception('The virkdata module is not enabled');
}
}
/**
* @inheritDoc
*/
function requireValidSecretKey(): void
{
// Check if the secret key is valid
if ($this->config->secret_key->getVariableValue() === null) {
self::exception(
'',
[
'status_code' => 500,
'error' => 'Invalid secret key defined in the config (virkdata_secret_key_c)',
],
500
);
}
}
/**
* @throws Exception
*/
function exception(string $search, array $data, int $status_code = 500): object
{
// Check if the response is valid JSON
if (!json_decode($data['response'])) {
throw new Exception('Invalid response');
}
// Add the request to the log
$this->virkdata_search->virkdata_search($search, $data, $status_code);;
return throw new Exception($data['error'] ?? 'An error occurred while processing the request, in ' . $this->config->getModuleName() . ' module');
}
/**
* @inheritDoc
* @throws Exception
*/
function sendGetRequest(string $search, string $endpoint, array $data): virkdata_response
{
if (!empty($data)) {
$query_string = '?' . http_build_query($data);
} else {
$query_string = '';
}
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://virkdata.dk/api/?search={$search}",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_HTTPHEADER => [
"Authorization: {$this->config->secret_key->getVariableValue()}"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
}
// Get the status code
$status_code = curl_getinfo($curl, CURLINFO_HTTP_CODE);
curl_close($curl);
// Check if the response is valid JSON
if (!json_decode($response)) {
$exception_message = match ($status_code) {
401 => 'Unauthorized',
404 => 'Not found',
429 => 'Too many requests',
default => 'Invalid response',
};
self::exception(
$search,
[
'method' => 'GET',
'endpoint' => $endpoint,
'data' => $data,
'response' => null,
'status_code' => $status_code,
'error' => $exception_message,
],
$status_code
);
}
return (new virkdata_response())->populate(json_decode($response, true));
}
}
+1
View File
@@ -75,6 +75,7 @@ require_once 'classes/image_processor.php';
require_once 'classes/upload_store.php';
require_once 'classes/attachments.php';
require_once 'classes/attachment_store.php';
require_once 'classes/virkdata.php';
/**
* Modules
@@ -0,0 +1,54 @@
<?php
namespace interfaces;
use Exception;
use modules\virkdata\helpers\virkdata_response;
interface virkdata_i
{
/**
* Get company information from the virkdata module
* @return virkdata_response The response from the virkdata module
* @param string $search The search term to use
* @param string $endpoint The endpoint to send the request to (e.g. "search")
* @param array $data The data to send with the request (e.g. ["key" => "value"])
* @return virkdata_response The response from the virkdata module
*/
function getCompanyInformation(string $search, string $endpoint, array $data): virkdata_response;
/**
* Send a request to the
* @param string $search The search term to use
* @param string $endpoint The endpoint to send the request to (e.g. "latest")
* @param array $data The data to send with the request (e.g. ["key" => "value"])
* @param string $method The method to use for the request (e.g. "GET")
* @return object The response from the virkdata module
*/
function sendRequest(string $search, string $endpoint, array $data, string $method): object;
/**
* Require the module to be enabled
* @return void
* @throws Exception If the module is not enabled
*/
function requireModuleEnabled(): void;
/**
* Require the secret key to be valid
* @return void
* @throws Exception If the secret key is not valid
*/
function requireValidSecretKey(): void;
/**
* Send a GET request to the virkdata
* @param string $search The search term to use
* @param string $endpoint The endpoint to send the request to (e.g. "latest")
* @param array $data The data to send with the request (e.g. ["key" => "value"])
* @return object The response from the virkdata
*/
function sendGetRequest(string $search, string $endpoint, array $data): object;
}
@@ -0,0 +1,68 @@
<?php
namespace virkdata\actions;
use Exception;
use traits\module_action_t;
class virkdata_search_a
{
use module_action_t;
/**
* @inheritDoc
* @throws Exception If the module name is invalid
*/
public function run(): void
{
self::set_module_name('VIRKDATA');
/** Add the action to the list of valid actions */
// Search
self::add_action(
'VIRKDATA_SEARCH',
'This action is when a search is performed',
// Method to call
self::class . '::virkdata_search',
[
'search' => 'The search term',
'data' => 'The data returned from the conversion (If any)',
'status_code' => 'The status code of the action',
]
);
}
/**
* Search action
* @param string $search The currency to convert from (ISO 4217)
* @param array|object $data The data returned from the conversion (If any)
* @param int $status_code The status code of the action
* @throws Exception If the action name is invalid
* @throws Exception If the action data is invalid
* @throws Exception If the action status code is invalid
* @throws Exception If the action is not valid
*/
public function virkdata_search(string $search, array|object $data = [], int $status_code = 0): void
{
self::set_action('VIRKDATA_SEARCH');
// If the data is an object, convert it to an array
if (is_object($data)) {
$data = (array)$data;
}
// Validate the action data
if (!is_array($data)) {
throw new Exception('Action data is invalid');
}
// Validate the action status code
self::set_data([
'search' => $search,
'data' => $data,
]);
self::set_status($status_code);
// Add the action to the log
self::add_action_log(
self::get_action_name(),
(int)self::get_status(),
);
}
}
@@ -0,0 +1,29 @@
<?php
namespace virkdata\config;
use Exception;
use traits\module_config_variable;
class virkdata_enabled_c
{
use module_config_variable;
/**
* @throws Exception
*/
public function __construct()
{
self::setupConfigVariable(
'virkdata',
'enabled',
'bool',
true,
null,
'Whether the virkdata module is enabled or not',
'1',
false,
'false'
);
}
}
@@ -0,0 +1,29 @@
<?php
namespace virkdata\config;
use Exception;
use traits\module_config_variable;
class virkdata_monthly_limit_c
{
use module_config_variable;
/**
* @throws Exception
*/
public function __construct()
{
self::setupConfigVariable(
'virkdata',
'monthly_limit',
'int',
true,
null,
'The API request limit for the virkdata module',
'1',
false,
0
);
}
}
@@ -0,0 +1,29 @@
<?php
namespace virkdata\config;
use Exception;
use traits\module_config_variable;
class virkdata_secret_key_c
{
use module_config_variable;
/**
* @throws Exception
*/
public function __construct()
{
self::setupConfigVariable(
'virkdata',
'secret_key',
'string',
false,
null,
'The API token for virkdata',
'1',
true,
''
);
}
}
@@ -0,0 +1,40 @@
<?php
namespace modules\virkdata\helpers;
/**
* Class virkdata_request_parameters
* @package modules\virkdata\helpers
* @see https://virkdata.dk/app/dokumentation
* @note https://virkdata.dk/api/?search=telia&format=json&country=dk - This is the final request example
*/
class virkdata_request_parameters
{
/**
* Search string
* @var string $search
*/
public string $search;
/**
* The format of response data
* @var virkdata_request_parameters_format $format
* @see virkdata_request_parameters_format::cases()
*/
public virkdata_request_parameters_format $format = virkdata_request_parameters_format::json;
/**
* The countrycode for database of companies you would like to search through
* @var virkdata_request_parameters_country $country
* @see virkdata_request_parameters_country::cases()
*/
public virkdata_request_parameters_country $country = virkdata_request_parameters_country::dk;
/**
* Financial Summary, only available for dk
* @var bool $financial_summary
*/
public bool $financial_summary = false;
/**
* Custom variable to add to the request. You can filter all your requests in your dashboard
* @use https://virkdata.dk/app/dokumentation
* @var ?string $custom
*/
public ?string $custom;
}
@@ -0,0 +1,12 @@
<?php
namespace modules\virkdata\helpers;
enum virkdata_request_parameters_country
{
// Cases: dk no fi ch
case dk; // default
case no;
case fi;
case ch;
}
@@ -0,0 +1,12 @@
<?php
namespace modules\virkdata\helpers;
enum virkdata_request_parameters_format
{
case json; // default
case xml;
case plain;
case html;
case raw;
}
@@ -0,0 +1,142 @@
<?php
namespace modules\virkdata\helpers;
/**
* Class virkdata_response
* @package modules\virkdata\helpers
* @see https://virkdata.dk/app/dokumentation
*/
class virkdata_response
{
/**
* Virksomhedens CVR-nummer
* @var ?int $vat
*/
public ?int $vat;
/**
* Status for virksomhed (Aktiv, ophørt, under konkurs m.m.)
* @var ?string $status
*/
public ?string $status;
/**
* Virksomhedens navn
* @var ?string $name
*/
public ?string $name;
/**
* Virksomhedens adresse
* @var ?string $address
*/
public ?string $address;
/**
* Postnummer hvor virksomheden beliggende
* @var ?int $zipcode
*/
public ?int $zipcode;
/**
* By hvor virksomheden beliggende
* @var ?string $city
*/
public ?string $city;
/**
* true = reklamebeskyttet. false = ikke reklamebeskyttet
* @var ?bool $protected
*/
public ?bool $protected;
/**
* Virksomhedens telefonnumer
* @var ?string $phone
*/
public ?string $phone;
/**
* Virksomhedens hjemmeside
* @var ?string $website
*/
public ?string $website;
/**
* Virksomhedens email
* @var ?string $email
*/
public ?string $email;
/**
* Virksomhedens faxnummer
* @var ?string $fax
*/
public ?string $fax;
/**
* Dato virksomheden blev etableret (format: YYYY-MM-DD)
* @var ?string $startdate
*/
public ?string $startdate;
/**
* Dato virksomheden blev lukket (format: YYYY-MM-DD). Er virksomheden fortsat aktivt returneres null
* @var ?string $enddate
*/
public ?string $enddate;
/**
* Antal medarbejdere ansat i virksomheden
* @var ?int $employees
*/
public ?int $employees;
/**
* Branchekode nummer
* @var ?int $industrycode
*/
public ?int $industrycode;
/**
* Branchekode beskrivelse
* @var ?string $industrydesc
*/
public ?string $industrydesc;
/**
* Virksomhedstype (kort beskrivelse)
* @var ?string $companytype
*/
public ?string $companytype;
/**
* Virksomhedstype (fuldt beskrivelse)
* @var ?string $companydesc
*/
public ?string $companydesc;
/**
* Ejere af virksomheden returneres som et array
* @var ?array $owners
*/
public ?array $owners;
/**
* Nøgletal for regnskab. Data er ikke tilgængelig for alle virksomheder
* @var ?array $financial_summary
*/
public ?array $financial_summary;
/**
* Fejlkode
* @var ?int $error_code
*/
public ?int $error_code;
/**
* (Fejl-)besked
* @var ?virkdata_response_error_codes $response
* @see virkdata_response_error_codes::cases()
*/
public ?virkdata_response_error_codes $response;
/**
* Besked om fejl
* @var ?string $message
*/
public ?string $message;
public function asArray(): array
{
return (array)$this;
}
public function populate(array $json_decode): static
{
foreach ($json_decode as $key => $value) {
if (property_exists($this, $key)) {
$this->$key = $value;
}
}
return $this;
}
}
@@ -0,0 +1,22 @@
<?php
namespace modules\virkdata\helpers;
enum virkdata_response_error_codes
{
case invalid_api_key; // 1001
case invalid_api_format; // 1002
case missing_auth_header; // 1003
case invalid_property; // 2001
case property_not_allowed; // 2002
case missing_request_value; // 3001
case no_results; // 3002
case test_connection_failed; // 6001
case quota_exceeded; // 7001
case daily_quota_exceeded; // 7002
case too_many_requests; // 8001
case invalid_format; // 4001
case invalid_country; // 5001
case country_not_allowed; // 5002
case unknown;
}
@@ -0,0 +1,51 @@
<?php
namespace virkdata;
require_once WD . '/modules/virkdata/config/virkdata_enabled_c.php';
require_once WD . '/modules/virkdata/config/virkdata_secret_key_c.php';
require_once WD . '/modules/virkdata/config/virkdata_monthly_limit_c.php';
// Require helpers
require_once WD . '/modules/virkdata/helpers/virkdata_request_parameters_country.php';
require_once WD . '/modules/virkdata/helpers/virkdata_request_parameters_format.php';
require_once WD . '/modules/virkdata/helpers/virkdata_response_error_codes.php';
require_once WD . '/modules/virkdata/helpers/virkdata_request_parameters.php';
require_once WD . '/modules/virkdata/helpers/virkdata_response.php';
use virkdata\config\virkdata_monthly_limit_c;
use virkdata\config\virkdata_enabled_c;
use virkdata\config\virkdata_secret_key_c;
use traits\module_config_t;
class virkdata_c
{
use module_config_t;
/**
* The status of the virkdata module
* @var virkdata_enabled_c
*/
public virkdata_enabled_c $enabled;
/**
* The secret key for the virkdata module (API token)
* @var virkdata_secret_key_c
*/
public virkdata_secret_key_c $secret_key;
/**
* The monthly limit for the virkdata module
* @var virkdata_monthly_limit_c
*/
public virkdata_monthly_limit_c $monthly_limit;
public function __construct()
{
$this->setupConfig('virkdata');
$this->allowUpdate([
virkdata_enabled_c::class,
virkdata_secret_key_c::class,
virkdata_monthly_limit_c::class
]);
$this->enabled = new virkdata_enabled_c();
$this->secret_key = new virkdata_secret_key_c();
$this->monthly_limit = new virkdata_monthly_limit_c();
}
}
@@ -601,5 +601,42 @@ class moduleConfigRoute
'modules_licenseplaterecognizer_config' => 'Update licenseplaterecognizer config'
]
);
/** Virkdata config > GET */
$this->get('/virkdata/config', function () {
global $response;
$this->requirePermission('modules_virkdata_config');
$user = (new authentication())->get_user();
if ($user) {
(new logs_o())->add('virkdata_config', 'global', 1, $user->id, 'VIRKDATA_CONFIG', 'Successfully fetched virkdata config');
$response->success(
(new \classes\virkdata())->config->getConfigRequest()
);
} else {
(new logs_o())->add('virkdata_config', 'global', 1, 0, 'VIRKDATA_CONFIG', 'No user found, or invalid session');
}
},
[
'modules_virkdata_config' => 'Get virkdata config'
]
);
/** Virkdata config > POST */
$this->post('/virkdata/config', function () {
global $response;
$this->requirePermission('modules_virkdata_config');
$user = (new authentication())->get_user();
if ($user) {
(new logs_o())->add('virkdata_config', 'global', 1, $user->id, 'VIRKDATA_CONFIG', 'Successfully updated virkdata config');
$response->success(
(new \classes\virkdata())->config->postConfigRequest()
);
} else {
(new logs_o())->add('virkdata_config', 'global', 1, 0, 'VIRKDATA_CONFIG', 'No user found, or invalid session');
$response->error('Invalid session', 400);
}
},
[
'modules_virkdata_config' => 'Update virkdata config'
]
);
}
}
@@ -0,0 +1,60 @@
<?php
namespace routes;
use classes\authentication;
use classes\fxratesapi;
use classes\response;
use classes\router;
use classes\virkdata;
use objects\currency_conversion_rates_o;
use objects\logs_o;
use traits\route_t;
class moduleVirkDataRoute
{
use route_t;
public function run(): void
{
global /** @var response $response */
/** @var router $router */
$router, $response;
/** Modules > VirkData > search > GET */
$this->get('/modules/virkdata/search', function () {
global $response;
self::requirePermission('modules_virkdata_search');
$user = (new authentication())->get_user();
if ($user) {
self::requireParameters(['search']);
self::requireType('search', self::type_string());
self::requireMinLength('search', 3);
self::requireMaxLength('search', 100);
(new logs_o())->add('modules_virkdata', 'global', 1, $user->id, 'MODULES_VIRKDATA', 'User accessed searched for a company');
$result = (new virkdata())->getCompanyInformation(
self::getParameter('search'),
'',
[]
);
// Check if the result is an error
if (isset($result->message)) {
(new logs_o())->add('modules_virkdata', 'global', 1, $user->id, 'MODULES_VIRKDATA', 'User accessed the conversion rate and got an error');
$response->error($result->message, 400);
}
$res = $result->asArray();
// Response
$response->success($res);
} else {
(new logs_o())->add('modules_virkdata', 'global', 1, 0, 'MODULES_VIRKDATA', 'User accessed the conversion rate without being logged in');
$response->error('Invalid session', 400);
}
},
[
'modules_virkdata_rate' => 'Update conversion rates using the VIRKDATA',
]
);
}
}
+47
View File
@@ -2,7 +2,10 @@
namespace routes;
use classes\economic;
use classes\router;
use classes\virkdata;
use objects\users_o;
use traits\route_t;
class workerRoute
@@ -16,6 +19,12 @@ class workerRoute
$response, $router;
$response->success(['message' => 'Worker is running', 'status' => 'OK', 'time' => date('Y-m-d H:i:s'), 'timezone' => date_default_timezone_get(), 'host' => gethostname(), 'version' => '1.0.1', 'routes' => $router->countRoutes()]);
});
$this->get('/worker/debug', function () {
global /** @var router $router */
$response, $router;
$virkdata = new virkdata();
$response->success('test');
});
$this->get('/worker/licenseplates', function () {
global /** @var router $router */
$response, $db;
@@ -48,6 +57,44 @@ class workerRoute
}
$response->success(['counted_license_plates' => $counted_plates, 'total_unique_plates' => count($counted_plates)]);
});
$this->get('/economic/doesCustomerExist', function () {
global $response;
self::requirePermission( 'economic_does_customer_exist'); // TODO: Remove this
self::requireParameters(['cvr']);
$cvr = self::getParameter('cvr');
// Check if the customer exists in E-conomic.
if (!is_numeric($cvr)) {
$response->error('Invalid CVR number', 400);
}
$economic = new economic();
$results = $economic->customers->customers->search([
'corporateIdentificationNumber' => (string)$cvr,
],[
'skipPages' => 0,
'pageSize' => 1000, // Since the limit is 1000, we need to set the page size to 1000.
])->collection;
if (count($results) === 0) {
$response->error('Customer not found', 404);
}
$response->success(['cvr' => $cvr, 'result' => $results], 200);
});
$this->get('/cvr/lookup', function () {
global $response;
self::requirePermission('cvr_lookup'); // TODO: Remove this
self::requireParameters(['cvr']);
$cvr = self::getParameter('cvr');
if (!is_numeric($cvr)) {
$response->error('Invalid CVR number', 400);
}
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'https://cvrapi.dk/api?search=' . $cvr . '&country=dk');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_USERAGENT, 'Truck Wash ApS (truckwash.dk) - Kundeimporterings-modul');
$result = curl_exec($ch);
curl_close($ch);
$result = json_decode($result);
$response->success(['cvr' => $cvr, 'result' => $result, 'phone' => $result->phone, 'email' => $result->email], 200);
});
}
protected function FORMAT_LICENSE_PLATE(string $plate): string