diff --git a/.gitignore b/.gitignore index 587d495..e84c409 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,8 @@ composer.phar /nbproject/ /tests/.phpunit.result.cache .vscode/settings.json +/.php-cs-fixer.cache +/.vscode/ +/.phpunit.result.cache +/src/.php-cs-fixer.cache +/.phpunit.cache/ diff --git a/README.md b/README.md index aaa5e8f..ee67d80 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,6 @@ # Realpad Takeout API Client for PHP -//for real estate developer IT / reporting departments, integrators, and -other 3rd parties// +*for real estate developer IT / reporting departments, integrators, and other 3rd parties* This document describes how to use the Realpad Takeout API to back up the data stored in the system. Both structured data (such as lists of customers, deals, etc) and files uploaded to the @@ -44,10 +43,14 @@ crc="3826804066"/> ● uid is the unique identifier of this resource, by which it can be retrieved using get-projects. + ● content-type is the MIME type of the file, resolved when uploaded to the system (it’s the best guess). + ● file-name is the original file name when it was uploaded to the system. + ● size is the file size in bytes. + ● crc is the CRC32 checksum of the file. This endpoint will always return all the resources. It’s up to your system to determine which @@ -62,7 +65,7 @@ curl \ https://cms.realpad.eu/resource/bd5563ae-abc... ``` -## Endpoints with a binary payload +## Endpoints All of these endpoints return a single Excel file with a .xls extension, containing all the relevant data stored in our system. These endpoints behave just like get-resource, in that the HTTP @@ -136,11 +139,14 @@ Accepts several additional optional parameters: ● `filter_status`` - if left empty, invoices in all statuses are sent. 1 - new invoices. 2 - invoices in Review #1. 3 - invoices in Review #2. 4 - invoices in approval. 5 - fully approved invoices. 6 - fully rejected invoices. + ●`filter_groupcompany` - if left empty, invoices from all the group companies are sent. If Realpad database IDs of group companies are provided (as a comma-separated list), then only invoices from these companies are sent. + ● `filter_issued_from`` - specify a date in the 2019-12-31 format to only send invoices issues after that date. + ● `filter_issued_to` - specify a date in the 2019-12-31 format to only send invoices issues before that date. The initial set of columns describes the Invoice itself, and the last set of columns contains the @@ -150,32 +156,59 @@ data of its Lines. Unit status enumeration ● 0 - free. + ● 1 - pre-reserved. + ● 2 - reserved. + ● 3 - sold. + ● 4 - not for sale. + ● 5 - delayed. Unit type enumeration + ● 1 - flat. + ● 2 - parking. + ● 3 - cellar. + ● 4 - outdoor parking. + ● 5 - garage. + ● 6 - commercial space. + ● 7 - family house. + ● 8 - land. + ● 9 - atelier. + ● 10 - office. + ● 11 - art workshop. + ● 12 - non-residential unit. + ● 13 - motorbike parking. + ● 14 - creative workshop. + ● 15 - townhouse. + ● 16 - utility room. + ● 17 - condominium. + ● 18 - storage. + ● 19 - apartment. + ● 20 - accommodation unit. + ● 21 - bike stand. + ● 22 - communal area. diff --git a/composer.json b/composer.json index fbe1c1a..373e5e4 100644 --- a/composer.json +++ b/composer.json @@ -3,7 +3,8 @@ "description": "Realpad Takeout API Client", "type": "library", "require": { - "vitexsoftware/ease-core": "dev-main" + "vitexsoftware/ease-core": "dev-main", + "phpoffice/phpspreadsheet": "dev-master" }, "require-dev": { "phpunit/phpunit": "10.5.x-dev" @@ -16,7 +17,7 @@ }, "authors": [ { - "name": "CyberVitexus", + "name": "Vítězslav Dvořák", "email": "info@vitexsoftware.cz" } ], diff --git a/examples/listcustomers.php b/examples/listcustomers.php new file mode 100644 index 0000000..8f2700b --- /dev/null +++ b/examples/listcustomers.php @@ -0,0 +1,11 @@ +listCustomers(); + +print_r($customers); diff --git a/examples/listexcelcustomers.php b/examples/listresources.php similarity index 80% rename from examples/listexcelcustomers.php rename to examples/listresources.php index 03ca716..5745ac6 100644 --- a/examples/listexcelcustomers.php +++ b/examples/listresources.php @@ -3,11 +3,10 @@ require_once __DIR__ . '/../vendor/autoload.php'; -\Ease\Shared::init(['REALPAD_USERNAME','REALPAD_PASSWORD'], '../.env' ); +\Ease\Shared::init(['REALPAD_USERNAME','REALPAD_PASSWORD'], '../.env'); $client = new \SpojeNet\Realpad\ApiClient(); -$resources = $client->getResources(); +$resources = $client->listResources(); print_r($resources); - diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..576697a --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,14 @@ + + + + + tests + + + + + + src + + + diff --git a/src/ApiClient.php b/src/ApiClient.php index 6d098c0..38e88cd 100644 --- a/src/ApiClient.php +++ b/src/ApiClient.php @@ -11,12 +11,14 @@ namespace SpojeNet\Realpad; +use PhpOffice\PhpSpreadsheet\IOFactory; + /** * Connect to TakeOut * * @author vitex */ -class ApiClient extends \Ease\Molecule +class ApiClient extends \Ease\Sand { /** * RealPad URI @@ -24,11 +26,59 @@ class ApiClient extends \Ease\Molecule */ public $baseEndpoint = 'https://cms.realpad.eu/'; - private $debug; - + /** + * CURL resource handle + * @var resource|\CurlHandle|null + */ private $curl; - private $timeout; + /** + * CURL response timeout + * @var int + */ + private $timeout = 0; + + /** + * Last CURL response info + * @var array + */ + private $curlInfo = []; + + /** + * Last CURL response error + * @var string + */ + private $lastCurlError; + + /** + * Throw Exception on error ? + * @var boolean + */ + public $throwException = true; + + /** + * Realpad Username + * @var string + */ + private $apiUsername; + + /** + * Realpad User password + * @var string + */ + private $apiPassword; + + /** + * May be huge response + * @var string + */ + private $lastCurlResponse; + + /** + * HTTP Response code of latst request + * @var int + */ + private $lastResponseCode; /** * RealPad Data obtainer @@ -37,80 +87,60 @@ public function __construct() { $this->apiUsername = \Ease\Shared::cfg('REALPAD_USERNAME'); $this->apiPassword = \Ease\Shared::cfg('REALPAD_PASSWORD'); + $this->curlInit(); } /** - * Inicializace CURL + * Initialize CURL * - * @return boolean Online Status + * @return mixed|boolean Online Status */ public function curlInit() { - if ($this->offline === false) { - $this->curl = \curl_init(); // create curl resource - \curl_setopt($this->curl, CURLOPT_RETURNTRANSFER, true); // return content as a string from curl_exec - \curl_setopt($this->curl, CURLOPT_FOLLOWLOCATION, true); // follow redirects - \curl_setopt($this->curl, CURLOPT_HTTPAUTH, true); // HTTP authentication - \curl_setopt($this->curl, CURLOPT_SSL_VERIFYPEER, true); - \curl_setopt($this->curl, CURLOPT_SSL_VERIFYHOST, false); - \curl_setopt($this->curl, CURLOPT_VERBOSE, ($this->debug === true)); // For debugging - if (empty($this->authSessionId)) { - \curl_setopt( - $this->curl, - CURLOPT_USERPWD, - $this->user . ':' . $this->password - ); // set username and password - } - if (!is_null($this->timeout)) { - \curl_setopt($this->curl, CURLOPT_HTTPHEADER, [ - 'Connection: Keep-Alive', - 'Keep-Alive: ' . $this->timeout - ]); - \curl_setopt($this->curl, CURLOPT_TIMEOUT, $this->timeout); - } - - \curl_setopt($this->curl, CURLOPT_USERAGENT, 'RealpadTakeout v' . \Ease\Shared::appVer() . ' https://github.com/Spoje-NET/Realpad-Takeout'); + $this->curl = \curl_init(); // create curl resource + \curl_setopt($this->curl, CURLOPT_RETURNTRANSFER, true); // return content as a string from curl_exec + \curl_setopt($this->curl, CURLOPT_FOLLOWLOCATION, true); // follow redirects + \curl_setopt($this->curl, CURLOPT_HTTPAUTH, true); // HTTP authentication + \curl_setopt($this->curl, CURLOPT_SSL_VERIFYPEER, true); + \curl_setopt($this->curl, CURLOPT_SSL_VERIFYHOST, false); + \curl_setopt($this->curl, CURLOPT_VERBOSE, ($this->debug === true)); // For debugging + if ($this->timeout) { + \curl_setopt($this->curl, CURLOPT_HTTPHEADER, [ + 'Connection: Keep-Alive', + 'Keep-Alive: ' . $this->timeout + ]); + \curl_setopt($this->curl, CURLOPT_TIMEOUT, $this->timeout); } - return !$this->offline; + \curl_setopt( + $this->curl, + CURLOPT_USERAGENT, + 'RealpadTakeout v' . \Ease\Shared::appVersion() . ' https://github.com/Spoje-NET/Realpad-Takeout' + ); + \curl_setopt( + $this->curl, + CURLOPT_POSTFIELDS, + 'login=' . $this->apiUsername . '&password=' . $this->apiPassword + ); + return $this->curl; } /** - * Vykonej HTTP požadavek + * Execute HTTP request * - * @param string $url URL požadavku + * @param string $url URL of request * @param string $method HTTP Method GET|POST|PUT|OPTIONS|DELETE - * @param string $format požadovaný formát komunikace * * @return int HTTP Response CODE */ - public function doCurlRequest($url, $method, $format = null) + public function doCurlRequest($url, $method = 'GET') { - if (is_null($format)) { - $format = $this->format; - } curl_setopt($this->curl, CURLOPT_URL, $url); -// Nastavení samotné operace + curl_setopt($this->curl, CURLOPT_CUSTOMREQUEST, strtoupper($method)); -//Vždy nastavíme byť i prázná postdata jako ochranu před chybou 411 - curl_setopt($this->curl, CURLOPT_POSTFIELDS, $this->postFields); - $httpHeaders = $this->defaultHttpHeaders; - $formats = Formats::bySuffix(); - if (!isset($httpHeaders['Accept'])) { - $httpHeaders['Accept'] = $formats[$format]['content-type']; - } - if (!isset($httpHeaders['Content-Type'])) { - $httpHeaders['Content-Type'] = $formats[$format]['content-type']; - } - array_walk($httpHeaders, function (&$value, $header) { - $value = $header . ': ' . $value; - }); - curl_setopt($this->curl, CURLOPT_HTTPHEADER, $httpHeaders); -// Proveď samotnou operaci $this->lastCurlResponse = curl_exec($this->curl); $this->curlInfo = curl_getinfo($this->curl); $this->curlInfo['when'] = microtime(); - $this->responseFormat = $this->contentTypeToResponseFormat(strval($this->curlInfo['content_type']), $url); $this->lastResponseCode = $this->curlInfo['http_code']; $this->lastCurlError = curl_error($this->curl); if (strlen($this->lastCurlError)) { @@ -120,13 +150,39 @@ public function doCurlRequest($url, $method, $format = null) throw new Exception($msg, $this); } } + return $this->lastResponseCode; + } - if ($this->debug === true) { - $this->saveDebugFiles(); - } + /** + * Curl Error getter + * + * @return string + */ + public function getErrors() + { + return $this->lastCurlError; + } + + /** + * + * @return + */ + public function getLastResponseCode() + { return $this->lastResponseCode; } + /** + * Convert XML to array + */ + public static function xml2array($xmlObject, $out = []) + { + foreach ((array) $xmlObject as $index => $node) { + $out[$index] = (is_object($node) || is_array($node)) ? self::xml2array($node) : $node; + } + return $out; + } + /** * Realpad server disconnect. */ @@ -138,6 +194,24 @@ public function disconnect() $this->curl = null; } + /** + * + * @return array + */ + public function listResources() + { + $responseData = []; + $responseCode = $this->doCurlRequest($this->baseEndpoint . 'ws/v10/list-resources', 'POST'); + if ($responseCode == 200) { + $responseRaw = self::xml2array(new \SimpleXMLElement($this->lastCurlResponse)); + foreach ($responseRaw['resource'] as $position => $attributes) { + $responseData[$attributes['@attributes']['uid']] = array_values($attributes)[0]; + $responseData[$attributes['@attributes']['uid']]['position'] = $position; + } + } + return $responseData; + } + /** * */ @@ -145,4 +219,26 @@ public function __destruct() { $this->disconnect(); } + + /** + * + * @return array + */ + public function listCustomers() + { + $responseCode = $this->doCurlRequest($this->baseEndpoint . 'ws/v10/list-excel-customers', 'POST'); + $customersData = []; + if ($responseCode == 200) { + $xls = sys_get_temp_dir() . '/' . \Ease\Functions::randomString() . '.xls'; + file_put_contents($xls, $this->lastCurlResponse); + $spreadsheet = IOFactory::load($xls); + unlink($xls); + $customersDataRaw = $spreadsheet->getActiveSheet()->toArray(null, true, true, true); + unset($customersDataRaw[1]); + foreach ($customersDataRaw as $recordId => $recordData) { + $customersData[$recordId] = array_combine($customersDataRaw[1], $recordData); + } + } + return $customersData; + } } diff --git a/src/Exception.php b/src/Exception.php new file mode 100644 index 0000000..9d8606a --- /dev/null +++ b/src/Exception.php @@ -0,0 +1,60 @@ +errorMessages = $caller->getErrors(); + parent::__construct(get_class($caller) . ': ' . $message, $caller->getLastResponseCode(), $previous); + } + + /** + * Get (first) error message + * + * @param int $index which message + * + * @return string + */ + public function getErrorMessage($index = 0) + { + return $this->errorMessages[$index]; + } + + /** + * All stored Error messages + * + * @return array + */ + public function getErrorMessages() + { + return $this->errorMessages; + } +} diff --git a/tests/ApiClientTest.php b/tests/ApiClientTest.php new file mode 100644 index 0000000..92aee9f --- /dev/null +++ b/tests/ApiClientTest.php @@ -0,0 +1,92 @@ +object = new ApiClient(); + } + + /** + * Tears down the fixture, for example, closes a network connection. + * This method is called after a test is executed. + */ + protected function tearDown(): void + { + + } + + /** + * @covers SpojeNet\Realpad\ApiClient::curlInit + */ + public function testcurlInit() + { + $this->assertIsObject($this->object->curlInit()); + } + + /** + * @covers SpojeNet\Realpad\ApiClient::doCurlRequest + */ + public function testdoCurlRequest() + { + $this->assertEquals(200, $this->object->doCurlRequest('https://realpadsoftware.com/cs/')); + } + + /** + * @covers SpojeNet\Realpad\ApiClient::xml2array + */ + public function testxml2array() + { + $xml = 'ToveJaniReminderthis weekend!'; + $array = ['to' => 'Tove', 'from' => 'Jani', 'heading' => 'Reminder', 'body' => 'this weekend!']; + $this->assertEquals($array, $this->object->xml2array(new \SimpleXMLElement($xml))); + } + + /** + * @covers SpojeNet\Realpad\ApiClient::disconnect + */ + public function testdisconnect() + { + $this->assertNull($this->object->disconnect()); + } + + /** + * @covers SpojeNet\Realpad\ApiClient::listResources + */ + public function testlistResources() + { + $this->assertIsArray($this->object->listResources()); + } + + /** + * @covers SpojeNet\Realpad\ApiClient::listCustomers + */ + public function testlistCustomers() + { + $this->assertIsArray($this->object->listCustomers()); + } + + /** + * @covers SpojeNet\Realpad\ApiClient::__destruct + */ + public function test__destruct() + { + $this->assertEmpty($this->object->__destruct()); + } +} diff --git a/tests/ExceptionTest.php b/tests/ExceptionTest.php new file mode 100644 index 0000000..06934b8 --- /dev/null +++ b/tests/ExceptionTest.php @@ -0,0 +1,52 @@ +doCurlRequest('https://realpadsoftware.com/cs/'); + $this->object = new Exception('Test Message', $client); + } + + /** + * Tears down the fixture, for example, closes a network connection. + * This method is called after a test is executed. + */ + protected function tearDown(): void + { + + } + + /** + * @covers SpojeNet\Realpad\Exception::getErrorMessage + */ + public function testgetErrorMessage() + { + $this->assertEquals('', $this->object->getErrorMessage()); + } + + /** + * @covers SpojeNet\Realpad\Exception::getErrorMessages + */ + public function testgetErrorMessages() + { + $this->assertEquals('', $this->object->getErrorMessages()); + } +} diff --git a/tests/bootstrap.php b/tests/bootstrap.php new file mode 100644 index 0000000..90fa9fa --- /dev/null +++ b/tests/bootstrap.php @@ -0,0 +1,5 @@ +