diff --git a/.gitignore b/.gitignore index aec89bf23..c8df0a7e2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ .php_cs.cache vendor +.idea +composer.phar diff --git a/composer.json b/composer.json index 0bcc100bb..fb7f4ae0f 100644 --- a/composer.json +++ b/composer.json @@ -4,6 +4,7 @@ }, "require": { "ext-zip": "*", - "ext-json": "*" + "ext-json": "*", + "ubnt/ucrm-plugin-sdk": "^0.8.1" } } diff --git a/composer.lock b/composer.lock index 3cafceed4..157fe12fe 100644 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,764 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "5244a2322296ef995cb05d9d7cd7fee2", - "packages": [], + "content-hash": "d1267552c53d52c061ccc566ac54a707", + "packages": [ + { + "name": "guzzlehttp/guzzle", + "version": "6.5.5", + "source": { + "type": "git", + "url": "https://github.com/guzzle/guzzle.git", + "reference": "9d4290de1cfd701f38099ef7e183b64b4b7b0c5e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/9d4290de1cfd701f38099ef7e183b64b4b7b0c5e", + "reference": "9d4290de1cfd701f38099ef7e183b64b4b7b0c5e", + "shasum": "" + }, + "require": { + "ext-json": "*", + "guzzlehttp/promises": "^1.0", + "guzzlehttp/psr7": "^1.6.1", + "php": ">=5.5", + "symfony/polyfill-intl-idn": "^1.17.0" + }, + "require-dev": { + "ext-curl": "*", + "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.4 || ^7.0", + "psr/log": "^1.1" + }, + "suggest": { + "psr/log": "Required for using the Log middleware" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "6.5-dev" + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\": "src/" + }, + "files": [ + "src/functions_include.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + } + ], + "description": "Guzzle is a PHP HTTP client library", + "homepage": "http://guzzlephp.org/", + "keywords": [ + "client", + "curl", + "framework", + "http", + "http client", + "rest", + "web service" + ], + "support": { + "issues": "https://github.com/guzzle/guzzle/issues", + "source": "https://github.com/guzzle/guzzle/tree/6.5" + }, + "time": "2020-06-16T21:01:06+00:00" + }, + { + "name": "guzzlehttp/promises", + "version": "1.5.1", + "source": { + "type": "git", + "url": "https://github.com/guzzle/promises.git", + "reference": "fe752aedc9fd8fcca3fe7ad05d419d32998a06da" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/promises/zipball/fe752aedc9fd8fcca3fe7ad05d419d32998a06da", + "reference": "fe752aedc9fd8fcca3fe7ad05d419d32998a06da", + "shasum": "" + }, + "require": { + "php": ">=5.5" + }, + "require-dev": { + "symfony/phpunit-bridge": "^4.4 || ^5.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.5-dev" + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Promise\\": "src/" + }, + "files": [ + "src/functions_include.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle promises library", + "keywords": [ + "promise" + ], + "support": { + "issues": "https://github.com/guzzle/promises/issues", + "source": "https://github.com/guzzle/promises/tree/1.5.1" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/promises", + "type": "tidelift" + } + ], + "time": "2021-10-22T20:56:57+00:00" + }, + { + "name": "guzzlehttp/psr7", + "version": "1.8.3", + "source": { + "type": "git", + "url": "https://github.com/guzzle/psr7.git", + "reference": "1afdd860a2566ed3c2b0b4a3de6e23434a79ec85" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/1afdd860a2566ed3c2b0b4a3de6e23434a79ec85", + "reference": "1afdd860a2566ed3c2b0b4a3de6e23434a79ec85", + "shasum": "" + }, + "require": { + "php": ">=5.4.0", + "psr/http-message": "~1.0", + "ralouphie/getallheaders": "^2.0.5 || ^3.0.0" + }, + "provide": { + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "ext-zlib": "*", + "phpunit/phpunit": "~4.8.36 || ^5.7.27 || ^6.5.14 || ^7.5.20 || ^8.5.8 || ^9.3.10" + }, + "suggest": { + "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.7-dev" + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Psr7\\": "src/" + }, + "files": [ + "src/functions_include.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "PSR-7 message implementation that also provides common utility methods", + "keywords": [ + "http", + "message", + "psr-7", + "request", + "response", + "stream", + "uri", + "url" + ], + "support": { + "issues": "https://github.com/guzzle/psr7/issues", + "source": "https://github.com/guzzle/psr7/tree/1.8.3" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7", + "type": "tidelift" + } + ], + "time": "2021-10-05T13:56:00+00:00" + }, + { + "name": "psr/http-message", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/f6561bf28d520154e4b0ec72be95418abe6d9363", + "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-message/tree/master" + }, + "time": "2016-08-06T14:39:51+00:00" + }, + { + "name": "ralouphie/getallheaders", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/ralouphie/getallheaders.git", + "reference": "120b605dfeb996808c31b6477290a714d356e822" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822", + "reference": "120b605dfeb996808c31b6477290a714d356e822", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.1", + "phpunit/phpunit": "^5 || ^6.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/getallheaders.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ralph Khattar", + "email": "ralph.khattar@gmail.com" + } + ], + "description": "A polyfill for getallheaders.", + "support": { + "issues": "https://github.com/ralouphie/getallheaders/issues", + "source": "https://github.com/ralouphie/getallheaders/tree/develop" + }, + "time": "2019-03-08T08:55:37+00:00" + }, + { + "name": "symfony/filesystem", + "version": "v4.3.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/filesystem.git", + "reference": "9abbb7ef96a51f4d7e69627bc6f63307994e4263" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/9abbb7ef96a51f4d7e69627bc6f63307994e4263", + "reference": "9abbb7ef96a51f4d7e69627bc6f63307994e4263", + "shasum": "" + }, + "require": { + "php": "^7.1.3", + "symfony/polyfill-ctype": "~1.8" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.3-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Filesystem\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Filesystem Component", + "homepage": "https://symfony.com", + "time": "2019-08-20T14:07:54+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.12.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "550ebaac289296ce228a706d0867afc34687e3f4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/550ebaac289296ce228a706d0867afc34687e3f4", + "reference": "550ebaac289296ce228a706d0867afc34687e3f4", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.12-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "time": "2019-08-06T08:03:45+00:00" + }, + { + "name": "symfony/polyfill-intl-idn", + "version": "v1.23.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-idn.git", + "reference": "65bd267525e82759e7d8c4e8ceea44f398838e65" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/65bd267525e82759e7d8c4e8ceea44f398838e65", + "reference": "65bd267525e82759e7d8c4e8ceea44f398838e65", + "shasum": "" + }, + "require": { + "php": ">=7.1", + "symfony/polyfill-intl-normalizer": "^1.10", + "symfony/polyfill-php72": "^1.10" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.23-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Intl\\Idn\\": "" + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Laurent Bassin", + "email": "laurent@bassin.info" + }, + { + "name": "Trevor Rowbotham", + "email": "trevor.rowbotham@pm.me" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's idn_to_ascii and idn_to_utf8 functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "idn", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.23.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2021-05-27T09:27:20+00:00" + }, + { + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.23.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "8590a5f561694770bdcd3f9b5c69dde6945028e8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/8590a5f561694770bdcd3f9b5c69dde6945028e8", + "reference": "8590a5f561694770bdcd3f9b5c69dde6945028e8", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.23-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, + "files": [ + "bootstrap.php" + ], + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's Normalizer class and related functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "intl", + "normalizer", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.23.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2021-02-19T12:13:01+00:00" + }, + { + "name": "symfony/polyfill-php72", + "version": "v1.12.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php72.git", + "reference": "04ce3335667451138df4307d6a9b61565560199e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/04ce3335667451138df4307d6a9b61565560199e", + "reference": "04ce3335667451138df4307d6a9b61565560199e", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.12-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Php72\\": "" + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 7.2+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "time": "2019-08-06T08:03:45+00:00" + }, + { + "name": "ubnt/ucrm-plugin-sdk", + "version": "0.8.1", + "source": { + "type": "git", + "url": "https://github.com/Ubiquiti-App/UCRM-Plugin-SDK.git", + "reference": "181e8a13b5b47ce2cb3cd6a5380e8fcf899d511a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Ubiquiti-App/UCRM-Plugin-SDK/zipball/181e8a13b5b47ce2cb3cd6a5380e8fcf899d511a", + "reference": "181e8a13b5b47ce2cb3cd6a5380e8fcf899d511a", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "ext-json": "*", + "guzzlehttp/guzzle": "~6.0", + "php": ">=7.3.0", + "symfony/filesystem": "^4.1" + }, + "require-dev": { + "eloquent/phony-phpunit": "^5.0", + "eloquent/phpstan-phony": "^0.7.0", + "ocramius/package-versions": "<1.6", + "php-coveralls/php-coveralls": "^2.2", + "phpstan/phpstan": "^0.12.15", + "phpstan/phpstan-strict-rules": "^0.12", + "phpunit/phpunit": "^8.5", + "symplify/easy-coding-standard": "^7.2" + }, + "suggest": { + "ext-zip": "Needed for pack-plugin script." + }, + "bin": [ + "bin/pack-plugin" + ], + "type": "library", + "autoload": { + "psr-4": { + "Ubnt\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "UCRM plugin SDK", + "homepage": "https://github.com/Ubiquiti-App/UCRM-Plugin-SDK", + "keywords": [ + "sdk", + "ucrm" + ], + "support": { + "issues": "https://github.com/Ubiquiti-App/UCRM-Plugin-SDK/issues", + "source": "https://github.com/Ubiquiti-App/UCRM-Plugin-SDK/tree/master" + }, + "time": "2020-07-16T13:32:38+00:00" + } + ], "packages-dev": [ { "name": "composer/semver", @@ -727,56 +1483,6 @@ ], "time": "2019-06-20T06:46:26+00:00" }, - { - "name": "symfony/filesystem", - "version": "v4.3.4", - "source": { - "type": "git", - "url": "https://github.com/symfony/filesystem.git", - "reference": "9abbb7ef96a51f4d7e69627bc6f63307994e4263" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/9abbb7ef96a51f4d7e69627bc6f63307994e4263", - "reference": "9abbb7ef96a51f4d7e69627bc6f63307994e4263", - "shasum": "" - }, - "require": { - "php": "^7.1.3", - "symfony/polyfill-ctype": "~1.8" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.3-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Component\\Filesystem\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony Filesystem Component", - "homepage": "https://symfony.com", - "time": "2019-08-20T14:07:54+00:00" - }, { "name": "symfony/finder", "version": "v4.3.4", @@ -880,64 +1586,6 @@ ], "time": "2019-08-08T09:29:19+00:00" }, - { - "name": "symfony/polyfill-ctype", - "version": "v1.12.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "550ebaac289296ce228a706d0867afc34687e3f4" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/550ebaac289296ce228a706d0867afc34687e3f4", - "reference": "550ebaac289296ce228a706d0867afc34687e3f4", - "shasum": "" - }, - "require": { - "php": ">=5.3.3" - }, - "suggest": { - "ext-ctype": "For best performance" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.12-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Ctype\\": "" - }, - "files": [ - "bootstrap.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Gert de Pagter", - "email": "BackEndTea@gmail.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill for ctype functions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "ctype", - "polyfill", - "portable" - ], - "time": "2019-08-06T08:03:45+00:00" - }, { "name": "symfony/polyfill-mbstring", "version": "v1.12.0", @@ -1056,61 +1704,6 @@ ], "time": "2019-08-06T08:03:45+00:00" }, - { - "name": "symfony/polyfill-php72", - "version": "v1.12.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-php72.git", - "reference": "04ce3335667451138df4307d6a9b61565560199e" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/04ce3335667451138df4307d6a9b61565560199e", - "reference": "04ce3335667451138df4307d6a9b61565560199e", - "shasum": "" - }, - "require": { - "php": ">=5.3.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.12-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Php72\\": "" - }, - "files": [ - "bootstrap.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill backporting some PHP 7.2+ features to lower PHP versions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "polyfill", - "portable", - "shim" - ], - "time": "2019-08-06T08:03:45+00:00" - }, { "name": "symfony/polyfill-php73", "version": "v1.12.0", @@ -1336,5 +1929,6 @@ "ext-zip": "*", "ext-json": "*" }, - "platform-dev": [] + "platform-dev": [], + "plugin-api-version": "2.1.0" } diff --git a/plugins/quickbooks-online/README.md b/plugins/quickbooks-online/README.md index 53019f9fa..2f64d3f1e 100644 --- a/plugins/quickbooks-online/README.md +++ b/plugins/quickbooks-online/README.md @@ -1,10 +1,10 @@ # QuickBooks Online import -This plugin handles import of your [UCRM](https://ucrm.ubnt.com/) customers, payments and invoices to +This plugin handles import of your [UCRM](https://ucrm.ubnt.com/) customers, payments, credit memos, and invoices to [QuickBooks Online](https://quickbooks.intuit.com/online/). ## About UCRM data integration - The UCRM data are the single source of truth, the plugin only pushes data from UCRM to QuickBooks. -- On first run, all clients, payments and invoices are pushed to QuickBooks. +- On first run, all clients, payments, credit memos and invoices are pushed to QuickBooks. - All following runs only pushes the newly created entities (i.e. new clients, new payments and new invoices with higher ID than the last). ## How to configure the plugin @@ -45,10 +45,25 @@ This plugin handles import of your [UCRM](https://ucrm.ubnt.com/) customers, pay ## To be done in future version (Feel free to push your upgrades in this repo.) -- Configurable date of the first payment or invoice to be imported. - Remove entity from QB when the related entity is deleted in UCRM. ## Changelog +### 2.1.0 (2023-12-04) +- Update dependencies and make it work for UISP 2.3.57 +### 2.0.0 (2021-12-01) +- Export Credit Memos. Set the `Date to start for Credit Memo export` value in the +plugin config so that older credits that you have already entered will not export. +- Don't generate new items all the time. Re-use the same items. +- Make it possible to use `Income account name` instead of `Income account ID` +in plugin config +- Export phone # and email address for new clients. +- Cleaner payment exports. Only export 1 transaction per payment. +- Export payment method, and optionally "Deposit to" account, to QBO along with the payment +- Logs get cleaned out so that only 10,000 rows are kept after file grows to over 1 Mb. + This improves plugin interaction speed. +- Add better error handling so that network hiccups don't cause +individual transactions to fail that easily. +- Make it possible to set logging level dynamically from plugin config ### 1.1.3 (2019-01-04) - draft, void and proforma invoices are no longer exported ([#100](https://github.com/Ubiquiti-App/UCRM-plugins/pull/100)) - added options to limit exported invoices and payments by start date ([#94](https://github.com/Ubiquiti-App/UCRM-plugins/pull/94), [#95](https://github.com/Ubiquiti-App/UCRM-plugins/pull/95), [#96](https://github.com/Ubiquiti-App/UCRM-plugins/pull/96)) diff --git a/plugins/quickbooks-online/quickbooks-online.zip b/plugins/quickbooks-online/quickbooks-online.zip index 6a0b5911b..2f20b442f 100644 Binary files a/plugins/quickbooks-online/quickbooks-online.zip and b/plugins/quickbooks-online/quickbooks-online.zip differ diff --git a/plugins/quickbooks-online/src/composer.json b/plugins/quickbooks-online/src/composer.json index 0193dbc01..ce655147e 100644 --- a/plugins/quickbooks-online/src/composer.json +++ b/plugins/quickbooks-online/src/composer.json @@ -10,9 +10,8 @@ } }, "require": { - "quickbooks/v3-php-sdk": "^5.0", - "php-di/php-di": "^5.4", - "doctrine/cache": "^1.7", + "quickbooks/v3-php-sdk": "^6.1", + "php-di/php-di": "^7.0", "katzgrau/klogger": "^1.2" } } diff --git a/plugins/quickbooks-online/src/composer.lock b/plugins/quickbooks-online/src/composer.lock index 1481b12c6..1ef35cfcb 100644 --- a/plugins/quickbooks-online/src/composer.lock +++ b/plugins/quickbooks-online/src/composer.lock @@ -4,78 +4,37 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "ede24fb45056653f2ce6cb774b441197", + "content-hash": "cd58d15f5e6d0f9adbfff7648e9a6dc6", "packages": [ { - "name": "container-interop/container-interop", - "version": "1.2.0", - "source": { - "type": "git", - "url": "https://github.com/container-interop/container-interop.git", - "reference": "79cbf1341c22ec75643d841642dd5d6acd83bdb8" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/container-interop/container-interop/zipball/79cbf1341c22ec75643d841642dd5d6acd83bdb8", - "reference": "79cbf1341c22ec75643d841642dd5d6acd83bdb8", - "shasum": "" - }, - "require": { - "psr/container": "^1.0" - }, - "type": "library", - "autoload": { - "psr-4": { - "Interop\\Container\\": "src/Interop/Container/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "Promoting the interoperability of container objects (DIC, SL, etc.)", - "homepage": "https://github.com/container-interop/container-interop", - "time": "2017-02-14T19:40:03+00:00" - }, - { - "name": "doctrine/cache", - "version": "v1.7.1", + "name": "katzgrau/klogger", + "version": "1.2.2", "source": { "type": "git", - "url": "https://github.com/doctrine/cache.git", - "reference": "b3217d58609e9c8e661cd41357a54d926c4a2a1a" + "url": "https://github.com/katzgrau/KLogger.git", + "reference": "36481c69db9305169a2ceadead25c2acaabd567c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/cache/zipball/b3217d58609e9c8e661cd41357a54d926c4a2a1a", - "reference": "b3217d58609e9c8e661cd41357a54d926c4a2a1a", + "url": "https://api.github.com/repos/katzgrau/KLogger/zipball/36481c69db9305169a2ceadead25c2acaabd567c", + "reference": "36481c69db9305169a2ceadead25c2acaabd567c", "shasum": "" }, "require": { - "php": "~7.1" - }, - "conflict": { - "doctrine/common": ">2.2,<2.4" + "php": ">=5.3", + "psr/log": "^1.0.0" }, "require-dev": { - "alcaeus/mongo-php-adapter": "^1.1", - "mongodb/mongodb": "^1.1", - "phpunit/phpunit": "^5.7", - "predis/predis": "~1.0" - }, - "suggest": { - "alcaeus/mongo-php-adapter": "Required to use legacy MongoDB driver" + "phpunit/phpunit": "^6.0.0" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.7.x-dev" - } - }, "autoload": { "psr-4": { - "Doctrine\\Common\\Cache\\": "lib/Doctrine/Common/Cache" - } + "Katzgrau\\KLogger\\": "src/" + }, + "classmap": [ + "src/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -83,63 +42,57 @@ ], "authors": [ { - "name": "Roman Borschel", - "email": "roman@code-factory.org" - }, - { - "name": "Benjamin Eberlei", - "email": "kontakt@beberlei.de" - }, - { - "name": "Guilherme Blanco", - "email": "guilhermeblanco@gmail.com" - }, - { - "name": "Jonathan Wage", - "email": "jonwage@gmail.com" + "name": "Kenny Katzgrau", + "email": "katzgrau@gmail.com" }, { - "name": "Johannes Schmitt", - "email": "schmittjoh@gmail.com" + "name": "Dan Horrigan", + "email": "dan@dhorrigan.com" } ], - "description": "Caching library offering an object-oriented API for many cache backends", - "homepage": "http://www.doctrine-project.org", + "description": "A Simple Logging Class", "keywords": [ - "cache", - "caching" + "logging" ], - "time": "2017-08-25T07:02:50+00:00" + "support": { + "issues": "https://github.com/katzgrau/KLogger/issues", + "source": "https://github.com/katzgrau/KLogger/tree/1.2.2" + }, + "time": "2022-07-29T20:41:14+00:00" }, { - "name": "katzgrau/klogger", - "version": "1.2.1", + "name": "laravel/serializable-closure", + "version": "v1.3.3", "source": { "type": "git", - "url": "https://github.com/katzgrau/KLogger.git", - "reference": "a4ed373fa8a214aa4ae7aa4f221fe2c6ce862ef1" + "url": "https://github.com/laravel/serializable-closure.git", + "reference": "3dbf8a8e914634c48d389c1234552666b3d43754" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/katzgrau/KLogger/zipball/a4ed373fa8a214aa4ae7aa4f221fe2c6ce862ef1", - "reference": "a4ed373fa8a214aa4ae7aa4f221fe2c6ce862ef1", + "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/3dbf8a8e914634c48d389c1234552666b3d43754", + "reference": "3dbf8a8e914634c48d389c1234552666b3d43754", "shasum": "" }, "require": { - "php": ">=5.3", - "psr/log": "^1.0.0" + "php": "^7.3|^8.0" }, "require-dev": { - "phpunit/phpunit": "4.0.*" + "nesbot/carbon": "^2.61", + "pestphp/pest": "^1.21.3", + "phpstan/phpstan": "^1.8.2", + "symfony/var-dumper": "^5.4.11" }, "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, "autoload": { "psr-4": { - "Katzgrau\\KLogger\\": "src/" - }, - "classmap": [ - "src/" - ] + "Laravel\\SerializableClosure\\": "src/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -147,40 +100,48 @@ ], "authors": [ { - "name": "Kenny Katzgrau", - "email": "katzgrau@gmail.com" + "name": "Taylor Otwell", + "email": "taylor@laravel.com" }, { - "name": "Dan Horrigan", - "email": "dan@dhorrigan.com" + "name": "Nuno Maduro", + "email": "nuno@laravel.com" } ], - "description": "A Simple Logging Class", + "description": "Laravel Serializable Closure provides an easy and secure way to serialize closures in PHP.", "keywords": [ - "logging" + "closure", + "laravel", + "serializable" ], - "time": "2016-11-07T19:29:14+00:00" + "support": { + "issues": "https://github.com/laravel/serializable-closure/issues", + "source": "https://github.com/laravel/serializable-closure" + }, + "time": "2023-11-08T14:08:06+00:00" }, { "name": "php-di/invoker", - "version": "1.3.3", + "version": "2.3.4", "source": { "type": "git", "url": "https://github.com/PHP-DI/Invoker.git", - "reference": "1f4ca63b9abc66109e53b255e465d0ddb5c2e3f7" + "reference": "33234b32dafa8eb69202f950a1fc92055ed76a86" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHP-DI/Invoker/zipball/1f4ca63b9abc66109e53b255e465d0ddb5c2e3f7", - "reference": "1f4ca63b9abc66109e53b255e465d0ddb5c2e3f7", + "url": "https://api.github.com/repos/PHP-DI/Invoker/zipball/33234b32dafa8eb69202f950a1fc92055ed76a86", + "reference": "33234b32dafa8eb69202f950a1fc92055ed76a86", "shasum": "" }, "require": { - "container-interop/container-interop": "~1.1" + "php": ">=7.3", + "psr/container": "^1.0|^2.0" }, "require-dev": { "athletic/athletic": "~0.1.8", - "phpunit/phpunit": "~4.5" + "mnapoli/hard-mode": "~0.3.0", + "phpunit/phpunit": "^9.0" }, "type": "library", "autoload": { @@ -202,129 +163,112 @@ "invoke", "invoker" ], - "time": "2016-07-14T13:09:58+00:00" + "support": { + "issues": "https://github.com/PHP-DI/Invoker/issues", + "source": "https://github.com/PHP-DI/Invoker/tree/2.3.4" + }, + "funding": [ + { + "url": "https://github.com/mnapoli", + "type": "github" + } + ], + "time": "2023-09-08T09:24:21+00:00" }, { "name": "php-di/php-di", - "version": "5.4.6", + "version": "7.0.6", "source": { "type": "git", "url": "https://github.com/PHP-DI/PHP-DI.git", - "reference": "3f9255659595f3e289f473778bb6c51aa72abbbd" + "reference": "8097948a89f6ec782839b3e958432f427cac37fd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHP-DI/PHP-DI/zipball/3f9255659595f3e289f473778bb6c51aa72abbbd", - "reference": "3f9255659595f3e289f473778bb6c51aa72abbbd", + "url": "https://api.github.com/repos/PHP-DI/PHP-DI/zipball/8097948a89f6ec782839b3e958432f427cac37fd", + "reference": "8097948a89f6ec782839b3e958432f427cac37fd", "shasum": "" }, "require": { - "container-interop/container-interop": "~1.2", - "php": ">=5.5.0", - "php-di/invoker": "^1.3.2", - "php-di/phpdoc-reader": "^2.0.1", - "psr/container": "~1.0" + "laravel/serializable-closure": "^1.0", + "php": ">=8.0", + "php-di/invoker": "^2.0", + "psr/container": "^1.1 || ^2.0" }, "provide": { - "container-interop/container-interop-implementation": "^1.0", "psr/container-implementation": "^1.0" }, - "replace": { - "mnapoli/php-di": "*" - }, "require-dev": { - "doctrine/annotations": "~1.2", - "doctrine/cache": "~1.4", - "mnapoli/phpunit-easymock": "~0.2.0", - "ocramius/proxy-manager": "~1.0|~2.0", - "phpbench/phpbench": "@dev", - "phpunit/phpunit": "~4.5" + "friendsofphp/php-cs-fixer": "^3", + "friendsofphp/proxy-manager-lts": "^1", + "mnapoli/phpunit-easymock": "^1.3", + "phpunit/phpunit": "^9.5", + "vimeo/psalm": "^4.6" }, "suggest": { - "doctrine/annotations": "Install it if you want to use annotations (version ~1.2)", - "doctrine/cache": "Install it if you want to use the cache (version ~1.4)", - "ocramius/proxy-manager": "Install it if you want to use lazy injection (version ~1.0 or ~2.0)" + "friendsofphp/proxy-manager-lts": "Install it if you want to use lazy injection (version ^1)" }, "type": "library", "autoload": { - "psr-4": { - "DI\\": "src/DI/" - }, "files": [ - "src/DI/functions.php" - ] + "src/functions.php" + ], + "psr-4": { + "DI\\": "src/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], "description": "The dependency injection container for humans", - "homepage": "http://php-di.org/", + "homepage": "https://php-di.org/", "keywords": [ + "PSR-11", "container", + "container-interop", "dependency injection", - "di" + "di", + "ioc", + "psr11" ], - "time": "2017-12-03T08:20:27+00:00" - }, - { - "name": "php-di/phpdoc-reader", - "version": "2.1.0", - "source": { - "type": "git", - "url": "https://github.com/PHP-DI/PhpDocReader.git", - "reference": "7d0de60b9341933c8afd172a6255cd7557601e0e" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/PHP-DI/PhpDocReader/zipball/7d0de60b9341933c8afd172a6255cd7557601e0e", - "reference": "7d0de60b9341933c8afd172a6255cd7557601e0e", - "shasum": "" - }, - "require": { - "php": ">=5.4.0" - }, - "require-dev": { - "phpunit/phpunit": "~4.6" + "support": { + "issues": "https://github.com/PHP-DI/PHP-DI/issues", + "source": "https://github.com/PHP-DI/PHP-DI/tree/7.0.6" }, - "type": "library", - "autoload": { - "psr-4": { - "PhpDocReader\\": "src/PhpDocReader" + "funding": [ + { + "url": "https://github.com/mnapoli", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/php-di/php-di", + "type": "tidelift" } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "PhpDocReader parses @var and @param values in PHP docblocks (supports namespaced class names with the same resolution rules as PHP)", - "keywords": [ - "phpdoc", - "reflection" ], - "time": "2018-02-18T17:39:01+00:00" + "time": "2023-11-02T10:04:50+00:00" }, { "name": "psr/container", - "version": "1.0.0", + "version": "2.0.2", "source": { "type": "git", "url": "https://github.com/php-fig/container.git", - "reference": "b7ce3b176482dbbc1245ebf52b181af44c2cf55f" + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/container/zipball/b7ce3b176482dbbc1245ebf52b181af44c2cf55f", - "reference": "b7ce3b176482dbbc1245ebf52b181af44c2cf55f", + "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", "shasum": "" }, "require": { - "php": ">=5.3.0" + "php": ">=7.4.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0.x-dev" + "dev-master": "2.0.x-dev" } }, "autoload": { @@ -339,7 +283,7 @@ "authors": [ { "name": "PHP-FIG", - "homepage": "http://www.php-fig.org/" + "homepage": "https://www.php-fig.org/" } ], "description": "Common Container Interface (PHP FIG PSR-11)", @@ -351,20 +295,24 @@ "container-interop", "psr" ], - "time": "2017-02-14T16:28:37+00:00" + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/2.0.2" + }, + "time": "2021-11-05T16:47:00+00:00" }, { "name": "psr/log", - "version": "1.0.2", + "version": "1.1.4", "source": { "type": "git", "url": "https://github.com/php-fig/log.git", - "reference": "4ebe3a8bf773a19edfe0a84b6585ba3d401b724d" + "reference": "d49695b909c3b7628b6289db5479a1c204601f11" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/log/zipball/4ebe3a8bf773a19edfe0a84b6585ba3d401b724d", - "reference": "4ebe3a8bf773a19edfe0a84b6585ba3d401b724d", + "url": "https://api.github.com/repos/php-fig/log/zipball/d49695b909c3b7628b6289db5479a1c204601f11", + "reference": "d49695b909c3b7628b6289db5479a1c204601f11", "shasum": "" }, "require": { @@ -373,7 +321,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0.x-dev" + "dev-master": "1.1.x-dev" } }, "autoload": { @@ -388,7 +336,7 @@ "authors": [ { "name": "PHP-FIG", - "homepage": "http://www.php-fig.org/" + "homepage": "https://www.php-fig.org/" } ], "description": "Common interface for logging libraries", @@ -398,27 +346,38 @@ "psr", "psr-3" ], - "time": "2016-10-10T12:19:37+00:00" + "support": { + "source": "https://github.com/php-fig/log/tree/1.1.4" + }, + "time": "2021-05-03T11:20:27+00:00" }, { "name": "quickbooks/v3-php-sdk", - "version": "v5.0.3", + "version": "v6.1.2", "source": { "type": "git", "url": "https://github.com/intuit/QuickBooks-V3-PHP-SDK.git", - "reference": "797d1a2dc2685b4f103e938c5d4f43da46af33ae" + "reference": "a4039a8257633ed1481dfe1e50a7881016fa0b1d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/intuit/QuickBooks-V3-PHP-SDK/zipball/797d1a2dc2685b4f103e938c5d4f43da46af33ae", - "reference": "797d1a2dc2685b4f103e938c5d4f43da46af33ae", + "url": "https://api.github.com/repos/intuit/QuickBooks-V3-PHP-SDK/zipball/a4039a8257633ed1481dfe1e50a7881016fa0b1d", + "reference": "a4039a8257633ed1481dfe1e50a7881016fa0b1d", "shasum": "" }, "require": { + "ext-dom": "*", + "ext-mbstring": "*", "php": ">=5.6.0" }, "require-dev": { - "phpunit/phpunit": "^5.7" + "php-mock/php-mock": "^2.3", + "php-mock/php-mock-phpunit": "^2.6", + "phpunit/phpunit": "^5.0 || ^6.0 || ^7.0 || ^8" + }, + "suggest": { + "ext-curl": "Uses Curl to make HTTP Requests", + "guzzlehttp/guzzle": "Uses Guzzle to make HTTP Requests" }, "type": "library", "autoload": { @@ -432,8 +391,8 @@ ], "authors": [ { - "name": "hlu2", - "email": "Hao_Lu@intuit.com" + "name": "abisalehalliprasan", + "email": "anil_kumar3@intuit.com" } ], "description": "The Official PHP SDK for QuickBooks Online Accounting API", @@ -445,7 +404,11 @@ "rest", "smallbusiness" ], - "time": "2018-08-09T00:18:21+00:00" + "support": { + "issues": "https://github.com/intuit/QuickBooks-V3-PHP-SDK/issues", + "source": "https://github.com/intuit/QuickBooks-V3-PHP-SDK/tree/v6.1.2" + }, + "time": "2023-08-02T04:48:35+00:00" } ], "packages-dev": [], @@ -455,5 +418,6 @@ "prefer-stable": false, "prefer-lowest": false, "platform": [], - "platform-dev": [] + "platform-dev": [], + "plugin-api-version": "2.2.0" } diff --git a/plugins/quickbooks-online/src/main.php b/plugins/quickbooks-online/src/main.php index f1e3c8387..bccb8c909 100644 --- a/plugins/quickbooks-online/src/main.php +++ b/plugins/quickbooks-online/src/main.php @@ -6,13 +6,12 @@ (function () { $builder = new \DI\ContainerBuilder(); - $builder->setDefinitionCache(new \Doctrine\Common\Cache\ApcuCache()); $container = $builder->build(); $plugin = $container->get(\QBExport\Plugin::class); try { $plugin->run(); } catch (Exception $exception) { - $logger = new \QBExport\Service\Logger(); + $logger = new \QBExport\Service\Logger(null); $logger->error($exception->getMessage()); } })(); diff --git a/plugins/quickbooks-online/src/manifest.json b/plugins/quickbooks-online/src/manifest.json old mode 100644 new mode 100755 index 0e968adb6..4eb10aef0 --- a/plugins/quickbooks-online/src/manifest.json +++ b/plugins/quickbooks-online/src/manifest.json @@ -5,7 +5,7 @@ "displayName": "QuickBooks Online export", "description": "This plugin exports payments and invoices to QuickBooks Online", "url": "https://github.com/Ubiquiti-App/UCRM-plugins/tree/master/plugins/quickbooks-online", - "version": "1.1.3", + "version": "2.1.0", "ucrmVersionCompliancy": { "min": "2.10.0-beta1", "max": null @@ -37,21 +37,52 @@ }, { "key": "qbIncomeAccountNumber", - "label": "Income account number", - "description": "Income Account number from QuickBooks", + "label": "Income account ID", + "description": "Income Account ID from QuickBooks. Enter 0 here if you are using \"Income account name\" below", "required": 1 }, + { + "key": "qbIncomeAccountName", + "label": "Income account name", + "description": "Income Account name from QuickBooks. This will only be used if \"Income account number\" above is 0", + "required": 0 + }, + { + "key": "itemNameFormat", + "label": "Item Name Format String", + "description": "Optional format string to generate QB item names derived from UCRM item type. Can be typically set to something like \"YourWisp %s\" or \"UCRM %s\" to end up with items named \"UCRM service\", \"UCRM product\" etc..", + "required": 0 + }, { "key": "invoicesFromDate", "label": "Date to start for Invoice export", "description": "Date in yyyy-mm-dd format", "required": 0 }, + { + "key": "creditsFromDate", + "label": "Date to start for Credit Memo export", + "description": "Date in yyyy-mm-dd format ", + "required": 0 + }, { "key": "paymentsFromDate", "label": "Date to start for Payment export", "description": "Date in yyyy-mm-dd format ", "required": 0 + }, + { + "key": "paymentTypeWithAccountLink", + "label": "Link payment method to QB account", + "description": "If you'd like to set the \"Deposit to\" field in QB based on payment type, you can set that here. Enter 1 payment type per line like this (without the double quotes): \"Cash=The Cash account\". If you want to enter more than 1 payment type, put the next payment type on the next line", + "type": "textarea", + "required": 0 + }, + { + "key": "logLevel", + "label": "Logging Level", + "description": "Leave empty for default (i.e. regular logging level). To get more details in log, enter \"debug\" (without the double quotes)", + "required": 0 } ] } diff --git a/plugins/quickbooks-online/src/src/Data/PluginData.php b/plugins/quickbooks-online/src/src/Data/PluginData.php old mode 100644 new mode 100755 index 114ec02e7..088187708 --- a/plugins/quickbooks-online/src/src/Data/PluginData.php +++ b/plugins/quickbooks-online/src/src/Data/PluginData.php @@ -28,6 +28,11 @@ class PluginData extends UcrmData */ public $qbIncomeAccountNumber; + /** + * @var string + */ + public $qbIncomeAccountName; + /** * @var string */ @@ -83,11 +88,21 @@ class PluginData extends UcrmData */ public $lastExportedInvoiceID; + /** + * @var string + */ + public $lastExportedCreditID; + /** * @var string */ public $displayedErrors; + /** + * @var string|null + */ + public $itemNameFormat; + /** * @var string|null */ @@ -97,4 +112,19 @@ class PluginData extends UcrmData * @var string|null */ public $paymentsFromDate; + + /** + * @var string|null + */ + public $creditsFromDate; + + /** + * @var string|null + */ + public $paymentTypeWithAccountLink; + + /** + * @var string|null + */ + public $logLevel; } diff --git a/plugins/quickbooks-online/src/src/Facade/QuickBooksFacade.php b/plugins/quickbooks-online/src/src/Facade/QuickBooksFacade.php old mode 100644 new mode 100755 index e47154346..3eb6d3bb3 --- a/plugins/quickbooks-online/src/src/Facade/QuickBooksFacade.php +++ b/plugins/quickbooks-online/src/src/Facade/QuickBooksFacade.php @@ -6,7 +6,9 @@ namespace QBExport\Facade; +use DateTime; use QBExport\Data\InvoiceStatus; +use QBExport\Data\PluginData; use QBExport\Exception\QBAuthorizationException; use QBExport\Factory\DataServiceFactory; use QBExport\Service\Logger; @@ -16,6 +18,7 @@ use QuickBooksOnline\API\Data\IPPIntuitEntity; use QuickBooksOnline\API\DataService\DataService; use QuickBooksOnline\API\Exception\ServiceException; +use QuickBooksOnline\API\Facades\CreditMemo; use QuickBooksOnline\API\Facades\Customer; use QuickBooksOnline\API\Facades\Invoice; use QuickBooksOnline\API\Facades\Item; @@ -44,6 +47,29 @@ class QuickBooksFacade */ private $ucrmApi; + /** + * @var int + */ + private $qbApiTimeoutDelay = 70; + + /** + * @var int + */ + private $qbApiErrorDelay = 5; + + private $itemCache = []; + private $depositToCache = []; + + /** + * @var int + */ + private $queryRunCount = 0; + + /** + * @var DateTime + */ + private $lastCall; + public function __construct( DataServiceFactory $dataServiceFactory, Logger $logger, @@ -91,20 +117,8 @@ public function obtainTokens(): void $this->optionsManager->update(); $this->logger->notice('Exchange Authorization Code for Access Token succeeded.'); - $activeAccounts = $this->getAccounts(); - - $accountsString = ''; - foreach ($activeAccounts as $account) { - $accountsString .= 'Account:' . $account->Name . ' ID: ' . $account->Id . PHP_EOL; - } - - $this->logger->info( - sprintf( - 'Income account numbers in QBO Active accounts:\n %s', - $accountsString - ) - ); - + $dataService = $this->dataServiceFactory->create(DataServiceFactory::TYPE_QUERY); + $this->getAndLogAccounts($dataService); } catch (ServiceException $exception) { $this->invalidateTokens(); } @@ -116,6 +130,10 @@ public function exportClients(): void $dataService = $this->dataServiceFactory->create(DataServiceFactory::TYPE_QUERY); foreach ($this->ucrmApi->query('clients?direction=ASC') as $ucrmClient) { + if (file_exists("data/stopclients") || file_exists("data/stop")) { + $this->logger->info(sprintf('exportClients: stop file detected')); + break; + } if ($ucrmClient['id'] <= $pluginData->lastExportedClientID) { continue; } @@ -124,11 +142,13 @@ public function exportClients(): void continue; } - $entities = $dataService->Query( - sprintf('SELECT * FROM Customer WHERE DisplayName LIKE \'%%UCRMID-%d%%\'', $ucrmClient['id']) + $entities = $this->dataServiceQuery($dataService, + sprintf('SELECT * FROM Customer WHERE DisplayName LIKE \'%%(UCRMID-%d)\'', $ucrmClient['id']) ); - if (! $entities) { + if ($entities) { + $this->logger->info(sprintf('Client ID: %s exists', $ucrmClient['id'])); + } else { $this->logger->info(sprintf('Client ID: %s needs to be exported', $ucrmClient['id'])); if ($ucrmClient['clientType'] === 1) { $nameForView = sprintf( @@ -140,6 +160,11 @@ public function exportClients(): void $nameForView = $ucrmClient['companyName']; } + $email = null; + $emailCheck = $ucrmClient['contacts'][0]['email']; + if (filter_var($emailCheck, FILTER_VALIDATE_EMAIL)) + $email = $emailCheck; + $customerData = [ 'DisplayName' => sprintf( '%s (UCRMID-%d)', @@ -149,6 +174,12 @@ public function exportClients(): void 'PrintOnCheckName' => $nameForView, 'GivenName' => $ucrmClient['firstName'], 'FamilyName' => $ucrmClient['lastName'], + 'PrimaryEmailAddr' => [ + 'Address' => $email + ], + 'Mobile' => [ + 'FreeFormNumber' => $ucrmClient['contacts'][0]['phone'] + ], 'ShipAddr' => [ 'Line1' => $ucrmClient['street1'], 'Line2' => $ucrmClient['street2'], @@ -164,7 +195,7 @@ public function exportClients(): void ]; try { - $response = $dataService->Add(Customer::create($customerData)); + $response = $this->dataServiceAdd($dataService, Customer::create($customerData)); if ($response instanceof IPPIntuitEntity) { $this->logger->info( sprintf('Client %s (ID: %s) exported successfully.', $nameForView, $ucrmClient['id']) @@ -175,10 +206,6 @@ public function exportClients(): void sprintf('Client %s (ID: %s) export failed.', $nameForView, $ucrmClient['id']) ); } - if ($response instanceof \Exception) { - throw $response; - } - $this->handleErrorResponse($dataService); } catch (\Exception $exception) { $this->logger->error( sprintf( @@ -189,10 +216,10 @@ public function exportClients(): void ) ); } - - $pluginData->lastExportedClientID = $ucrmClient['id']; - $this->optionsManager->update(); } + + $pluginData->lastExportedClientID = $ucrmClient['id']; + $this->optionsManager->update(); } } @@ -215,6 +242,8 @@ public function refreshExpiredToken(): void $this->optionsManager->update(); $this->logger->notice('Refresh of Token succeeded.'); } + + $this->logger->notice(sprintf('qbBaseUrl: %s', $pluginData->qbBaseUrl)); } public function exportInvoices(): void @@ -222,43 +251,51 @@ public function exportInvoices(): void $pluginData = $this->optionsManager->load(); $dataService = $this->dataServiceFactory->create(DataServiceFactory::TYPE_QUERY); - $activeAccounts = $this->getAccounts(); - - if (! array_key_exists((int) $pluginData->qbIncomeAccountNumber, $activeAccounts)) { - $accountsString = ''; - foreach ($activeAccounts as $account) { - $accountsString .= 'Account:' . $account->Name . ' ID: ' . $account->Id . PHP_EOL; - } - - $this->logger->info( - sprintf( - 'Income account number (%s) set in the plugin config does not exist in QB or is not active. Active accounts:\n %s', - $pluginData->qbIncomeAccountNumber, - $accountsString - ) - ); - + if ($pluginData->qbIncomeAccountNumber == 0) + $query = sprintf('SELECT * FROM Account WHERE AccountType = \'Income\' AND Name = \'%s\'', $pluginData->qbIncomeAccountName); + else + $query = sprintf('SELECT * FROM Account WHERE AccountType = \'Income\' AND Id = \'%s\'', $pluginData->qbIncomeAccountNumber); + + $account = $this->dataServiceQuery($dataService, $query); + if ($account) { + $qbIncomeAccountId = $account[0]->Id; + $this->logger->debug("Found income account id $qbIncomeAccountId"); + } else { + $this->logger->error("Unable to find Income account (ID {$pluginData->qbIncomeAccountNumber}, Name {$pluginData->qbIncomeAccountName}) set in the plugin config"); return; - } + } $query = 'invoices?direction=ASC'; if ($pluginData->invoicesFromDate) { $query = sprintf('%s&createdDateFrom=%s', $query, $pluginData->invoicesFromDate); } foreach ($this->ucrmApi->query($query) as $ucrmInvoice) { + + if (file_exists("data/stopinvoices") || file_exists("data/stop")) { + $this->logger->info(sprintf('exportInvoices: stop file detected')); + break; + } + if ($ucrmInvoice['id'] <= $pluginData->lastExportedInvoiceID) { continue; } // do not process DRAFT, VOID or PROFORMA invoices - if ( - ($ucrmInvoice['proforma'] ?? false) - || in_array($ucrmInvoice['status'], [InvoiceStatus::DRAFT, InvoiceStatus::VOID], true) + if (($ucrmInvoice['proforma'] ?? false) || + $ucrmInvoice['total'] == 0 || + in_array($ucrmInvoice['status'], [InvoiceStatus::DRAFT, InvoiceStatus::VOID], true) ) { continue; } - $this->logger->info(sprintf('Export of invoice ID %s started.', $ucrmInvoice['id'])); + $this->logger->debug(sprintf('Export of invoice ID %s started.', $ucrmInvoice['id'])); + + $docNumber = "{$ucrmInvoice['number']}/{$ucrmInvoice['id']}"; + $invoice = $this->dataServiceQuery($dataService,"SELECT * FROM INVOICE WHERE DOCNUMBER = '$docNumber'"); + if ($invoice) { + $this->logger->error(sprintf('Invoice %s already in QBO', $docNumber)); + continue; + } $qbClient = $this->getQBClient($dataService, $ucrmInvoice['clientId']); @@ -269,60 +306,22 @@ public function exportInvoices(): void continue; } - $lines = []; - $TaxCode = 'NON'; - foreach ($ucrmInvoice['items'] as $item) { - $qbItem = $this->createQBLineFromItem( - $dataService, - $item, - (int) $pluginData->qbIncomeAccountNumber - ); - if ($qbItem) { - if ($item['tax1Id']) { - $TaxCode = 'TAX'; - } else { - $TaxCode = 'NON'; - } - - $lines[] = [ - 'Amount' => $item['total'], - 'Description' => $item['label'], - 'DetailType' => 'SalesItemLineDetail', - 'SalesItemLineDetail' => [ - 'ItemRef' => [ - 'value' => $qbItem->Id, - ], - 'UnitPrice' => $item['price'], - 'Qty' => $item['quantity'], - 'TaxCodeRef' => [ - 'value' => $TaxCode, - ], - ], - ]; - if ($item['discountTotal'] < 0) { - $lines[] = [ - 'Amount' => $item['discountTotal'] * -1, - 'DiscountLineDetail' => [ - 'PercentBased' => 'false', - ], - 'DetailType' => 'DiscountLineDetail', - 'Description' => 'Discount given', - ]; - } - } - } + $lines = $this->getItems($ucrmInvoice['items'], (int)$qbIncomeAccountId, false, $dataService, $pluginData); try { - $response = $dataService->Add( + $this->logger->info(sprintf('Invoice::create DocNumber=%s DueDate=%s Total=%s', + sprintf('%s/%s', $ucrmInvoice['number'], $ucrmInvoice['id']), $ucrmInvoice['dueDate'], $ucrmInvoice['total'])); + + $response = $this->dataServiceAdd($dataService, Invoice::create( [ - 'DocNumber' => $ucrmInvoice['id'], + 'DocNumber' => sprintf('%s/%s', $ucrmInvoice['number'], $ucrmInvoice['id']), 'DueDate' => $ucrmInvoice['dueDate'], 'TxnDate' => $ucrmInvoice['createdDate'], 'Line' => $lines, 'TxnTaxDetail' => [ 'TotalTax' => $ucrmInvoice['taxes']['totalValue'], - 'TxnTaxCodeRef' => $TaxCode, + 'TxnTaxCodeRef' => 'TAX', ], 'CustomerRef' => [ 'value' => $qbClient->Id, @@ -331,21 +330,11 @@ public function exportInvoices(): void ) ); - if ($response instanceof \Exception) { - throw $response; - } - if ($response instanceof IPPIntuitEntity) { $this->logger->info( sprintf('Invoice ID: %s exported successfully.', $ucrmInvoice['id']) ); - } else { - $this->logger->info( - sprintf('Invoice ID: %s export failed.', $ucrmInvoice['id']) - ); } - - $this->handleErrorResponse($dataService); } catch (\Exception $exception) { $this->logger->error( sprintf( @@ -354,6 +343,10 @@ public function exportInvoices(): void $exception->getMessage() ) ); + + // don't mark as done yet if there was error. If there's more successful exports after this + // then the final saved exportedInvoiceId will still be higher than this one which had error + continue; } $pluginData->lastExportedInvoiceID = $ucrmInvoice['id']; @@ -376,123 +369,251 @@ function (array $a, array $b) { return $a['id'] <=> $b['id']; } ); + + $paymentIdComplete = null; + $paymentMethodQbCache = null; foreach ($ucrmPayments as $ucrmPayment) { - if ($ucrmPayment['id'] <= $pluginData->lastExportedPaymentID || ! $ucrmPayment['clientId']) { - continue; + + if (file_exists("data/stoppayments") || file_exists("data/stop")) { + $this->logger->info(sprintf('exportPayments: stop file detected')); + break; } - $this->logger->info(sprintf('Payment ID: %s needs to be exported', $ucrmPayment['id'])); + $paymentId = $ucrmPayment['id']; + if ($paymentId <= $pluginData->lastExportedPaymentID || ! $ucrmPayment['clientId']) { + continue; + } $qbClient = $this->getQBClient($dataService, $ucrmPayment['clientId']); if (! $qbClient) { $this->logger->error( - sprintf('Client with Display name containing: UCRMID-%s is not found', $ucrmPayment['clientId']) + sprintf("Payment ID $paymentId export failed. Client with Display name containing: UCRMID-{$ucrmPayment['clientId']} is not found") ); continue; } - /* if there is a credit on the payment deal with it first */ - $this->logger->notice(sprintf('Invoice credit amount is : %s', $ucrmPayment['creditAmount'])); - if ($ucrmPayment['creditAmount'] > 0) { - try { - $theResourceObj = Payment::create( - [ - 'CustomerRef' => [ - 'value' => $qbClient->Id, - 'name' => $qbClient->DisplayName, - ], - 'TotalAmt' => $ucrmPayment['creditAmount'], - 'TxnDate' => substr($ucrmPayment['createdDate'], 0, 10), - ] - ); - $response = $dataService->Add($theResourceObj); - if ($response instanceof IPPIntuitEntity) { - $this->logger->info( - sprintf('Payment ID: %s credit exported successfully.', $ucrmPayment['id']) - ); - } - if (! $response) { - $this->logger->info( - sprintf('Payment ID: %s credit export failed.', $ucrmPayment['id']) - ); - } - if ($response instanceof \Exception) { - throw $response; - } - $this->handleErrorResponse($dataService); + $paymentInfoText = "Payment ID $paymentId created {$ucrmPayment['createdDate']},creditAmount={$ucrmPayment['creditAmount']}," . + "total={$ucrmPayment['amount']} for Client Id={$qbClient->Id} DisplayName={$qbClient->DisplayName}"; + $this->logger->debug("Exporting $paymentInfoText"); + + $qbPaymentMethod = $paymentMethodQbCache[$ucrmPayment['methodId']] ?? false; + if (!$qbPaymentMethod) { + $paymentMethod = $this->ucrmApi->query("payment-methods/{$ucrmPayment['methodId']}"); + $qbPaymentMethodResponse = $this->dataServiceQuery($dataService, "SELECT * FROM PaymentMethod WHERE Name = '{$paymentMethod['name']}'"); + if ($qbPaymentMethodResponse) { + $qbPaymentMethod = $qbPaymentMethodResponse[0]; + $paymentMethodQbCache[$ucrmPayment['methodId']] = $qbPaymentMethod; + } + } - } catch (\Exception $exception) { - $this->logger->error( - sprintf( - 'Payment ID: %s export failed with error %s.', - $ucrmPayment['id'], - $exception->getMessage() - ) + [$lineArray, $additionalUnapplied, $totalApplied] = $this->getPaymentCovers($ucrmPayment['paymentCovers'], $dataService); + + try { + $totalUnapplied = $ucrmPayment['creditAmount'] + $additionalUnapplied; + if ($ucrmPayment['creditAmount'] > 0) + $this->logger->debug(sprintf('Non-applied credit amount was: %s, total set as unapplied will be: %s', $ucrmPayment['creditAmount'], $totalUnapplied)); + + $refNumber = $ucrmPayment['checkNumber']; + $note = $ucrmPayment['note']; + if ($refNumber && trim($refNumber) != '') { + $refNumber = "Chk $refNumber"; + if ($note && trim($note) != '') + $refNumber = $refNumber . ", $note"; + } else { + $refNumber = $note; + } + $paymentArray = [ + 'CustomerRef' => [ + 'value' => $qbClient->Id + ], + 'TotalAmt' => $ucrmPayment['amount'], + 'UnappliedAmt' => $totalUnapplied, + 'Line' => $lineArray, + 'TxnDate' => substr($ucrmPayment['createdDate'], 0, 10), + 'PaymentRefNum' => $refNumber, + 'PrivateNote' => "UCRM Payment ID " . $paymentId, + ]; + + if ($qbPaymentMethod) { + $this->logger->debug("Adding payment method; Id {$qbPaymentMethod->Id}, name {$qbPaymentMethod->Name}"); + $paymentArray['PaymentMethodRef'] = [ + 'value' => $qbPaymentMethod->Id + ]; + + $depositToId = $this->getDepositToIdForPayment($qbPaymentMethod->Name, $dataService, $pluginData); + if ($depositToId) + $paymentArray['DepositToAccountRef'] = [ + 'value' => $depositToId + ]; + } + + $paymentObject = Payment::create($paymentArray); + + $response = $this->dataServiceAdd($dataService, $paymentObject); + if ($response instanceof IPPIntuitEntity) { + $this->logger->info( + sprintf('Payment exported successfully. %s', $paymentInfoText) ); } + } catch (\Exception $exception) { + $this->logger->error( + sprintf( + 'Payment ID: %s export failed with error %s. Info: %s', + $paymentId, + $exception->getMessage(), + $paymentInfoText + ) + ); + + continue; } - /* now look and see if part of the payment is applied to existing invoices */ - foreach ($ucrmPayment['paymentCovers'] as $paymentCovers) { + $paymentIdComplete = $paymentId; + } - $LineObj = null; - $lineArray = null; + if ($paymentIdComplete) { + // update last processed payment in the end + $pluginData->lastExportedPaymentID = $paymentIdComplete; + $this->optionsManager->update(); + } + } - $invoices = $dataService->Query( - sprintf('SELECT * FROM INVOICE WHERE DOCNUMBER = \'%d\'', $paymentCovers['invoiceId']) + public function exportCreditNotes(): void + { + $pluginData = $this->optionsManager->load(); + $dataService = $this->dataServiceFactory->create(DataServiceFactory::TYPE_QUERY); + + if ($pluginData->qbIncomeAccountNumber == 0) + $query = sprintf('SELECT * FROM Account WHERE AccountType = \'Income\' AND Name = \'%s\'', $pluginData->qbIncomeAccountName); + else + $query = sprintf('SELECT * FROM Account WHERE AccountType = \'Income\' AND Id = \'%s\'', $pluginData->qbIncomeAccountNumber); + + $account = $this->dataServiceQuery($dataService, $query); + if ($account) { + $qbIncomeAccountId = $account[0]->Id; + $this->logger->debug("Found income account id $qbIncomeAccountId"); + } else { + $this->logger->error("Unable to find Income account (ID {$pluginData->qbIncomeAccountNumber}, Name {$pluginData->qbIncomeAccountName}) set in the plugin config"); + return; + } + + $query = 'credit-notes?direction=ASC'; + if ($pluginData->creditsFromDate) { + $query = sprintf('%s&createdDateFrom=%s', $query, $pluginData->creditsFromDate); + } + + foreach ($this->ucrmApi->query($query) as $ucrmCredit) { + if (file_exists("data/stopcredits") || file_exists("data/stop")) { + $this->logger->info(sprintf('exportCredits: stop file detected')); + break; + } + + if ($ucrmCredit['id'] <= $pluginData->lastExportedCreditID) + continue; + + $this->logger->debug(sprintf('Export of credit memo ID %s started.', $ucrmCredit['id'])); + + $qbClient = $this->getQBClient($dataService, $ucrmCredit['clientId']); + + if (!$qbClient) { + $this->logger->error( + sprintf('Client with Display name containing: UCRMID-%s is not found.', $ucrmCredit['clientId']) ); + continue; + } - $INVOICES = json_decode(json_encode($invoices), true); + $docNumber = sprintf('C%s/%s', $ucrmCredit['number'], $ucrmCredit['id']); + $credit = $this->dataServiceQuery($dataService,"SELECT * FROM CreditMemo WHERE DocNumber = '$docNumber'"); + if ($credit) { + $this->logger->error(sprintf('Credit Memo %s already in QBO', $docNumber)); + continue; + } + + $lines = $this->getItems($ucrmCredit['items'], (int)$qbIncomeAccountId, true, $dataService, $pluginData); - if ($invoices) { - $LineObj = Line::create( + $this->logger->info(sprintf('CreditMemo::create DocNumber=%s DueDate=%s Total=%s', + $docNumber, $ucrmCredit['dueDate'], $ucrmCredit['total'])); + + try { + $response = $this->dataServiceAdd($dataService, + CreditMemo::create( [ - 'Amount' => $ucrmPayment['amount'], - 'LinkedTxn' => [ - 'TxnId' => $INVOICES[0]['Id'], - 'TxnType' => 'Invoice', + 'DocNumber' => $docNumber, + 'TxnDate' => $ucrmCredit['createdDate'], + 'Line' => $lines, + 'TxnTaxDetail' => [ + 'TotalTax' => 0-$ucrmCredit['taxes']['totalValue'], + 'TxnTaxCodeRef' => 'TAX', + ], + 'CustomerRef' => [ + 'value' => $qbClient->Id, ], ] + ) + ); + + if ($response instanceof IPPIntuitEntity) { + $this->logger->info( + sprintf('Credit Memo ID: %s exported successfully.', $ucrmCredit['id']) ); + } + } catch (\Exception $exception) { + $this->logger->error( + sprintf( + 'Credit ID: %s export failed with error %s.', + $ucrmCredit['id'], + $exception->getMessage() + ) + ); - $lineArray[] = $LineObj; + // don't mark as done yet if there was error. If there's more successful exports after this + // then the final saved exportedCreditId will still be higher than this one which had error + continue; + } + + if ($response instanceof IPPIntuitEntity) { + $txnId = $response->Id; + [$lineArray, $additionalUnapplied, $totalApplied] = $this->getPaymentCovers($ucrmCredit['paymentCovers'], $dataService); + if (!$lineArray || $totalApplied == 0 || $additionalUnapplied <> 0) { + if ($lineArray && $totalApplied <> 0) // issue is with additionalUnapplied not being 0. Credit can only be applied if all invoice(s) are found to link to + $this->logger->warning("Will not apply credit to invoices because not all invoices could be found"); + } else { try { - $theResourceObj = Payment::create( + $lineArray[] = Line::create( [ - 'CustomerRef' => [ - 'value' => $qbClient->Id, - 'name' => $qbClient->DisplayName, + 'Amount' => $totalApplied, + 'LinkedTxn' => [ + 'TxnId' => $txnId, + 'TxnType' => 'CreditMemo', ], - 'TotalAmt' => $ucrmPayment['amount'], - 'Line' => $lineArray, - 'TxnDate' => substr($ucrmPayment['createdDate'], 0, 10), ] ); + $paymentArray = [ + 'CustomerRef' => [ + 'value' => $qbClient->Id + ], + 'TotalAmt' => 0.0, + 'Line' => $lineArray, + 'TxnDate' => substr($ucrmCredit['createdDate'], 0, 10), + 'PaymentRefNum' => 'Auto-apply credit', + 'PrivateNote' => 'Created by UCRM to link credits to charges', + ]; + + $paymentObject = Payment::create($paymentArray); - $response = $dataService->Add($theResourceObj); + $response = $this->dataServiceAdd($dataService, $paymentObject); if ($response instanceof IPPIntuitEntity) { $this->logger->info( - sprintf('Payment ID: %s exported successfully.', $ucrmPayment['id']) + sprintf('Payment to apply credit exported successfully. Total credit was: %s, Credit applied: %s', $ucrmCredit['total'], $totalApplied) ); } - if (! $response) { - $this->logger->info( - sprintf('Payment ID: %s export failed.', $ucrmPayment['id']) - ); - } - if ($response instanceof \Exception) { - throw $response; - } - - $this->handleErrorResponse($dataService); } catch (\Exception $exception) { $this->logger->error( sprintf( - 'Payment ID: %s export failed with error %s.', - $ucrmPayment['id'], + 'Payment to apply credit export failed with error %s', $exception->getMessage() ) ); @@ -500,19 +621,223 @@ function (array $a, array $b) { } } - // update last processed payment in the end - $pluginData->lastExportedPaymentID = $ucrmPayment['id']; + $pluginData->lastExportedCreditID = $ucrmCredit['id']; $this->optionsManager->update(); + } // end foreach $ucrmCredit + } + + private function getPaymentCovers($payments, DataService $dataService): array { + $lineArray = null; + $additionalUnapplied = 0.0; + $totalApplied = 0.0; + foreach ($payments as $paymentCovers) { + if ($paymentCovers['amount'] == 0) continue; + + if ($paymentCovers['refundId']) { + $this->logger->notice('Payment has refundId, not yet supported! Will set as unapplied'); + $additionalUnapplied += $paymentCovers['amount']; + continue; + } + if ($paymentCovers['invoiceId'] == '') { + $this->logger->notice('Payment has empty invoiceId! Will set as unapplied'); + $additionalUnapplied += $paymentCovers['amount']; + continue; + } + + $invId = $paymentCovers['invoiceId']; + $invoices = $this->dataServiceQuery($dataService,"SELECT * FROM INVOICE WHERE DOCNUMBER LIKE '%/$invId'"); + if (!$invoices) + $invoices = $this->dataServiceQuery($dataService,"SELECT * FROM INVOICE WHERE DOCNUMBER = '$invId'"); + + if (!$invoices) { + $this->logger->warning(sprintf('Unable to find invoiceId %s covered by payment, will set as unapplied', $paymentCovers['invoiceId'])); + $additionalUnapplied += $paymentCovers['amount']; + continue; + } + + $lineArray[] = Line::create( + [ + 'Amount' => $paymentCovers['amount'], + 'LinkedTxn' => [ + 'TxnId' => $invoices[0]->Id, + 'TxnType' => 'Invoice', + ], + ] + ); + + $totalApplied += $paymentCovers['amount']; + $this->logger->debug("Payment applying \${$paymentCovers['amount']} to Invoice {$invoices[0]->DocNumber}"); } + + return [$lineArray, $additionalUnapplied, $totalApplied]; + } + + private function getItems($items, int $qbIncomeAccountId, bool $negateQty, DataService $dataService, PluginData $pluginData): array { + $lines = []; + foreach ($items as $item) { + $qbItem = $this->itemCache[$item['label']] ?? false; + if (! $qbItem) { + $qbItem = $this->createQBLineFromItem( + $dataService, + $item, + $qbIncomeAccountId, + $pluginData->itemNameFormat + ); + if (! $qbItem) { + $this->logger->error("Could not get item \"{$item['label']}\" from QBO"); + continue; + } + + $this->itemCache[$item['label']] = $qbItem; + } + + if ($item['tax1Id']) { + $TaxCode = 'TAX'; + } else { + $TaxCode = 'NON'; + } + + $this->logger->debug(sprintf('Description=%s TaxCode=%s', $item['label'], $TaxCode)); + + $lines[] = [ + 'Amount' => $this->negateAmount($item['total'], $negateQty), + 'Description' => $item['label'], + 'DetailType' => 'SalesItemLineDetail', + 'SalesItemLineDetail' => [ + 'ItemRef' => [ + 'value' => $qbItem->Id, + ], + 'UnitPrice' => $this->negateAmount($item['price'], $negateQty), + 'Qty' => $this->negateAmount($item['quantity'], $negateQty), + 'TaxCodeRef' => [ + 'value' => $TaxCode, + ], + ], + ]; + if ($item['discountTotal'] < 0) { + $lines[] = [ + 'Amount' => 0-$item['discountTotal'], + 'DiscountLineDetail' => [ + 'PercentBased' => 'false', + ], + 'DetailType' => 'DiscountLineDetail', + 'Description' => 'Discount given', + ]; + } + } + + return $lines; + } + + private function negateAmount($amount, $doNegate) { + if ($doNegate) + return 0-$amount; + else + return $amount; + } + + /** + * @throws QBAuthorizationException + * @throws \Exception + */ + private function dataServiceQuery(DataService $dataService, string $query, bool $throwForErrors = false): ?array { + $tryCount = 0; + $tryNumber = 3; + $output = null; + do { + try { + $this->pauseIfNeeded(); + $output = $dataService->Query($query); + break; + } catch (\Exception $e) { + $tryCount++; + if ($tryCount < $tryNumber) { + $this->logger->info("Waiting {$this->qbApiErrorDelay} seconds to retry after issue getting data from QBO"); + sleep($this->qbApiErrorDelay); + } + elseif ($throwForErrors) + throw $e; + } + } while ($tryCount < $tryNumber); + + if ($throwForErrors) { + if ($output instanceof \Exception) + throw $output; + $this->handleErrorResponse($dataService); + } + + return $output; + } + + /** + * @throws QBAuthorizationException + * @throws \QuickBooksOnline\API\Exception\IdsException + * @throws \Exception + */ + private function dataServiceAdd(DataService $dataService, IPPIntuitEntity $entity, bool $throwForErrors = true) { + $tryCount = 0; + $tryNumber = 3; + $output = null; + do { + try { + $this->pauseIfNeeded(); + $output = $dataService->Add($entity); + break; + } catch (\Exception $e) { + $tryCount++; + if ($tryCount < $tryNumber) { + $this->logger->info("Waiting {$this->qbApiErrorDelay} seconds to retry after issue getting data from QBO"); + sleep($this->qbApiErrorDelay); + } + elseif ($throwForErrors) + throw $e; + } + } while ($tryCount < $tryNumber); + + if ($throwForErrors) { + if ($output instanceof \Exception) + throw $output; + $this->handleErrorResponse($dataService); + } + + return $output; + } + + /** + * This function is called before most QB api queries because QB has some limits in how many calls can be + * made per minute. See this link. + */ + private function pauseIfNeeded() { + if ($this->lastCall === NULL) + $this->lastCall = date_create(); + + $callsBeforeWait = 450; + if ($this->lastCall->getTimestamp() < (date_create()->getTimestamp() - 60)) + // reset query run count because qb limit is per minute and there has been no call for more than a minute + $this->queryRunCount = 0; + elseif ($this->queryRunCount >= $callsBeforeWait) { + $this->queryRunCount = 0; + $this->logger->notice("Now waiting {$this->qbApiTimeoutDelay} to run next QB api call because there were at least $callsBeforeWait calls in the last minute"); + sleep($this->qbApiTimeoutDelay); + } + + $this->queryRunCount++; + $this->lastCall = date_create(); } private function getQBClient(DataService $dataService, int $ucrmClientId) { - $customers = $dataService->Query( - sprintf('SELECT * FROM Customer WHERE DisplayName LIKE \'%%UCRMID-%d)\'', $ucrmClientId) - ); + $customers = null; + try { + $customers = $this->dataServiceQuery($dataService, + sprintf('SELECT * FROM Customer WHERE DisplayName LIKE \'%%(UCRMID-%d)\'', $ucrmClientId), + false, true + ); + } catch (\Exception $e) { + $this->logger->error("Could not get customer from QBO; id $ucrmClientId; error {$e->getMessage()}"); + } - if (! $customers) { + if (!$customers) { return null; } @@ -522,13 +847,34 @@ private function getQBClient(DataService $dataService, int $ucrmClientId) private function createQBLineFromItem( DataService $dataService, array $item, - int $qbIncomeAccountNumber + int $qbIncomeAccountNumber, + ?string $itemNameFormat ): ?IPPIntuitEntity { + + if($itemNameFormat) { + $itemName=sprintf($itemNameFormat, $item['type']); + } else { + $itemName=$item['type']; + } + + $response = null; + try { + $this->logger->debug("Get item \"$itemName\" from QBO"); + $response = $this->dataServiceQuery($dataService,"SELECT * FROM Item WHERE Name = '$itemName'", false, true); + } catch (\Exception $e) { + $this->logger->error("Trying to get item $itemName from QBO: {$e->getMessage()}"); + } + + if($response) { + return reset($response); + } + try { - $response = $dataService->Add( + $this->logger->info("Adding new item into QBO: \"$itemName\""); + return $this->dataServiceAdd($dataService, Item::create( [ - 'Name' => sprintf('%s (UCRMID-%s)', $item['label'], $item['id']), + 'Name' => $itemName, 'Type' => 'Service', 'IncomeAccountRef' => [ 'value' => $qbIncomeAccountNumber, @@ -536,13 +882,9 @@ private function createQBLineFromItem( ] ) ); - - $this->handleErrorResponse($dataService); - - return $response; } catch (\Exception $exception) { $this->logger->error( - sprintf('Item ID: %s export failed with error %s.', $item['id'], $exception->getMessage()) + sprintf('Item: %s create failed with error %s.', $itemName, $exception->getMessage()) ); } @@ -587,32 +929,75 @@ private function handleErrorResponse(DataService $dataService): void } } - private function getAccounts(): array + private function getAndLogAccounts(DataService $dataService): void { - $activeAccounts = []; - try { - $dataService = $this->dataServiceFactory->create(DataServiceFactory::TYPE_QUERY); + $response = $this->dataServiceQuery($dataService,"SELECT * FROM Account WHERE Active = true", false, true); - $response = $dataService->FindAll('Account'); + $accountsString = ''; + foreach ($response as $account) + $accountsString .= 'Account:' . $account->Name . ' ID: ' . $account->Id . PHP_EOL; - $this->handleErrorResponse($dataService); + $this->logger->info("Income account numbers in QBO Active accounts:\n$accountsString"); + } catch (\Exception $exception) { + $this->logger->error( + sprintf('Account: Getting all Accounts failed with error %s.', $exception->getMessage()) + ); + } + } - foreach ($response as $account) { - if (! $account->Active) { - continue; + private function getDepositToIdForPayment(string $paymentMethodName, DataService $dataService, PluginData $pluginData): ?string + { + $links = $pluginData->paymentTypeWithAccountLink; + if (!$links) return null; + $this->logger->debug("Checking DepositTo user saved info: $links"); + $lines = preg_split("(\r\n|\n|\r)", $links); + if (!$lines) return null; + + foreach ($lines as $line) { + if (trim($line) == '') continue; + $this->logger->debug("Checking DepositTo line: $line"); + $methodAcct = explode("=", $line, 2); + if (!$methodAcct) continue; + + if (count($methodAcct) != 2) { + $this->logger->warning("Line for link payment method does not have 2 parts (separated by = sign): $line"); + continue; + } + $this->logger->debug("Checking DepositTo line: [0]={$methodAcct[0]} [1]={$methodAcct[1]}"); + + $payType = trim($methodAcct[0]); + if ($payType != $paymentMethodName) continue; + + $cachedId = $this->depositToCache[$payType] ?? false; + if ($cachedId) + return $cachedId; + + $depositAcct = trim($methodAcct[1]); + $accounts = $this->dataServiceQuery($dataService, "SELECT * FROM Account WHERE Name = '$depositAcct'"); + if ($accounts) { + $useAccount = null; + foreach ($accounts as $account) { + if ($account->AccountType == 'Bank' || $account->AccountType == 'Other Current Asset') { + $useAccount = $account; + break; + } } - $activeAccounts[$account->Id] = $account; + if ($useAccount) { + $id = $useAccount->Id; + $this->depositToCache[$payType] = $id; + $this->logger->debug("Found account for payment \"deposit to\" with Id $id"); + return $id; + } } - } catch (\Exception $exception) { - $this->logger->error( - sprintf('Account: Getting all Accounts failed with error %s.', $exception->getMessage()) - ); + $this->logger->warning("Payment \"deposit to\" account not found for \"$depositAcct\""); + + break; } - return $activeAccounts; + return null; } /** diff --git a/plugins/quickbooks-online/src/src/Plugin.php b/plugins/quickbooks-online/src/src/Plugin.php index 65e6a704f..8fb82b166 100644 --- a/plugins/quickbooks-online/src/src/Plugin.php +++ b/plugins/quickbooks-online/src/src/Plugin.php @@ -70,6 +70,8 @@ private function processCli(): void $this->quickBooksFacade->exportClients(); $this->quickBooksFacade->exportInvoices(); $this->quickBooksFacade->exportPayments(); + $this->quickBooksFacade->exportCreditNotes(); + $this->cleanLog(); } $this->logger->info('CLI process ended'); } catch (QBAuthorizationException $exception) { @@ -93,4 +95,36 @@ private function processHttpRequest(): void echo 'Authorization Code obtained.'; } } + + private function cleanLog(): void { + $dir = Logger::logFileDirectory; + $file = Logger::logFileName; + $ext = Logger::logFileExtension; + $logPath = "$dir/$file.$ext"; + + $mb = 1000000; + $size = filesize($logPath); + // never trim file if it's less than 1Mb + if ($size < $mb) return; + + $mbSize = $size / $mb; + $this->logger->info("Cleaning up log, size is $mbSize MB"); + $this->trimLogToLength($logPath, 10000); + } + + /** + * Idea from [here](https://stackoverflow.com/a/45090213), but modified + */ + function trimLogToLength($path, $numRowsToKeep) { + $file = file($path); + if (!$file) return; + + // if this file is long enough that we should be truncating it + $countFile = count($file); + if ($countFile > $numRowsToKeep) { + // figure out the rows we want to keep + $dataRowsToKeep = array_slice($file,$countFile-$numRowsToKeep, $numRowsToKeep); + file_put_contents($path, implode($dataRowsToKeep)); + } + } } diff --git a/plugins/quickbooks-online/src/src/Service/Logger.php b/plugins/quickbooks-online/src/src/Service/Logger.php index 98db8ac7c..7a1be1c61 100644 --- a/plugins/quickbooks-online/src/src/Service/Logger.php +++ b/plugins/quickbooks-online/src/src/Service/Logger.php @@ -9,15 +9,41 @@ class Logger extends \Katzgrau\KLogger\Logger { - public function __construct() + const logFileDirectory = "data"; + const logFileName = "plugin"; + const logFileExtension = "log"; + public function __construct(?OptionsManager $optionsManager) { + $logLevel = null; + if ($optionsManager) { + $pluginData = $optionsManager->load(); + $logLevel = Logger::checkLogLevel($pluginData->logLevel); + } parent::__construct( - 'data', - LogLevel::DEBUG, + self::logFileDirectory, + $logLevel ?? LogLevel::INFO, [ - 'extension' => 'log', - 'filename' => 'plugin', + 'extension' => self::logFileExtension, + 'filename' => self::logFileName, ] ); } + + private function checkLogLevel(?string $level): ?string { + if (!$level || ($trimmed = trim($level)) == '') return null; + + switch ($trimmed) { + case LogLevel::EMERGENCY: + case LogLevel::ALERT: + case LogLevel::CRITICAL: + case LogLevel::ERROR: + case LogLevel::WARNING: + case LogLevel::NOTICE: + case LogLevel::INFO: + case LogLevel::DEBUG: + return $trimmed; + } + + return null; + } }