diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..acde986 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,19 @@ +.git +/.env.local +**/.env.* +**/.app_env* +**/*.log +**/._* +**/.DS_Store +**/.gitignore +**/.gitattributes +**/Thumbs.db +**/*.md +**/.dockerignore +Dockerfile* +docs/ +node_modules/ +public/build/ +public/bundles/ +var/ +vendor/ diff --git a/.env b/.env new file mode 100644 index 0000000..4c446a2 --- /dev/null +++ b/.env @@ -0,0 +1,44 @@ +# In all environments, the following files are loaded if they exist, +# the latter taking precedence over the former: +# +# * .env contains default values for the environment variables needed by the app +# * .env.local uncommitted file with local overrides +# * .env.$APP_ENV committed environment-specific defaults +# * .env.$APP_ENV.local uncommitted environment-specific overrides +# +# Real environment variables win over .env files. +# +# DO NOT DEFINE PRODUCTION SECRETS IN THIS FILE NOR IN ANY OTHER COMMITTED FILES. +# +# Run "composer dump-env prod" to compile .env files for production use (requires symfony/flex >=1.2). +# https://symfony.com/doc/current/best_practices.html#use-environment-variables-for-infrastructure-configuration + +###> symfony/framework-bundle ### +APP_ENV=dev +APP_SECRET=3247a721904471e968b39c1652eff071 +#TRUSTED_PROXIES=127.0.0.1,127.0.0.2 +#TRUSTED_HOSTS='^localhost|example\.com$' +###< symfony/framework-bundle ### + +###> doctrine/doctrine-bundle ### +# Format described at https://www.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html#connecting-using-a-url +# For an SQLite database, use: "sqlite:///%kernel.project_dir%/var/data.db" +# For a PostgreSQL database, use: "postgresql://db_user:db_password@127.0.0.1:5432/db_name?serverVersion=11&charset=utf8" +# IMPORTANT: You MUST configure your server version, either here or in config/packages/doctrine.yaml +DATABASE_URL=mysql://dev:dev@127.0.0.1:3316/qb_api?serverVersion=mariadb-10.2.22 +###< doctrine/doctrine-bundle ### + +###> symfony/mailer ### +MAILER_DSN=smtp://localhost +###< symfony/mailer ### + +###> symfony/amazon-mailer ### +# MAILER_DSN=ses://ACCESS_KEY:SECRET_KEY@default?region=eu-west-1 +# MAILER_DSN=ses+smtp://ACCESS_KEY:SECRET_KEY@default?region=eu-west-1 +###< symfony/amazon-mailer ### + +DOMAIN=app.easyquickimport.com +WEBDAV_URL= +WEBDAV_USERNAME= +WEBDAV_PASSWORD= +WEBDAV_PATH= diff --git a/.env.test b/.env.test new file mode 100644 index 0000000..de52917 --- /dev/null +++ b/.env.test @@ -0,0 +1,5 @@ +# define your env variables for the test env here +KERNEL_CLASS='App\Kernel' +APP_SECRET='s$cretf0rt3st' +SYMFONY_DEPRECATIONS_HELPER=999999 +PANTHER_APP_ENV=panther diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2ba9b6e --- /dev/null +++ b/.gitignore @@ -0,0 +1,16 @@ +###> symfony/framework-bundle ### +/.env.local +/.env.local.php +/.env.*.local +/.idea +/config/secrets/prod/prod.decrypt.private.php +/public/bundles/ +/var/ +/vendor/ +###< symfony/framework-bundle ### + +###> symfony/phpunit-bridge ### +.phpunit +.phpunit.result.cache +/phpunit.xml +###< symfony/phpunit-bridge ### diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..2041c56 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,80 @@ +#ENV variables: https://docs.gitlab.com/ce/ci/variables/ +image: karser/docker-compose-ci + +stages: + - test + - build + - deploy + +before_script: + - docker -v && docker-compose -v + +test dev: + stage: test + only: + refs: + - branches + variables: + COMPOSE_PROJECT_NAME: "easyquickimport_test${CI_JOB_ID}" + DOCKER_ENV: test + APP_ENV: test + script: + - cd docker + - bin/copy-env.sh + - bin/build.sh + - docker-compose run --rm php sh -c "bin/run-tests.sh" + after_script: + - cd docker + - docker-compose down --remove-orphans + artifacts: + expire_in: 1 week + when: always + paths: + - var/log + +build image: + stage: build + variables: + DOCKER_ENV: prod + APP_ENV: prod + script: + - cd docker + - bin/copy-env.sh + - bin/build.sh + - bin/push.sh + +deploy prod: + stage: deploy + environment: + name: easyquickimport-prod + url: https://app.easyquickimport.com/ + only: + - tags + - branches + when: manual + variables: + DOCKER_ENV: prod + ENVIRONMENT: prod + APP_ENV: prod + COMPOSE_PROJECT_NAME: easyquickimport + VIRTUAL_HOST: app.easyquickimport.com + DATABASE_HOST: ${HZ4_DB_HOST} + MAILER_DSN: ${PROD_MAILER_DSN} + DATABASE_URL: ${PROD_DATABASE_URL} + # connect to the server + DOCKER_TLS_VERIFY: "1" + DOCKER_HOST: $HZ4_DOCKER_HOST + DOCKER_CERT_PATH: "/tmp/certs" + before_script: + - mkdir -p $DOCKER_CERT_PATH + - echo "$HZ4_CA" > $DOCKER_CERT_PATH/ca.pem + - echo "$HZ4_CLIENT_CERT" > $DOCKER_CERT_PATH/cert.pem + - echo "$HZ4_CLIENT_KEY" > $DOCKER_CERT_PATH/key.pem + - ls -alh $DOCKER_CERT_PATH + - echo ${CI_REGISTRY_PASSWORD} | docker login ${CI_REGISTRY} -u ${CI_REGISTRY_USER} --password-stdin + script: + - cd docker + - bin/copy-env.sh + - docker-compose config | docker stack deploy --with-registry-auth -c - ${COMPOSE_PROJECT_NAME} + after_script: + - rm -rf $DOCKER_CERT_PATH diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..8011fe8 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,152 @@ +ARG PHP_VERSION=7.4 +ARG NGINX_VERSION=1.18.0 + + +# "php base" stage +FROM php:${PHP_VERSION}-fpm-alpine AS app_php_base + +# persistent / runtime deps +RUN apk add --no-cache \ + acl \ + bash \ + fcgi \ + file \ + gettext \ + git \ + freetype \ + libjpeg-turbo \ + libpng \ + nano \ + ; + +ARG APCU_VERSION=5.1.18 +RUN set -eux; \ + apk add --no-cache --virtual .build-deps \ + $PHPIZE_DEPS \ + icu-dev \ + freetype-dev \ + libjpeg-turbo-dev \ + libpng-dev \ + libzip-dev \ + postgresql-dev \ + zlib-dev \ + ; \ + \ + docker-php-ext-configure zip; \ + docker-php-ext-configure gd \ + --with-freetype=/usr/include/ \ + --with-jpeg=/usr/include/ \ + ; \ + docker-php-ext-install -j$(nproc) \ + gd \ + intl \ + mysqli \ + pdo \ + pdo_mysql \ + zip \ + ; \ + pecl install \ + apcu-${APCU_VERSION} \ + ; \ + pecl clear-cache; \ + docker-php-ext-enable \ + gd \ + apcu \ +# opcache \ + mysqli \ + pdo_mysql \ + ; \ + \ + runDeps="$( \ + scanelf --needed --nobanner --format '%n#p' --recursive /usr/local/lib/php/extensions \ + | tr ',' '\n' \ + | sort -u \ + | awk 'system("[ -e /usr/local/lib/" $1 " ]") == 0 { next } { print "so:" $1 }' \ + )"; \ + apk add --no-cache --virtual .api-phpexts-rundeps $runDeps; \ + \ + apk del .build-deps + +COPY --from=composer:1.10 /usr/bin/composer /usr/bin/composer + +RUN ln -s $PHP_INI_DIR/php.ini-production $PHP_INI_DIR/php.ini +COPY docker/php/conf.d/app.prod.ini $PHP_INI_DIR/conf.d/app.ini + +RUN set -eux; \ + { \ + echo '[www]'; \ + echo 'ping.path = /ping'; \ + echo 'clear_env = no'; \ + } | tee /usr/local/etc/php-fpm.d/docker-config.conf + +# https://getcomposer.org/doc/03-cli.md#composer-allow-superuser +ENV COMPOSER_ALLOW_SUPERUSER=1 +# install Symfony Flex globally to speed up download of Composer packages (parallelized prefetching) +RUN set -eux; \ + composer global require "symfony/flex" --prefer-dist --no-progress --no-suggest --classmap-authoritative; \ + composer clear-cache +ENV PATH="${PATH}:/root/.composer/vendor/bin" + +WORKDIR /var/www/app + +# "php prod" stage +FROM app_php_base AS app_php + +# prevent the reinstallation of vendors at every changes in the source code +COPY composer.json composer.lock symfony.lock ./ +RUN set -eux; \ + composer install --prefer-dist --no-dev --no-scripts --no-progress --no-suggest; \ + composer clear-cache + +# do not use .env files in production +COPY .env ./ +RUN composer dump-env prod; \ + rm .env + +# copy only specifically what we need +COPY bin bin/ +COPY config config/ +COPY public public/ +COPY src src/ +COPY templates templates/ +COPY translations translations/ + +RUN set -eux; \ + mkdir -p var/cache var/log; \ + composer dump-autoload --classmap-authoritative --no-dev; \ + composer run-script --no-dev post-install-cmd; \ + chmod +x bin/console; sync +VOLUME /var/www/app/var + +COPY docker/php/docker-healthcheck.sh /usr/local/bin/docker-healthcheck +RUN chmod +x /usr/local/bin/docker-healthcheck + +HEALTHCHECK --interval=10s --timeout=3s --retries=3 CMD ["docker-healthcheck"] + +COPY docker/php/docker-entrypoint.sh /usr/local/bin/docker-entrypoint +RUN chmod +x /usr/local/bin/docker-entrypoint + +ENTRYPOINT ["docker-entrypoint"] +CMD ["php-fpm"] + + +# "nginx" stage +# depends on the "php" stage above +FROM nginx:${NGINX_VERSION}-alpine AS app_nginx + +ADD ./docker/nginx/nginx.conf /etc/nginx/ +COPY docker/nginx/conf.d/default.conf /etc/nginx/conf.d/default.conf + +WORKDIR /var/www/app/public + +COPY --from=app_php /var/www/app/public ./ + +ARG PUID=1000 +ARG PGID=1000 + +RUN if [[ -z $(getent group ${PGID}) ]] ; then \ + addgroup -g ${PGID} www-data; \ + else \ + addgroup www-data; \ + fi; \ + adduser -D -u ${PUID} -G www-data www-data diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a4be167 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Dmitrii Poddubnyi + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..89b87bb --- /dev/null +++ b/README.md @@ -0,0 +1,99 @@ +# EasyQuickImport — Import transactions, invoices and bills into QuickBooks Desktop from Excel or CSV + +[![Build Status](https://gitlab.dev.trackmage.com/karser/easyquickimport/badges/master/pipeline.svg)](https://gitlab.dev.trackmage.com/karser/easyquickimport/pipelines) +[![Total Downloads](https://poser.pugx.org/karser/easy-quick-import/downloads)](https://packagist.org/packages/karser/easy-quick-import) + +EasyQuickImport is a tool that helps you import invoices, bills, transactions, +customers and vendors into QuickBooks Desktop in multiple currencies in bulk. + +### Features +- **Invoices, bills, transactions (General Journal entries), customers, and vendors import in csv, xlsx, xls formats.** +- **Multicurrency support with NON-USD base currency**. The currency of your accounts is automatically detected + from the imported Chart of accounts. +- **Cross Currency Transactions**. Transfer between accounts of different currencies goes through the Undeposited + funds account. The Undeposited funds balance remains zero because EasyQuickImport uses the accurate historical exchange rate. +- **Historical exchange rate**. EasyQuickImport automatically obtains the exchange rate from European Central Bank + on a given date for any currency for each transaction. You can use [other exchange rate sources](https://github.com/florianv/exchanger) as well. +- **Multi-tenancy**: if you have multiple company files on the same computer, you can add them all to EasyQuickImport. + + +## Getting started: + +Either install [self-hosted](#how-to-install-easyquickimport) or [sign up](https://app.easyquickimport.com/register) for a free cloud account. + +### Connect EasyQuickImport to QuickBooks Desktop + +**In EasyQuickImport**: + +Add a company file in Users, define username, password and specify the home currency. +It's recommended to specify the company file location if you are going to use multiple company files on the same computer. +Once it's done download the QWC file. + +**In Quickbooks** click `File / Update Web Services` + +Then Add an Application, in the file dialog select the downloaded QWC. +Then click Yes, then select "When company file is open" and click continue. +When it's done don't forget to specify the password that you defined in EasyQuickImport. + +[![Connect EasyQuickImport to QuickBooks Desktop](https://user-images.githubusercontent.com/1675033/117167904-5a9d6780-add0-11eb-901d-8228443be18c.png)](https://www.youtube.com/watch?v=6kVJrthCQr0) + +### How to import invoices from Excel into QuickBooks Desktop + +[![Import invoices from Excel into QuickBooks Desktop](https://user-images.githubusercontent.com/1675033/117167991-7274eb80-add0-11eb-99ef-e6f27e72e509.png)](https://www.youtube.com/watch?v=ZKe002JUIww) + +### How to import transactions from Excel into QuickBooks Desktop + +[![How to import transactions from Excel into QuickBooks Desktop](https://user-images.githubusercontent.com/1675033/117168077-83bdf800-add0-11eb-8ad9-9d8668164752.png)](https://www.youtube.com/watch?v=-hmhxs72W1E) + +### How to import bills and vendors from Excel into QuickBooks Desktop + +[![How to import bills and vendors from Excel into QuickBooks Desktop](https://user-images.githubusercontent.com/1675033/117168137-90dae700-add0-11eb-826d-b09c1cbd1b71.png)](https://www.youtube.com/watch?v=vcSeREomzuE) + +### How to import multicurrency transactions from Excel into QuickBooks Desktop + +[![How to import multicurrency transactions from Excel into QuickBooks Desktop](https://user-images.githubusercontent.com/1675033/117168217-a223f380-add0-11eb-8033-def86fc9c824.png)](https://www.youtube.com/watch?v=NvMpb3wVIXc) + + +## How to install EasyQuickImport + +1. Clone the repo +``` +git clone https://github.com/karser/EasyQuickImport.git +``` +2. Install packages with composer +``` +composer install +``` +3. Copy `.env` to `.env.local` and configure DATABASE_URL and DOMAIN +4. Recreate the database +``` +bin/console doctrine:schema:drop --full-database --force \ +&& bin/console doctrine:migrations:migrate -n +#fixtures +bin/console doctrine:fixtures:load +``` +5. Create the user +``` +bin/console app:create-user user@example.com --password pass123 +``` +6. Run the server +``` +cd ./public +php -S 127.0.0.1:9090 +``` + +### Tests +``` +bin/phpunit +``` + +### phpstan +``` +vendor/bin/phpstan analyse -c phpstan.neon +vendor/bin/phpstan analyse -c phpstan-tests.neon +``` + +### Lookup historical currency rate +``` +bin/console app:currency:get USD --date 2020-03-05 --base HKD +``` diff --git a/bin/console b/bin/console new file mode 100755 index 0000000..5de0e1c --- /dev/null +++ b/bin/console @@ -0,0 +1,42 @@ +#!/usr/bin/env php +getParameterOption(['--env', '-e'], null, true)) { + putenv('APP_ENV='.$_SERVER['APP_ENV'] = $_ENV['APP_ENV'] = $env); +} + +if ($input->hasParameterOption('--no-debug', true)) { + putenv('APP_DEBUG='.$_SERVER['APP_DEBUG'] = $_ENV['APP_DEBUG'] = '0'); +} + +require dirname(__DIR__).'/config/bootstrap.php'; + +if ($_SERVER['APP_DEBUG']) { + umask(0000); + + if (class_exists(Debug::class)) { + Debug::enable(); + } +} + +$kernel = new Kernel($_SERVER['APP_ENV'], (bool) $_SERVER['APP_DEBUG']); +$application = new Application($kernel); +$application->run($input); diff --git a/bin/phpunit b/bin/phpunit new file mode 100755 index 0000000..4d1ed05 --- /dev/null +++ b/bin/phpunit @@ -0,0 +1,13 @@ +#!/usr/bin/env php + ./public/build/manifest.json +fi + +composer install --dev --prefer-dist --no-interaction --no-scripts --no-progress --no-suggest + +echo "Waiting for db to be ready..." +until bin/console doctrine:query:sql "SELECT 1" > /dev/null 2>&1; do + sleep 1 +done +bin/console doctrine:migrations:migrate --no-interaction + +vendor/bin/simple-phpunit -c ./phpunit.xml.dist diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..760cfe3 --- /dev/null +++ b/composer.json @@ -0,0 +1,112 @@ +{ + "name": "karser/easy-quick-import", + "type": "project", + "description": "Import transactions, invoices and bills into QuickBooks Desktop from Excel or CSV", + "keywords": ["quickbooks", "quickbooks-desktop", "quickbooks-web-connector", "importer"], + "homepage": "https://github.com/karser/EasyQuickImport", + "license": "MIT", + "require": { + "php": "^7.4", + "ext-ctype": "*", + "ext-gd": "*", + "ext-iconv": "*", + "ext-intl": "*", + "ext-sqlite3": "*", + "ext-json": "*", + "consolibyte/quickbooks": "dev-qbxmlops130", + "craue/formflow-bundle": "^3.3", + "doctrine/doctrine-bundle": "^1.11", + "doctrine/doctrine-migrations-bundle": "^2.0", + "doctrine/orm": "^2.6", + "easycorp/easyadmin-bundle": "^2.2", + "florianv/exchanger": "^2.7", + "league/flysystem-webdav": "^1.0.7", + "nyholm/psr7": "^1.1", + "oneup/flysystem-bundle": "^3.1", + "php-http/guzzle6-adapter": "^2.0", + "php-http/message": "^1.7", + "phpoffice/phpspreadsheet": "^1.7", + "ramsey/uuid": "^4.0", + "symfony/amazon-mailer": "*", + "symfony/console": "*", + "symfony/dotenv": "*", + "symfony/flex": "^1.1", + "symfony/form": "*", + "symfony/framework-bundle": "*", + "symfony/http-client": "*", + "symfony/mailer": "*", + "symfony/monolog-bundle": "^3.5", + "symfony/security": "*", + "symfony/security-bundle": "*", + "symfony/serializer-pack": "^1.0", + "symfony/translation": "*", + "symfony/twig-bundle": "*", + "symfony/validator": "*", + "symfony/yaml": "*", + "symfonycasts/reset-password-bundle": "^1.1" + }, + "config": { + "preferred-install": { + "*": "dist" + }, + "sort-packages": true + }, + "autoload": { + "psr-4": { + "App\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "App\\Tests\\": "tests/" + } + }, + "replace": { + "paragonie/random_compat": "2.*", + "symfony/polyfill-ctype": "*", + "symfony/polyfill-iconv": "*", + "symfony/polyfill-php71": "*", + "symfony/polyfill-php70": "*", + "symfony/polyfill-php56": "*" + }, + "scripts": { + "auto-scripts": { + "cache:clear": "symfony-cmd", + "assets:install %PUBLIC_DIR%": "symfony-cmd" + }, + "post-install-cmd": [ + "@auto-scripts" + ], + "post-update-cmd": [ + "@auto-scripts" + ] + }, + "repositories": [ + { + "type": "git", + "url": "https://github.com/karser/quickbooks-php.git" + } + ], + "conflict": { + "symfony/symfony": "*" + }, + "extra": { + "symfony": { + "allow-contrib": false, + "require": "4.4.*" + } + }, + "require-dev": { + "doctrine/doctrine-fixtures-bundle": "^3.3", + "phpstan/phpstan": "^0.12", + "phpstan/phpstan-deprecation-rules": "^0.12", + "phpstan/phpstan-doctrine": "^0.12", + "phpstan/phpstan-phpunit": "^0.12", + "phpstan/phpstan-strict-rules": "^0.12", + "phpstan/phpstan-symfony": "^0.12", + "phpstan/phpstan-webmozart-assert": "^0.12", + "symfony/maker-bundle": "^1.16", + "symfony/test-pack": "^1.0", + "symfony/flex": "^1.6" + } +} diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..4def48a --- /dev/null +++ b/composer.lock @@ -0,0 +1,10374 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "733a37cc6a1bb8b5758dc79e805ab3cf", + "packages": [ + { + "name": "brick/math", + "version": "0.9.2", + "source": { + "type": "git", + "url": "https://github.com/brick/math.git", + "reference": "dff976c2f3487d42c1db75a3b180e2b9f0e72ce0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/brick/math/zipball/dff976c2f3487d42c1db75a3b180e2b9f0e72ce0", + "reference": "dff976c2f3487d42c1db75a3b180e2b9f0e72ce0", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.2", + "phpunit/phpunit": "^7.5.15 || ^8.5 || ^9.0", + "vimeo/psalm": "4.3.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Brick\\Math\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Arbitrary-precision arithmetic library", + "keywords": [ + "Arbitrary-precision", + "BigInteger", + "BigRational", + "arithmetic", + "bigdecimal", + "bignum", + "brick", + "math" + ], + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/brick/math", + "type": "tidelift" + } + ], + "time": "2021-01-20T22:51:39+00:00" + }, + { + "name": "clue/stream-filter", + "version": "v1.5.0", + "source": { + "type": "git", + "url": "https://github.com/clue/stream-filter.git", + "reference": "aeb7d8ea49c7963d3b581378955dbf5bc49aa320" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/clue/stream-filter/zipball/aeb7d8ea49c7963d3b581378955dbf5bc49aa320", + "reference": "aeb7d8ea49c7963d3b581378955dbf5bc49aa320", + "shasum": "" + }, + "require": { + "php": ">=5.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3 || ^5.7 || ^4.8.36" + }, + "type": "library", + "autoload": { + "psr-4": { + "Clue\\StreamFilter\\": "src/" + }, + "files": [ + "src/functions_include.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering" + } + ], + "description": "A simple and modern approach to stream filtering in PHP", + "homepage": "https://github.com/clue/php-stream-filter", + "keywords": [ + "bucket brigade", + "callback", + "filter", + "php_user_filter", + "stream", + "stream_filter_append", + "stream_filter_register" + ], + "funding": [ + { + "url": "https://clue.engineering/support", + "type": "custom" + }, + { + "url": "https://github.com/clue", + "type": "github" + } + ], + "time": "2020-10-02T12:38:20+00:00" + }, + { + "name": "composer/package-versions-deprecated", + "version": "1.11.99.1", + "source": { + "type": "git", + "url": "https://github.com/composer/package-versions-deprecated.git", + "reference": "7413f0b55a051e89485c5cb9f765fe24bb02a7b6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/package-versions-deprecated/zipball/7413f0b55a051e89485c5cb9f765fe24bb02a7b6", + "reference": "7413f0b55a051e89485c5cb9f765fe24bb02a7b6", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^1.1.0 || ^2.0", + "php": "^7 || ^8" + }, + "replace": { + "ocramius/package-versions": "1.11.99" + }, + "require-dev": { + "composer/composer": "^1.9.3 || ^2.0@dev", + "ext-zip": "^1.13", + "phpunit/phpunit": "^6.5 || ^7" + }, + "type": "composer-plugin", + "extra": { + "class": "PackageVersions\\Installer", + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "PackageVersions\\": "src/PackageVersions" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com" + }, + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be" + } + ], + "description": "Composer plugin that provides efficient querying for installed package versions (no runtime IO)", + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2020-11-11T10:22:58+00:00" + }, + { + "name": "consolibyte/quickbooks", + "version": "dev-qbxmlops130", + "source": { + "type": "git", + "url": "https://github.com/karser/quickbooks-php.git", + "reference": "b4e9e8a756df597104aa32749f36b6eb49829e77" + }, + "require": { + "php": ">=5.0.0" + }, + "type": "library", + "autoload": { + "files": [ + "QuickBooks.php" + ] + }, + "license": [ + "EPL-1.0" + ], + "authors": [ + { + "name": "Keith Palmer", + "email": "support@consolibyte.com", + "homepage": "http://www.consolibyte.com/", + "role": "Developer" + } + ], + "description": "QuickBooks DevKit with support for Intuit Anywhere, Intuit Partner Platform, Web Connector, QuickBooks Merchant Services, and more.", + "homepage": "http://www.consolibyte.com/", + "keywords": [ + "intuit", + "intuit anywhere", + "intuit data services", + "intuit merchant services", + "intuit partner platform", + "qbxml", + "quickbooks", + "quickbooks merchant services", + "quickbooks web connector", + "web connector" + ], + "time": "2020-05-15T15:13:27+00:00" + }, + { + "name": "craue/formflow-bundle", + "version": "3.4.1", + "source": { + "type": "git", + "url": "https://github.com/craue/CraueFormFlowBundle.git", + "reference": "03edb599cab904091fdc074caacc877c12368406" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/craue/CraueFormFlowBundle/zipball/03edb599cab904091fdc074caacc877c12368406", + "reference": "03edb599cab904091fdc074caacc877c12368406", + "shasum": "" + }, + "require": { + "php": "^7.3|^8", + "symfony/config": "~3.4|~4.4|~5.1", + "symfony/dependency-injection": "~3.4|~4.4|~5.1", + "symfony/event-dispatcher": "~3.4|~4.4|~5.1", + "symfony/form": "~3.4|~4.4|~5.1", + "symfony/http-foundation": "~3.4|~4.4|~5.1", + "symfony/http-kernel": "~3.4|~4.4|~5.1", + "symfony/options-resolver": "~3.4|~4.4|~5.1", + "symfony/security-core": "~3.4|~4.4|~5.1", + "symfony/translation": "~3.4|~4.4|~5.1", + "symfony/validator": "~3.4|~4.4|~5.1", + "symfony/yaml": "~3.4|~4.4|~5.1" + }, + "require-dev": { + "craue/translations-tests": "^1.1", + "doctrine/common": "~2.7|~3.0", + "doctrine/doctrine-bundle": "~1.10|~2.0", + "phpunit/phpunit": "^9.4", + "symfony/phpunit-bridge": "^5.2", + "symfony/symfony": "~3.4.23|~4.4|~5.1" + }, + "type": "symfony-bundle", + "extra": { + "branch-alias": { + "dev-master": "3.4.x-dev" + } + }, + "autoload": { + "psr-4": { + "Craue\\FormFlowBundle\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Raue", + "email": "christian.raue@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://github.com/craue/CraueFormFlowBundle/contributors" + } + ], + "description": "Multi-step forms for your Symfony project.", + "homepage": "https://github.com/craue/CraueFormFlowBundle", + "keywords": [ + "form", + "step", + "symfony", + "wizard" + ], + "time": "2021-03-31T16:44:37+00:00" + }, + { + "name": "doctrine/annotations", + "version": "1.12.1", + "source": { + "type": "git", + "url": "https://github.com/doctrine/annotations.git", + "reference": "b17c5014ef81d212ac539f07a1001832df1b6d3b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/annotations/zipball/b17c5014ef81d212ac539f07a1001832df1b6d3b", + "reference": "b17c5014ef81d212ac539f07a1001832df1b6d3b", + "shasum": "" + }, + "require": { + "doctrine/lexer": "1.*", + "ext-tokenizer": "*", + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "doctrine/cache": "1.*", + "doctrine/coding-standard": "^6.0 || ^8.1", + "phpstan/phpstan": "^0.12.20", + "phpunit/phpunit": "^7.5 || ^9.1.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Common\\Annotations\\": "lib/Doctrine/Common/Annotations" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "Docblock Annotations Parser", + "homepage": "https://www.doctrine-project.org/projects/annotations.html", + "keywords": [ + "annotations", + "docblock", + "parser" + ], + "time": "2021-02-21T21:00:45+00:00" + }, + { + "name": "doctrine/cache", + "version": "1.11.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/cache.git", + "reference": "a9c1b59eba5a08ca2770a76eddb88922f504e8e0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/cache/zipball/a9c1b59eba5a08ca2770a76eddb88922f504e8e0", + "reference": "a9c1b59eba5a08ca2770a76eddb88922f504e8e0", + "shasum": "" + }, + "require": { + "php": "~7.1 || ^8.0" + }, + "conflict": { + "doctrine/common": ">2.2,<2.4", + "psr/cache": ">=3" + }, + "require-dev": { + "alcaeus/mongo-php-adapter": "^1.1", + "cache/integration-tests": "dev-master", + "doctrine/coding-standard": "^8.0", + "mongodb/mongodb": "^1.1", + "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0", + "predis/predis": "~1.0", + "psr/cache": "^1.0 || ^2.0", + "symfony/cache": "^4.4 || ^5.2" + }, + "suggest": { + "alcaeus/mongo-php-adapter": "Required to use legacy MongoDB driver" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Common\\Cache\\": "lib/Doctrine/Common/Cache" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Cache library is a popular cache implementation that supports many different drivers such as redis, memcache, apc, mongodb and others.", + "homepage": "https://www.doctrine-project.org/projects/cache.html", + "keywords": [ + "abstraction", + "apcu", + "cache", + "caching", + "couchdb", + "memcached", + "php", + "redis", + "xcache" + ], + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fcache", + "type": "tidelift" + } + ], + "time": "2021-04-13T14:46:17+00:00" + }, + { + "name": "doctrine/collections", + "version": "1.6.7", + "source": { + "type": "git", + "url": "https://github.com/doctrine/collections.git", + "reference": "55f8b799269a1a472457bd1a41b4f379d4cfba4a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/collections/zipball/55f8b799269a1a472457bd1a41b4f379d4cfba4a", + "reference": "55f8b799269a1a472457bd1a41b4f379d4cfba4a", + "shasum": "" + }, + "require": { + "php": "^7.1.3 || ^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^6.0", + "phpstan/phpstan-shim": "^0.9.2", + "phpunit/phpunit": "^7.0", + "vimeo/psalm": "^3.8.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Common\\Collections\\": "lib/Doctrine/Common/Collections" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Collections library that adds additional functionality on top of PHP arrays.", + "homepage": "https://www.doctrine-project.org/projects/collections.html", + "keywords": [ + "array", + "collections", + "iterators", + "php" + ], + "time": "2020-07-27T17:53:49+00:00" + }, + { + "name": "doctrine/common", + "version": "2.13.3", + "source": { + "type": "git", + "url": "https://github.com/doctrine/common.git", + "reference": "f3812c026e557892c34ef37f6ab808a6b567da7f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/common/zipball/f3812c026e557892c34ef37f6ab808a6b567da7f", + "reference": "f3812c026e557892c34ef37f6ab808a6b567da7f", + "shasum": "" + }, + "require": { + "doctrine/annotations": "^1.0", + "doctrine/cache": "^1.0", + "doctrine/collections": "^1.0", + "doctrine/event-manager": "^1.0", + "doctrine/inflector": "^1.0", + "doctrine/lexer": "^1.0", + "doctrine/persistence": "^1.3.3", + "doctrine/reflection": "^1.0", + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^1.0", + "phpstan/phpstan": "^0.11", + "phpstan/phpstan-phpunit": "^0.11", + "phpunit/phpunit": "^7.0", + "squizlabs/php_codesniffer": "^3.0", + "symfony/phpunit-bridge": "^4.0.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.11.x-dev" + } + }, + "autoload": { + "psr-4": { + "Doctrine\\Common\\": "lib/Doctrine/Common" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + }, + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com" + } + ], + "description": "PHP Doctrine Common project is a library that provides additional functionality that other Doctrine projects depend on such as better reflection support, persistence interfaces, proxies, event system and much more.", + "homepage": "https://www.doctrine-project.org/projects/common.html", + "keywords": [ + "common", + "doctrine", + "php" + ], + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fcommon", + "type": "tidelift" + } + ], + "time": "2020-06-05T16:46:05+00:00" + }, + { + "name": "doctrine/dbal", + "version": "2.13.1", + "source": { + "type": "git", + "url": "https://github.com/doctrine/dbal.git", + "reference": "c800380457948e65bbd30ba92cc17cda108bf8c9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/dbal/zipball/c800380457948e65bbd30ba92cc17cda108bf8c9", + "reference": "c800380457948e65bbd30ba92cc17cda108bf8c9", + "shasum": "" + }, + "require": { + "doctrine/cache": "^1.0", + "doctrine/deprecations": "^0.5.3", + "doctrine/event-manager": "^1.0", + "ext-pdo": "*", + "php": "^7.1 || ^8" + }, + "require-dev": { + "doctrine/coding-standard": "8.2.0", + "jetbrains/phpstorm-stubs": "2020.2", + "phpstan/phpstan": "0.12.81", + "phpunit/phpunit": "^7.5.20|^8.5|9.5.0", + "squizlabs/php_codesniffer": "3.6.0", + "symfony/console": "^2.0.5|^3.0|^4.0|^5.0", + "vimeo/psalm": "4.6.4" + }, + "suggest": { + "symfony/console": "For helpful console commands such as SQL execution and import of files." + }, + "bin": [ + "bin/doctrine-dbal" + ], + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\DBAL\\": "lib/Doctrine/DBAL" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + } + ], + "description": "Powerful PHP database abstraction layer (DBAL) with many features for database schema introspection and management.", + "homepage": "https://www.doctrine-project.org/projects/dbal.html", + "keywords": [ + "abstraction", + "database", + "db2", + "dbal", + "mariadb", + "mssql", + "mysql", + "oci8", + "oracle", + "pdo", + "pgsql", + "postgresql", + "queryobject", + "sasql", + "sql", + "sqlanywhere", + "sqlite", + "sqlserver", + "sqlsrv" + ], + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fdbal", + "type": "tidelift" + } + ], + "time": "2021-04-17T17:30:19+00:00" + }, + { + "name": "doctrine/deprecations", + "version": "v0.5.3", + "source": { + "type": "git", + "url": "https://github.com/doctrine/deprecations.git", + "reference": "9504165960a1f83cc1480e2be1dd0a0478561314" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/deprecations/zipball/9504165960a1f83cc1480e2be1dd0a0478561314", + "reference": "9504165960a1f83cc1480e2be1dd0a0478561314", + "shasum": "" + }, + "require": { + "php": "^7.1|^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^6.0|^7.0|^8.0", + "phpunit/phpunit": "^7.0|^8.0|^9.0", + "psr/log": "^1.0" + }, + "suggest": { + "psr/log": "Allows logging deprecations via PSR-3 logger implementation" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Deprecations\\": "lib/Doctrine/Deprecations" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A small layer on top of trigger_error(E_USER_DEPRECATED) or PSR-3 logging with options to disable all deprecations or selectively for packages.", + "homepage": "https://www.doctrine-project.org/", + "time": "2021-03-21T12:59:47+00:00" + }, + { + "name": "doctrine/doctrine-bundle", + "version": "1.12.13", + "source": { + "type": "git", + "url": "https://github.com/doctrine/DoctrineBundle.git", + "reference": "85460b85edd8f61a16ad311e7ffc5d255d3c937c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/DoctrineBundle/zipball/85460b85edd8f61a16ad311e7ffc5d255d3c937c", + "reference": "85460b85edd8f61a16ad311e7ffc5d255d3c937c", + "shasum": "" + }, + "require": { + "doctrine/dbal": "^2.5.12|^3.0", + "doctrine/doctrine-cache-bundle": "~1.2", + "doctrine/persistence": "^1.3.3", + "jdorn/sql-formatter": "^1.2.16", + "php": "^7.1 || ^8.0", + "symfony/cache": "^3.4.30|^4.3.3", + "symfony/config": "^3.4.30|^4.3.3", + "symfony/console": "^3.4.30|^4.3.3", + "symfony/dependency-injection": "^3.4.30|^4.3.3", + "symfony/doctrine-bridge": "^3.4.30|^4.3.3", + "symfony/framework-bundle": "^3.4.30|^4.3.3", + "symfony/service-contracts": "^1.1.1|^2.0" + }, + "conflict": { + "doctrine/orm": "<2.6", + "twig/twig": "<1.34|>=2.0,<2.4" + }, + "require-dev": { + "doctrine/coding-standard": "^6.0", + "doctrine/orm": "^2.6", + "ocramius/proxy-manager": "^2.1", + "php-coveralls/php-coveralls": "^2.1", + "phpunit/phpunit": "^7.5", + "symfony/phpunit-bridge": "^4.2", + "symfony/property-info": "^3.4.30|^4.3.3", + "symfony/proxy-manager-bridge": "^3.4|^4|^5", + "symfony/twig-bridge": "^3.4|^4.1", + "symfony/validator": "^3.4.30|^4.3.3", + "symfony/web-profiler-bundle": "^3.4.30|^4.3.3", + "symfony/yaml": "^3.4.30|^4.3.3", + "twig/twig": "^1.34|^2.12" + }, + "suggest": { + "doctrine/orm": "The Doctrine ORM integration is optional in the bundle.", + "symfony/web-profiler-bundle": "To use the data collector." + }, + "type": "symfony-bundle", + "extra": { + "branch-alias": { + "dev-master": "2.3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Doctrine\\Bundle\\DoctrineBundle\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Symfony Community", + "homepage": "http://symfony.com/contributors" + }, + { + "name": "Doctrine Project", + "homepage": "http://www.doctrine-project.org/" + } + ], + "description": "Symfony DoctrineBundle", + "homepage": "http://www.doctrine-project.org", + "keywords": [ + "database", + "dbal", + "orm", + "persistence" + ], + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fdoctrine-bundle", + "type": "tidelift" + } + ], + "time": "2020-11-14T13:38:44+00:00" + }, + { + "name": "doctrine/doctrine-cache-bundle", + "version": "1.4.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/DoctrineCacheBundle.git", + "reference": "6bee2f9b339847e8a984427353670bad4e7bdccb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/DoctrineCacheBundle/zipball/6bee2f9b339847e8a984427353670bad4e7bdccb", + "reference": "6bee2f9b339847e8a984427353670bad4e7bdccb", + "shasum": "" + }, + "require": { + "doctrine/cache": "^1.4.2", + "doctrine/inflector": "^1.0", + "php": "^7.1", + "symfony/doctrine-bridge": "^3.4|^4.0" + }, + "require-dev": { + "instaclick/coding-standard": "~1.1", + "instaclick/object-calisthenics-sniffs": "dev-master", + "instaclick/symfony2-coding-standard": "dev-remaster", + "phpunit/phpunit": "^7.0", + "predis/predis": "~0.8", + "satooshi/php-coveralls": "^1.0", + "squizlabs/php_codesniffer": "~1.5", + "symfony/console": "^3.4|^4.0", + "symfony/finder": "^3.4|^4.0", + "symfony/framework-bundle": "^3.4|^4.0", + "symfony/phpunit-bridge": "^3.4|^4.0", + "symfony/security-acl": "^2.8", + "symfony/validator": "^3.4|^4.0", + "symfony/yaml": "^3.4|^4.0" + }, + "suggest": { + "symfony/security-acl": "For using this bundle to cache ACLs" + }, + "type": "symfony-bundle", + "extra": { + "branch-alias": { + "dev-master": "1.4.x-dev" + } + }, + "autoload": { + "psr-4": { + "Doctrine\\Bundle\\DoctrineCacheBundle\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Fabio B. Silva", + "email": "fabio.bat.silva@gmail.com" + }, + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@hotmail.com" + }, + { + "name": "Symfony Community", + "homepage": "http://symfony.com/contributors" + }, + { + "name": "Doctrine Project", + "homepage": "http://www.doctrine-project.org/" + } + ], + "description": "Symfony Bundle for Doctrine Cache", + "homepage": "https://www.doctrine-project.org", + "keywords": [ + "cache", + "caching" + ], + "abandoned": true, + "time": "2019-11-29T11:22:01+00:00" + }, + { + "name": "doctrine/doctrine-migrations-bundle", + "version": "2.2.2", + "source": { + "type": "git", + "url": "https://github.com/doctrine/DoctrineMigrationsBundle.git", + "reference": "85f0b847174daf243362c7da80efe1539be64f47" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/DoctrineMigrationsBundle/zipball/85f0b847174daf243362c7da80efe1539be64f47", + "reference": "85f0b847174daf243362c7da80efe1539be64f47", + "shasum": "" + }, + "require": { + "doctrine/doctrine-bundle": "~1.0|~2.0", + "doctrine/migrations": "^2.2", + "php": "^7.1|^8.0", + "symfony/framework-bundle": "~3.4|~4.0|~5.0" + }, + "require-dev": { + "doctrine/coding-standard": "^8.0", + "mikey179/vfsstream": "^1.6", + "phpstan/phpstan": "^0.12", + "phpstan/phpstan-strict-rules": "^0.12", + "phpunit/phpunit": "^7.0|^8.0|^9.0" + }, + "type": "symfony-bundle", + "extra": { + "branch-alias": { + "dev-master": "2.1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Doctrine\\Bundle\\MigrationsBundle\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Doctrine Project", + "homepage": "http://www.doctrine-project.org" + }, + { + "name": "Symfony Community", + "homepage": "http://symfony.com/contributors" + } + ], + "description": "Symfony DoctrineMigrationsBundle", + "homepage": "https://www.doctrine-project.org", + "keywords": [ + "dbal", + "migrations", + "schema" + ], + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fdoctrine-migrations-bundle", + "type": "tidelift" + } + ], + "time": "2020-12-23T15:06:17+00:00" + }, + { + "name": "doctrine/event-manager", + "version": "1.1.1", + "source": { + "type": "git", + "url": "https://github.com/doctrine/event-manager.git", + "reference": "41370af6a30faa9dc0368c4a6814d596e81aba7f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/event-manager/zipball/41370af6a30faa9dc0368c4a6814d596e81aba7f", + "reference": "41370af6a30faa9dc0368c4a6814d596e81aba7f", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "doctrine/common": "<2.9@dev" + }, + "require-dev": { + "doctrine/coding-standard": "^6.0", + "phpunit/phpunit": "^7.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Doctrine\\Common\\": "lib/Doctrine/Common" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + }, + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com" + } + ], + "description": "The Doctrine Event Manager is a simple PHP event system that was built to be used with the various Doctrine projects.", + "homepage": "https://www.doctrine-project.org/projects/event-manager.html", + "keywords": [ + "event", + "event dispatcher", + "event manager", + "event system", + "events" + ], + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fevent-manager", + "type": "tidelift" + } + ], + "time": "2020-05-29T18:28:51+00:00" + }, + { + "name": "doctrine/inflector", + "version": "1.4.4", + "source": { + "type": "git", + "url": "https://github.com/doctrine/inflector.git", + "reference": "4bd5c1cdfcd00e9e2d8c484f79150f67e5d355d9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/inflector/zipball/4bd5c1cdfcd00e9e2d8c484f79150f67e5d355d9", + "reference": "4bd5c1cdfcd00e9e2d8c484f79150f67e5d355d9", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^8.0", + "phpstan/phpstan": "^0.12", + "phpstan/phpstan-phpunit": "^0.12", + "phpstan/phpstan-strict-rules": "^0.12", + "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Doctrine\\Common\\Inflector\\": "lib/Doctrine/Common/Inflector", + "Doctrine\\Inflector\\": "lib/Doctrine/Inflector" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Inflector is a small library that can perform string manipulations with regard to upper/lowercase and singular/plural forms of words.", + "homepage": "https://www.doctrine-project.org/projects/inflector.html", + "keywords": [ + "inflection", + "inflector", + "lowercase", + "manipulation", + "php", + "plural", + "singular", + "strings", + "uppercase", + "words" + ], + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finflector", + "type": "tidelift" + } + ], + "time": "2021-04-16T17:34:40+00:00" + }, + { + "name": "doctrine/instantiator", + "version": "1.4.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/instantiator.git", + "reference": "d56bf6102915de5702778fe20f2de3b2fe570b5b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/d56bf6102915de5702778fe20f2de3b2fe570b5b", + "reference": "d56bf6102915de5702778fe20f2de3b2fe570b5b", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^8.0", + "ext-pdo": "*", + "ext-phar": "*", + "phpbench/phpbench": "^0.13 || 1.0.0-alpha2", + "phpstan/phpstan": "^0.12", + "phpstan/phpstan-phpunit": "^0.12", + "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com", + "homepage": "https://ocramius.github.io/" + } + ], + "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", + "homepage": "https://www.doctrine-project.org/projects/instantiator.html", + "keywords": [ + "constructor", + "instantiate" + ], + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator", + "type": "tidelift" + } + ], + "time": "2020-11-10T18:47:58+00:00" + }, + { + "name": "doctrine/lexer", + "version": "1.2.1", + "source": { + "type": "git", + "url": "https://github.com/doctrine/lexer.git", + "reference": "e864bbf5904cb8f5bb334f99209b48018522f042" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/lexer/zipball/e864bbf5904cb8f5bb334f99209b48018522f042", + "reference": "e864bbf5904cb8f5bb334f99209b48018522f042", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^6.0", + "phpstan/phpstan": "^0.11.8", + "phpunit/phpunit": "^8.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Doctrine\\Common\\Lexer\\": "lib/Doctrine/Common/Lexer" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Lexer parser library that can be used in Top-Down, Recursive Descent Parsers.", + "homepage": "https://www.doctrine-project.org/projects/lexer.html", + "keywords": [ + "annotations", + "docblock", + "lexer", + "parser", + "php" + ], + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Flexer", + "type": "tidelift" + } + ], + "time": "2020-05-25T17:44:05+00:00" + }, + { + "name": "doctrine/migrations", + "version": "2.3.3", + "source": { + "type": "git", + "url": "https://github.com/doctrine/migrations.git", + "reference": "c4c46f7064f6e7795bd7f26549579918b46790fa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/migrations/zipball/c4c46f7064f6e7795bd7f26549579918b46790fa", + "reference": "c4c46f7064f6e7795bd7f26549579918b46790fa", + "shasum": "" + }, + "require": { + "composer/package-versions-deprecated": "^1.8", + "doctrine/dbal": "^2.9", + "friendsofphp/proxy-manager-lts": "^1.0", + "php": "^7.1 || ^8.0", + "symfony/console": "^3.4||^4.4.16||^5.0", + "symfony/stopwatch": "^3.4||^4.0||^5.0" + }, + "require-dev": { + "doctrine/coding-standard": "^8.2", + "doctrine/orm": "^2.6", + "ext-pdo_sqlite": "*", + "jdorn/sql-formatter": "^1.1", + "mikey179/vfsstream": "^1.6", + "phpstan/phpstan": "^0.12", + "phpstan/phpstan-deprecation-rules": "^0.12", + "phpstan/phpstan-phpunit": "^0.12", + "phpstan/phpstan-strict-rules": "^0.12", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.4", + "symfony/process": "^3.4||^4.0||^5.0", + "symfony/yaml": "^3.4||^4.0||^5.0" + }, + "suggest": { + "jdorn/sql-formatter": "Allows to generate formatted SQL with the diff command.", + "symfony/yaml": "Allows the use of yaml for migration configuration files." + }, + "bin": [ + "bin/doctrine-migrations" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Doctrine\\Migrations\\": "lib/Doctrine/Migrations" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Michael Simonson", + "email": "contact@mikesimonson.com" + } + ], + "description": "PHP Doctrine Migrations project offer additional functionality on top of the database abstraction layer (DBAL) for versioning your database schema and easily deploying changes to it. It is a very easy to use and a powerful tool.", + "homepage": "https://www.doctrine-project.org/projects/migrations.html", + "keywords": [ + "database", + "dbal", + "migrations", + "php" + ], + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fmigrations", + "type": "tidelift" + } + ], + "time": "2021-03-14T10:22:48+00:00" + }, + { + "name": "doctrine/orm", + "version": "2.7.5", + "source": { + "type": "git", + "url": "https://github.com/doctrine/orm.git", + "reference": "01187c9260cd085529ddd1273665217cae659640" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/orm/zipball/01187c9260cd085529ddd1273665217cae659640", + "reference": "01187c9260cd085529ddd1273665217cae659640", + "shasum": "" + }, + "require": { + "composer/package-versions-deprecated": "^1.8", + "doctrine/annotations": "^1.11.1", + "doctrine/cache": "^1.9.1", + "doctrine/collections": "^1.5", + "doctrine/common": "^2.11 || ^3.0", + "doctrine/dbal": "^2.9.3", + "doctrine/event-manager": "^1.1", + "doctrine/inflector": "^1.0", + "doctrine/instantiator": "^1.3", + "doctrine/lexer": "^1.0", + "doctrine/persistence": "^1.3.3 || ^2.0", + "ext-pdo": "*", + "php": "^7.1", + "symfony/console": "^3.0|^4.0|^5.0" + }, + "require-dev": { + "doctrine/coding-standard": "^6.0", + "phpstan/phpstan": "^0.12.18", + "phpunit/phpunit": "^8.0", + "symfony/yaml": "^3.4|^4.0|^5.0", + "vimeo/psalm": "^3.11" + }, + "suggest": { + "symfony/yaml": "If you want to use YAML Metadata Mapping Driver" + }, + "bin": [ + "bin/doctrine" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.7.x-dev" + } + }, + "autoload": { + "psr-4": { + "Doctrine\\ORM\\": "lib/Doctrine/ORM" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com" + } + ], + "description": "Object-Relational-Mapper for PHP", + "homepage": "https://www.doctrine-project.org/projects/orm.html", + "keywords": [ + "database", + "orm" + ], + "time": "2020-12-03T08:52:14+00:00" + }, + { + "name": "doctrine/persistence", + "version": "1.3.8", + "source": { + "type": "git", + "url": "https://github.com/doctrine/persistence.git", + "reference": "7a6eac9fb6f61bba91328f15aa7547f4806ca288" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/persistence/zipball/7a6eac9fb6f61bba91328f15aa7547f4806ca288", + "reference": "7a6eac9fb6f61bba91328f15aa7547f4806ca288", + "shasum": "" + }, + "require": { + "doctrine/annotations": "^1.0", + "doctrine/cache": "^1.0", + "doctrine/collections": "^1.0", + "doctrine/event-manager": "^1.0", + "doctrine/reflection": "^1.2", + "php": "^7.1 || ^8.0" + }, + "conflict": { + "doctrine/common": "<2.10@dev" + }, + "require-dev": { + "doctrine/coding-standard": "^6.0", + "phpstan/phpstan": "^0.11", + "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0", + "vimeo/psalm": "^3.11" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Doctrine\\Common\\": "lib/Doctrine/Common", + "Doctrine\\Persistence\\": "lib/Doctrine/Persistence" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + }, + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com" + } + ], + "description": "The Doctrine Persistence project is a set of shared interfaces and functionality that the different Doctrine object mappers share.", + "homepage": "https://doctrine-project.org/projects/persistence.html", + "keywords": [ + "mapper", + "object", + "odm", + "orm", + "persistence" + ], + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fpersistence", + "type": "tidelift" + } + ], + "time": "2020-06-20T12:56:16+00:00" + }, + { + "name": "doctrine/reflection", + "version": "1.2.2", + "source": { + "type": "git", + "url": "https://github.com/doctrine/reflection.git", + "reference": "fa587178be682efe90d005e3a322590d6ebb59a5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/reflection/zipball/fa587178be682efe90d005e3a322590d6ebb59a5", + "reference": "fa587178be682efe90d005e3a322590d6ebb59a5", + "shasum": "" + }, + "require": { + "doctrine/annotations": "^1.0", + "ext-tokenizer": "*", + "php": "^7.1 || ^8.0" + }, + "conflict": { + "doctrine/common": "<2.9" + }, + "require-dev": { + "doctrine/coding-standard": "^6.0 || ^8.2.0", + "doctrine/common": "^2.10", + "phpstan/phpstan": "^0.11.0 || ^0.12.20", + "phpstan/phpstan-phpunit": "^0.11.0 || ^0.12.16", + "phpunit/phpunit": "^7.5 || ^9.1.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Doctrine\\Common\\": "lib/Doctrine/Common" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + }, + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com" + } + ], + "description": "The Doctrine Reflection project is a simple library used by the various Doctrine projects which adds some additional functionality on top of the reflection functionality that comes with PHP. It allows you to get the reflection information about classes, methods and properties statically.", + "homepage": "https://www.doctrine-project.org/projects/reflection.html", + "keywords": [ + "reflection", + "static" + ], + "abandoned": "roave/better-reflection", + "time": "2020-10-27T21:46:55+00:00" + }, + { + "name": "easycorp/easyadmin-bundle", + "version": "v2.3.12", + "source": { + "type": "git", + "url": "https://github.com/EasyCorp/EasyAdminBundle.git", + "reference": "a51b3e5eaa5bfafda9ed2791d2a2e0735c845893" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/EasyCorp/EasyAdminBundle/zipball/a51b3e5eaa5bfafda9ed2791d2a2e0735c845893", + "reference": "a51b3e5eaa5bfafda9ed2791d2a2e0735c845893", + "shasum": "" + }, + "require": { + "doctrine/common": "^2.8 || ^3.0", + "doctrine/doctrine-bundle": "^1.8|^2.0", + "doctrine/orm": "^2.6.3", + "doctrine/persistence": "^1.3.4 || ^2.0", + "pagerfanta/pagerfanta": "^1.0.1|^2.0", + "php": ">=7.1.3", + "symfony/asset": "^4.2|^5.0", + "symfony/cache": "^4.2|^5.0", + "symfony/config": "^4.2|^5.0", + "symfony/dependency-injection": "^4.2|^5.0", + "symfony/doctrine-bridge": "^4.2|^5.0", + "symfony/error-handler": "^4.4|^5.0", + "symfony/event-dispatcher": "^4.2|^5.0", + "symfony/expression-language": "^4.2|^5.0", + "symfony/finder": "^4.2|^5.0", + "symfony/form": "^4.2|^5.0", + "symfony/framework-bundle": "^4.2|^5.0", + "symfony/http-foundation": "^4.2|^5.0", + "symfony/http-kernel": "^4.3.7|^5.0", + "symfony/polyfill-mbstring": "^1.7", + "symfony/property-access": "^4.2|^5.0", + "symfony/security-bundle": "^4.2|^5.0", + "symfony/translation": "^4.2|^5.0", + "symfony/twig-bridge": "^4.2|^5.0", + "symfony/twig-bundle": "^4.2|^5.0", + "symfony/validator": "^4.2|^5.0", + "twig/twig": "^2.11.3|^3.0" + }, + "require-dev": { + "doctrine/data-fixtures": "^1.3", + "doctrine/doctrine-fixtures-bundle": "^3.0", + "psr/log": "~1.0", + "symfony/browser-kit": "^4.2|^5.0", + "symfony/console": "^4.2|^5.0", + "symfony/css-selector": "^4.2|^5.0", + "symfony/dom-crawler": "^4.2|^5.0", + "symfony/phpunit-bridge": "^4.3.5|^5.0", + "symfony/var-dumper": "^4.2|^5.0", + "symfony/yaml": "^4.2|^5.0" + }, + "type": "symfony-bundle", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "EasyCorp\\Bundle\\EasyAdminBundle\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Project Contributors", + "homepage": "https://github.com/EasyCorp/EasyAdminBundle/blob/master/CONTRIBUTORS.md" + } + ], + "description": "Admin generator for Symfony applications", + "homepage": "https://github.com/EasyCorp/EasyAdminBundle", + "keywords": [ + "admin", + "backend", + "generator" + ], + "funding": [ + { + "url": "https://github.com/javiereguiluz", + "type": "github" + } + ], + "time": "2021-01-21T18:39:17+00:00" + }, + { + "name": "egulias/email-validator", + "version": "3.1.1", + "source": { + "type": "git", + "url": "https://github.com/egulias/EmailValidator.git", + "reference": "c81f18a3efb941d8c4d2e025f6183b5c6d697307" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/c81f18a3efb941d8c4d2e025f6183b5c6d697307", + "reference": "c81f18a3efb941d8c4d2e025f6183b5c6d697307", + "shasum": "" + }, + "require": { + "doctrine/lexer": "^1.2", + "php": ">=7.2", + "symfony/polyfill-intl-idn": "^1.15" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.2", + "phpunit/phpunit": "^8.5.8|^9.3.3", + "vimeo/psalm": "^4" + }, + "suggest": { + "ext-intl": "PHP Internationalization Libraries are required to use the SpoofChecking validation" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Egulias\\EmailValidator\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Eduardo Gulias Davis" + } + ], + "description": "A library for validating emails against several RFCs", + "homepage": "https://github.com/egulias/EmailValidator", + "keywords": [ + "email", + "emailvalidation", + "emailvalidator", + "validation", + "validator" + ], + "funding": [ + { + "url": "https://github.com/egulias", + "type": "github" + } + ], + "time": "2021-04-01T18:37:14+00:00" + }, + { + "name": "ezyang/htmlpurifier", + "version": "v4.13.0", + "source": { + "type": "git", + "url": "https://github.com/ezyang/htmlpurifier.git", + "reference": "08e27c97e4c6ed02f37c5b2b20488046c8d90d75" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ezyang/htmlpurifier/zipball/08e27c97e4c6ed02f37c5b2b20488046c8d90d75", + "reference": "08e27c97e4c6ed02f37c5b2b20488046c8d90d75", + "shasum": "" + }, + "require": { + "php": ">=5.2" + }, + "require-dev": { + "simpletest/simpletest": "dev-master#72de02a7b80c6bb8864ef9bf66d41d2f58f826bd" + }, + "type": "library", + "autoload": { + "psr-0": { + "HTMLPurifier": "library/" + }, + "files": [ + "library/HTMLPurifier.composer.php" + ], + "exclude-from-classmap": [ + "/library/HTMLPurifier/Language/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-2.1-or-later" + ], + "authors": [ + { + "name": "Edward Z. Yang", + "email": "admin@htmlpurifier.org", + "homepage": "http://ezyang.com" + } + ], + "description": "Standards compliant HTML filter written in PHP", + "homepage": "http://htmlpurifier.org/", + "keywords": [ + "html" + ], + "time": "2020-06-29T00:56:53+00:00" + }, + { + "name": "florianv/exchanger", + "version": "2.7.0", + "source": { + "type": "git", + "url": "https://github.com/florianv/exchanger.git", + "reference": "082894c630e9dc03154ef75b1bbb5586b7c2841a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/florianv/exchanger/zipball/082894c630e9dc03154ef75b1bbb5586b7c2841a", + "reference": "082894c630e9dc03154ef75b1bbb5586b7c2841a", + "shasum": "" + }, + "require": { + "ext-simplexml": "*", + "php": "^7.1.3 || ^8.0", + "php-http/client-implementation": "^1.0", + "php-http/discovery": "^1.6", + "php-http/httplug": "^1.0 || ^2.0", + "php-http/message-factory": "^1.0.2", + "psr/simple-cache": "^1.0" + }, + "require-dev": { + "nyholm/psr7": "^1.0", + "php-http/message": "^1.7", + "php-http/mock-client": "^1.0", + "phpunit/phpunit": "^7 || ^8 || ^9.4" + }, + "suggest": { + "php-http/guzzle6-adapter": "Required to use Guzzle for sending HTTP requests", + "php-http/message": "Required to use Guzzle for sending HTTP requests" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Exchanger\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Florian Voutzinos", + "email": "florian@voutzinos.com", + "homepage": "https://voutzinos.com" + } + ], + "description": "Currency exchange rates framework for PHP", + "homepage": "https://github.com/florianv/exchanger", + "keywords": [ + "Rate", + "conversion", + "currency", + "exchange rates", + "money" + ], + "time": "2021-04-26T07:00:48+00:00" + }, + { + "name": "friendsofphp/proxy-manager-lts", + "version": "v1.0.3", + "source": { + "type": "git", + "url": "https://github.com/FriendsOfPHP/proxy-manager-lts.git", + "reference": "121af47c9aee9c03031bdeca3fac0540f59aa5c3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/FriendsOfPHP/proxy-manager-lts/zipball/121af47c9aee9c03031bdeca3fac0540f59aa5c3", + "reference": "121af47c9aee9c03031bdeca3fac0540f59aa5c3", + "shasum": "" + }, + "require": { + "laminas/laminas-code": "~3.4.1|^4.0", + "php": ">=7.1", + "symfony/filesystem": "^4.4.17|^5.0" + }, + "conflict": { + "laminas/laminas-stdlib": "<3.2.1", + "zendframework/zend-stdlib": "<3.2.1" + }, + "replace": { + "ocramius/proxy-manager": "^2.1" + }, + "require-dev": { + "ext-phar": "*", + "symfony/phpunit-bridge": "^5.2" + }, + "type": "library", + "extra": { + "thanks": { + "name": "ocramius/proxy-manager", + "url": "https://github.com/Ocramius/ProxyManager" + } + }, + "autoload": { + "psr-4": { + "ProxyManager\\": "src/ProxyManager" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com", + "homepage": "http://ocramius.github.io/" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + } + ], + "description": "Adding support for a wider range of PHP versions to ocramius/proxy-manager", + "homepage": "https://github.com/FriendsOfPHP/proxy-manager-lts", + "keywords": [ + "aop", + "lazy loading", + "proxy", + "proxy pattern", + "service proxies" + ], + "funding": [ + { + "url": "https://github.com/Ocramius", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/ocramius/proxy-manager", + "type": "tidelift" + } + ], + "time": "2021-01-14T21:52:44+00:00" + }, + { + "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" + ], + "time": "2020-06-16T21:01:06+00:00" + }, + { + "name": "guzzlehttp/promises", + "version": "1.4.1", + "source": { + "type": "git", + "url": "https://github.com/guzzle/promises.git", + "reference": "8e7d04f1f6450fef59366c399cfad4b9383aa30d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/promises/zipball/8e7d04f1f6450fef59366c399cfad4b9383aa30d", + "reference": "8e7d04f1f6450fef59366c399cfad4b9383aa30d", + "shasum": "" + }, + "require": { + "php": ">=5.5" + }, + "require-dev": { + "symfony/phpunit-bridge": "^4.4 || ^5.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.4-dev" + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Promise\\": "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 promises library", + "keywords": [ + "promise" + ], + "time": "2021-03-07T09:25:29+00:00" + }, + { + "name": "guzzlehttp/psr7", + "version": "1.8.2", + "source": { + "type": "git", + "url": "https://github.com/guzzle/psr7.git", + "reference": "dc960a912984efb74d0a90222870c72c87f10c91" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/dc960a912984efb74d0a90222870c72c87f10c91", + "reference": "dc960a912984efb74d0a90222870c72c87f10c91", + "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": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Tobias Schultze", + "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" + ], + "time": "2021-04-26T09:17:50+00:00" + }, + { + "name": "jdorn/sql-formatter", + "version": "v1.2.17", + "source": { + "type": "git", + "url": "https://github.com/jdorn/sql-formatter.git", + "reference": "64990d96e0959dff8e059dfcdc1af130728d92bc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/jdorn/sql-formatter/zipball/64990d96e0959dff8e059dfcdc1af130728d92bc", + "reference": "64990d96e0959dff8e059dfcdc1af130728d92bc", + "shasum": "" + }, + "require": { + "php": ">=5.2.4" + }, + "require-dev": { + "phpunit/phpunit": "3.7.*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.3.x-dev" + } + }, + "autoload": { + "classmap": [ + "lib" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jeremy Dorn", + "email": "jeremy@jeremydorn.com", + "homepage": "http://jeremydorn.com/" + } + ], + "description": "a PHP SQL highlighting library", + "homepage": "https://github.com/jdorn/sql-formatter/", + "keywords": [ + "highlight", + "sql" + ], + "time": "2014-01-12T16:20:24+00:00" + }, + { + "name": "laminas/laminas-code", + "version": "4.2.1", + "source": { + "type": "git", + "url": "https://github.com/laminas/laminas-code.git", + "reference": "a1e7f8b6467ac7f277b8e027e8537fa13664a8d8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laminas/laminas-code/zipball/a1e7f8b6467ac7f277b8e027e8537fa13664a8d8", + "reference": "a1e7f8b6467ac7f277b8e027e8537fa13664a8d8", + "shasum": "" + }, + "require": { + "laminas/laminas-eventmanager": "^3.3", + "php": "^7.4 || ~8.0.0" + }, + "conflict": { + "phpspec/prophecy": "<1.9.0" + }, + "replace": { + "zendframework/zend-code": "self.version" + }, + "require-dev": { + "doctrine/annotations": "^1.10.4", + "ext-phar": "*", + "laminas/laminas-coding-standard": "^2.1.4", + "laminas/laminas-stdlib": "^3.3.0", + "phpunit/phpunit": "^9.4.2", + "psalm/plugin-phpunit": "^0.14.0", + "vimeo/psalm": "^4.3.1" + }, + "suggest": { + "doctrine/annotations": "Doctrine\\Common\\Annotations >=1.0 for annotation features", + "laminas/laminas-stdlib": "Laminas\\Stdlib component", + "laminas/laminas-zendframework-bridge": "A bridge with Zend Framework" + }, + "type": "library", + "autoload": { + "psr-4": { + "Laminas\\Code\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "Extensions to the PHP Reflection API, static code scanning, and code generation", + "homepage": "https://laminas.dev", + "keywords": [ + "code", + "laminas", + "laminasframework" + ], + "funding": [ + { + "url": "https://funding.communitybridge.org/projects/laminas-project", + "type": "community_bridge" + } + ], + "time": "2021-04-23T11:54:51+00:00" + }, + { + "name": "laminas/laminas-eventmanager", + "version": "3.3.1", + "source": { + "type": "git", + "url": "https://github.com/laminas/laminas-eventmanager.git", + "reference": "966c859b67867b179fde1eff0cd38df51472ce4a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laminas/laminas-eventmanager/zipball/966c859b67867b179fde1eff0cd38df51472ce4a", + "reference": "966c859b67867b179fde1eff0cd38df51472ce4a", + "shasum": "" + }, + "require": { + "laminas/laminas-zendframework-bridge": "^1.0", + "php": "^7.3 || ^8.0" + }, + "replace": { + "zendframework/zend-eventmanager": "^3.2.1" + }, + "require-dev": { + "container-interop/container-interop": "^1.1", + "laminas/laminas-coding-standard": "~1.0.0", + "laminas/laminas-stdlib": "^2.7.3 || ^3.0", + "phpbench/phpbench": "^0.17.1", + "phpunit/phpunit": "^8.5.8" + }, + "suggest": { + "container-interop/container-interop": "^1.1, to use the lazy listeners feature", + "laminas/laminas-stdlib": "^2.7.3 || ^3.0, to use the FilterChain feature" + }, + "type": "library", + "autoload": { + "psr-4": { + "Laminas\\EventManager\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "Trigger and listen to events within a PHP application", + "homepage": "https://laminas.dev", + "keywords": [ + "event", + "eventmanager", + "events", + "laminas" + ], + "funding": [ + { + "url": "https://funding.communitybridge.org/projects/laminas-project", + "type": "community_bridge" + } + ], + "time": "2021-03-08T15:24:29+00:00" + }, + { + "name": "laminas/laminas-zendframework-bridge", + "version": "1.2.0", + "source": { + "type": "git", + "url": "https://github.com/laminas/laminas-zendframework-bridge.git", + "reference": "6cccbddfcfc742eb02158d6137ca5687d92cee32" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laminas/laminas-zendframework-bridge/zipball/6cccbddfcfc742eb02158d6137ca5687d92cee32", + "reference": "6cccbddfcfc742eb02158d6137ca5687d92cee32", + "shasum": "" + }, + "require": { + "php": "^7.3 || ^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^5.7 || ^6.5 || ^7.5 || ^8.1 || ^9.3", + "psalm/plugin-phpunit": "^0.15.1", + "squizlabs/php_codesniffer": "^3.5", + "vimeo/psalm": "^4.6" + }, + "type": "library", + "extra": { + "laminas": { + "module": "Laminas\\ZendFrameworkBridge" + } + }, + "autoload": { + "files": [ + "src/autoload.php" + ], + "psr-4": { + "Laminas\\ZendFrameworkBridge\\": "src//" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "Alias legacy ZF class names to Laminas Project equivalents.", + "keywords": [ + "ZendFramework", + "autoloading", + "laminas", + "zf" + ], + "funding": [ + { + "url": "https://funding.communitybridge.org/projects/laminas-project", + "type": "community_bridge" + } + ], + "time": "2021-02-25T21:54:58+00:00" + }, + { + "name": "league/flysystem", + "version": "1.1.3", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/flysystem.git", + "reference": "9be3b16c877d477357c015cec057548cf9b2a14a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/9be3b16c877d477357c015cec057548cf9b2a14a", + "reference": "9be3b16c877d477357c015cec057548cf9b2a14a", + "shasum": "" + }, + "require": { + "ext-fileinfo": "*", + "league/mime-type-detection": "^1.3", + "php": "^7.2.5 || ^8.0" + }, + "conflict": { + "league/flysystem-sftp": "<1.0.6" + }, + "require-dev": { + "phpspec/prophecy": "^1.11.1", + "phpunit/phpunit": "^8.5.8" + }, + "suggest": { + "ext-fileinfo": "Required for MimeType", + "ext-ftp": "Allows you to use FTP server storage", + "ext-openssl": "Allows you to use FTPS server storage", + "league/flysystem-aws-s3-v2": "Allows you to use S3 storage with AWS SDK v2", + "league/flysystem-aws-s3-v3": "Allows you to use S3 storage with AWS SDK v3", + "league/flysystem-azure": "Allows you to use Windows Azure Blob storage", + "league/flysystem-cached-adapter": "Flysystem adapter decorator for metadata caching", + "league/flysystem-eventable-filesystem": "Allows you to use EventableFilesystem", + "league/flysystem-rackspace": "Allows you to use Rackspace Cloud Files", + "league/flysystem-sftp": "Allows you to use SFTP server storage via phpseclib", + "league/flysystem-webdav": "Allows you to use WebDAV storage", + "league/flysystem-ziparchive": "Allows you to use ZipArchive adapter", + "spatie/flysystem-dropbox": "Allows you to use Dropbox storage", + "srmklive/flysystem-dropbox-v2": "Allows you to use Dropbox storage for PHP 5 applications" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Flysystem\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frenky.net" + } + ], + "description": "Filesystem abstraction: Many filesystems, one API.", + "keywords": [ + "Cloud Files", + "WebDAV", + "abstraction", + "aws", + "cloud", + "copy.com", + "dropbox", + "file systems", + "files", + "filesystem", + "filesystems", + "ftp", + "rackspace", + "remote", + "s3", + "sftp", + "storage" + ], + "funding": [ + { + "url": "https://offset.earth/frankdejonge", + "type": "other" + } + ], + "time": "2020-08-23T07:39:11+00:00" + }, + { + "name": "league/flysystem-webdav", + "version": "1.0.9", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/flysystem-webdav.git", + "reference": "b5c3c756e60cbd495173ce09c0a68a858803f4ce" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/flysystem-webdav/zipball/b5c3c756e60cbd495173ce09c0a68a858803f4ce", + "reference": "b5c3c756e60cbd495173ce09c0a68a858803f4ce", + "shasum": "" + }, + "require": { + "league/flysystem": "~1.0", + "php": ">=5.6", + "sabre/dav": "~4.0|~3.1" + }, + "require-dev": { + "mockery/mockery": "~1.2", + "phpunit/phpunit": "~4.8" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Flysystem\\WebDAV\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frenky.net" + } + ], + "description": "Flysystem adapter for WebDAV", + "time": "2019-12-13T22:44:03+00:00" + }, + { + "name": "league/mime-type-detection", + "version": "1.7.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/mime-type-detection.git", + "reference": "3b9dff8aaf7323590c1d2e443db701eb1f9aa0d3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/mime-type-detection/zipball/3b9dff8aaf7323590c1d2e443db701eb1f9aa0d3", + "reference": "3b9dff8aaf7323590c1d2e443db701eb1f9aa0d3", + "shasum": "" + }, + "require": { + "ext-fileinfo": "*", + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^2.18", + "phpstan/phpstan": "^0.12.68", + "phpunit/phpunit": "^8.5.8 || ^9.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "League\\MimeTypeDetection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frankdejonge.nl" + } + ], + "description": "Mime-type detection for Flysystem", + "funding": [ + { + "url": "https://github.com/frankdejonge", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/league/flysystem", + "type": "tidelift" + } + ], + "time": "2021-01-18T20:58:21+00:00" + }, + { + "name": "maennchen/zipstream-php", + "version": "2.1.0", + "source": { + "type": "git", + "url": "https://github.com/maennchen/ZipStream-PHP.git", + "reference": "c4c5803cc1f93df3d2448478ef79394a5981cc58" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/maennchen/ZipStream-PHP/zipball/c4c5803cc1f93df3d2448478ef79394a5981cc58", + "reference": "c4c5803cc1f93df3d2448478ef79394a5981cc58", + "shasum": "" + }, + "require": { + "myclabs/php-enum": "^1.5", + "php": ">= 7.1", + "psr/http-message": "^1.0", + "symfony/polyfill-mbstring": "^1.0" + }, + "require-dev": { + "ext-zip": "*", + "guzzlehttp/guzzle": ">= 6.3", + "mikey179/vfsstream": "^1.6", + "phpunit/phpunit": ">= 7.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "ZipStream\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Paul Duncan", + "email": "pabs@pablotron.org" + }, + { + "name": "Jonatan Männchen", + "email": "jonatan@maennchen.ch" + }, + { + "name": "Jesse Donat", + "email": "donatj@gmail.com" + }, + { + "name": "András Kolesár", + "email": "kolesar@kolesar.hu" + } + ], + "description": "ZipStream is a library for dynamically streaming dynamic zip files from PHP without writing to the disk at all on the server.", + "keywords": [ + "stream", + "zip" + ], + "funding": [ + { + "url": "https://opencollective.com/zipstream", + "type": "open_collective" + } + ], + "time": "2020-05-30T13:11:16+00:00" + }, + { + "name": "markbaker/complex", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/MarkBaker/PHPComplex.git", + "reference": "9999f1432fae467bc93c53f357105b4c31bb994c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/MarkBaker/PHPComplex/zipball/9999f1432fae467bc93c53f357105b4c31bb994c", + "reference": "9999f1432fae467bc93c53f357105b4c31bb994c", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "^0.7.0", + "phpcompatibility/php-compatibility": "^9.0", + "phpdocumentor/phpdocumentor": "2.*", + "phploc/phploc": "^4.0", + "phpmd/phpmd": "2.*", + "phpunit/phpunit": "^7.0 || ^8.0 || ^9.3", + "sebastian/phpcpd": "^4.0", + "squizlabs/php_codesniffer": "^3.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Complex\\": "classes/src/" + }, + "files": [ + "classes/src/functions/abs.php", + "classes/src/functions/acos.php", + "classes/src/functions/acosh.php", + "classes/src/functions/acot.php", + "classes/src/functions/acoth.php", + "classes/src/functions/acsc.php", + "classes/src/functions/acsch.php", + "classes/src/functions/argument.php", + "classes/src/functions/asec.php", + "classes/src/functions/asech.php", + "classes/src/functions/asin.php", + "classes/src/functions/asinh.php", + "classes/src/functions/atan.php", + "classes/src/functions/atanh.php", + "classes/src/functions/conjugate.php", + "classes/src/functions/cos.php", + "classes/src/functions/cosh.php", + "classes/src/functions/cot.php", + "classes/src/functions/coth.php", + "classes/src/functions/csc.php", + "classes/src/functions/csch.php", + "classes/src/functions/exp.php", + "classes/src/functions/inverse.php", + "classes/src/functions/ln.php", + "classes/src/functions/log2.php", + "classes/src/functions/log10.php", + "classes/src/functions/negative.php", + "classes/src/functions/pow.php", + "classes/src/functions/rho.php", + "classes/src/functions/sec.php", + "classes/src/functions/sech.php", + "classes/src/functions/sin.php", + "classes/src/functions/sinh.php", + "classes/src/functions/sqrt.php", + "classes/src/functions/tan.php", + "classes/src/functions/tanh.php", + "classes/src/functions/theta.php", + "classes/src/operations/add.php", + "classes/src/operations/subtract.php", + "classes/src/operations/multiply.php", + "classes/src/operations/divideby.php", + "classes/src/operations/divideinto.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mark Baker", + "email": "mark@lange.demon.co.uk" + } + ], + "description": "PHP Class for working with complex numbers", + "homepage": "https://github.com/MarkBaker/PHPComplex", + "keywords": [ + "complex", + "mathematics" + ], + "time": "2020-08-26T10:42:07+00:00" + }, + { + "name": "markbaker/matrix", + "version": "2.1.2", + "source": { + "type": "git", + "url": "https://github.com/MarkBaker/PHPMatrix.git", + "reference": "361c0f545c3172ee26c3d596a0aa03f0cef65e6a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/MarkBaker/PHPMatrix/zipball/361c0f545c3172ee26c3d596a0aa03f0cef65e6a", + "reference": "361c0f545c3172ee26c3d596a0aa03f0cef65e6a", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "^0.7.0", + "phpcompatibility/php-compatibility": "^9.0", + "phpdocumentor/phpdocumentor": "2.*", + "phploc/phploc": "^4.0", + "phpmd/phpmd": "2.*", + "phpunit/phpunit": "^7.0 || ^8.0 || ^9.3", + "sebastian/phpcpd": "^4.0", + "squizlabs/php_codesniffer": "^3.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Matrix\\": "classes/src/" + }, + "files": [ + "classes/src/Functions/adjoint.php", + "classes/src/Functions/antidiagonal.php", + "classes/src/Functions/cofactors.php", + "classes/src/Functions/determinant.php", + "classes/src/Functions/diagonal.php", + "classes/src/Functions/identity.php", + "classes/src/Functions/inverse.php", + "classes/src/Functions/minors.php", + "classes/src/Functions/trace.php", + "classes/src/Functions/transpose.php", + "classes/src/Operations/add.php", + "classes/src/Operations/directsum.php", + "classes/src/Operations/subtract.php", + "classes/src/Operations/multiply.php", + "classes/src/Operations/divideby.php", + "classes/src/Operations/divideinto.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mark Baker", + "email": "mark@demon-angel.eu" + } + ], + "description": "PHP Class for working with matrices", + "homepage": "https://github.com/MarkBaker/PHPMatrix", + "keywords": [ + "mathematics", + "matrix", + "vector" + ], + "time": "2021-01-23T16:37:31+00:00" + }, + { + "name": "monolog/monolog", + "version": "1.26.0", + "source": { + "type": "git", + "url": "https://github.com/Seldaek/monolog.git", + "reference": "2209ddd84e7ef1256b7af205d0717fb62cfc9c33" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/2209ddd84e7ef1256b7af205d0717fb62cfc9c33", + "reference": "2209ddd84e7ef1256b7af205d0717fb62cfc9c33", + "shasum": "" + }, + "require": { + "php": ">=5.3.0", + "psr/log": "~1.0" + }, + "provide": { + "psr/log-implementation": "1.0.0" + }, + "require-dev": { + "aws/aws-sdk-php": "^2.4.9 || ^3.0", + "doctrine/couchdb": "~1.0@dev", + "graylog2/gelf-php": "~1.0", + "php-amqplib/php-amqplib": "~2.4", + "php-console/php-console": "^3.1.3", + "phpstan/phpstan": "^0.12.59", + "phpunit/phpunit": "~4.5", + "ruflin/elastica": ">=0.90 <3.0", + "sentry/sentry": "^0.13", + "swiftmailer/swiftmailer": "^5.3|^6.0" + }, + "suggest": { + "aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB", + "doctrine/couchdb": "Allow sending log messages to a CouchDB server", + "ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)", + "ext-mongo": "Allow sending log messages to a MongoDB server", + "graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server", + "mongodb/mongodb": "Allow sending log messages to a MongoDB server via PHP Driver", + "php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib", + "php-console/php-console": "Allow sending log messages to Google Chrome", + "rollbar/rollbar": "Allow sending log messages to Rollbar", + "ruflin/elastica": "Allow sending log messages to an Elastic Search server", + "sentry/sentry": "Allow sending log messages to a Sentry server" + }, + "type": "library", + "autoload": { + "psr-4": { + "Monolog\\": "src/Monolog" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "Sends your logs to files, sockets, inboxes, databases and various web services", + "homepage": "http://github.com/Seldaek/monolog", + "keywords": [ + "log", + "logging", + "psr-3" + ], + "funding": [ + { + "url": "https://github.com/Seldaek", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/monolog/monolog", + "type": "tidelift" + } + ], + "time": "2020-12-14T12:56:38+00:00" + }, + { + "name": "myclabs/php-enum", + "version": "1.8.0", + "source": { + "type": "git", + "url": "https://github.com/myclabs/php-enum.git", + "reference": "46cf3d8498b095bd33727b13fd5707263af99421" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/php-enum/zipball/46cf3d8498b095bd33727b13fd5707263af99421", + "reference": "46cf3d8498b095bd33727b13fd5707263af99421", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": "^7.3 || ^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.5", + "squizlabs/php_codesniffer": "1.*", + "vimeo/psalm": "^4.5.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "MyCLabs\\Enum\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP Enum contributors", + "homepage": "https://github.com/myclabs/php-enum/graphs/contributors" + } + ], + "description": "PHP Enum implementation", + "homepage": "http://github.com/myclabs/php-enum", + "keywords": [ + "enum" + ], + "funding": [ + { + "url": "https://github.com/mnapoli", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/php-enum", + "type": "tidelift" + } + ], + "time": "2021-02-15T16:11:48+00:00" + }, + { + "name": "nyholm/psr7", + "version": "1.4.0", + "source": { + "type": "git", + "url": "https://github.com/Nyholm/psr7.git", + "reference": "23ae1f00fbc6a886cbe3062ca682391b9cc7c37b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Nyholm/psr7/zipball/23ae1f00fbc6a886cbe3062ca682391b9cc7c37b", + "reference": "23ae1f00fbc6a886cbe3062ca682391b9cc7c37b", + "shasum": "" + }, + "require": { + "php": ">=7.1", + "php-http/message-factory": "^1.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.0" + }, + "provide": { + "psr/http-factory-implementation": "1.0", + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "http-interop/http-factory-tests": "^0.8", + "php-http/psr7-integration-tests": "^1.0", + "phpunit/phpunit": "^7.5 || 8.5 || 9.4", + "symfony/error-handler": "^4.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.4-dev" + } + }, + "autoload": { + "psr-4": { + "Nyholm\\Psr7\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com" + }, + { + "name": "Martijn van der Ven", + "email": "martijn@vanderven.se" + } + ], + "description": "A fast PHP7 implementation of PSR-7", + "homepage": "https://tnyholm.se", + "keywords": [ + "psr-17", + "psr-7" + ], + "funding": [ + { + "url": "https://github.com/Zegnat", + "type": "github" + }, + { + "url": "https://github.com/nyholm", + "type": "github" + } + ], + "time": "2021-02-18T15:41:32+00:00" + }, + { + "name": "oneup/flysystem-bundle", + "version": "3.7.0", + "source": { + "type": "git", + "url": "https://github.com/1up-lab/OneupFlysystemBundle.git", + "reference": "de1aa0fa361496b3837da5cac286f029595581da" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/1up-lab/OneupFlysystemBundle/zipball/de1aa0fa361496b3837da5cac286f029595581da", + "reference": "de1aa0fa361496b3837da5cac286f029595581da", + "shasum": "" + }, + "require": { + "league/flysystem": "^1.0.26", + "php": ">=7.1", + "symfony/config": "^3.4 || ^4.0 || ^5.0", + "symfony/dependency-injection": "^3.4 || ^4.0 || ^5.0", + "symfony/http-kernel": "^3.4 || ^4.0 || ^5.0" + }, + "conflict": { + "async-aws/flysystem-s3": "<1.0" + }, + "require-dev": { + "async-aws/flysystem-s3": "^1.0", + "jenko/flysystem-gaufrette": "^1.0", + "league/flysystem-aws-s3-v2": "^1.0", + "league/flysystem-azure-blob-storage": "^0.1", + "league/flysystem-cached-adapter": "^1.0", + "league/flysystem-gridfs": "^1.0", + "league/flysystem-memory": "^1.0", + "league/flysystem-rackspace": "^1.0", + "league/flysystem-replicate-adapter": "^1.0", + "league/flysystem-sftp": "^1.0", + "league/flysystem-webdav": "^1.0", + "league/flysystem-ziparchive": "^1.0", + "litipk/flysystem-fallback-adapter": "^0.1", + "phpunit/phpunit": "^6.5 || ^7.5 || ^8.5", + "spatie/flysystem-dropbox": "^1.0", + "superbalist/flysystem-google-storage": "^4.0", + "symfony/asset": "^3.4 || ^4.0 || ^5.0", + "symfony/browser-kit": "^3.4 || ^4.0 || ^5.0", + "symfony/finder": "^3.4 || ^4.0 || ^5.0", + "symfony/templating": "^3.4 || ^4.0 || ^5.0", + "symfony/translation": "^3.4 || ^4.0 || ^5.0", + "symfony/yaml": "^3.4 || ^4.0 || ^5.0", + "twistor/flysystem-stream-wrapper": "^1.0" + }, + "suggest": { + "ext-fileinfo": "Required for MimeType", + "ext-ftp": "Required for FTP and SFTP", + "jenko/flysystem-gaufrette": "Allows you to use gaufrette adapter", + "league/flysystem-aws-s3-v2": "Use S3 storage with AWS SDK v2", + "league/flysystem-aws-s3-v3": "Use S3 storage with AWS SDK v3", + "league/flysystem-azure-blob-storage": "Allows you to use Azure Blob Storage adapter", + "league/flysystem-cached-adapter": "Flysystem adapter decorator for metadata caching", + "league/flysystem-gridfs": "Allows you to use GridFS adapter", + "league/flysystem-rackspace": "Allows you to use Rackspace Cloud Files", + "league/flysystem-replicate-adapter": "Allows you to use the Replicate adapter from Flysystem", + "league/flysystem-sftp": "Allows SFTP server storage via phpseclib", + "league/flysystem-webdav": "Allows you to use WebDAV storage", + "league/flysystem-ziparchive": "Allows you to use ZipArchive adapter", + "litipk/flysystem-fallback-adapter": "Allows you to use a fallback filesystem", + "spatie/flysystem-dropbox": "Use Dropbox storage", + "superbalist/flysystem-google-storage": "Allows you to use Google Cloud Storage buckets", + "twistor/flysystem-stream-wrapper": "Allows you to use stream wrapper" + }, + "type": "symfony-bundle", + "autoload": { + "psr-4": { + "Oneup\\FlysystemBundle\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jim Schmid", + "email": "js@1up.io", + "homepage": "https://1up.io", + "role": "Developer" + }, + { + "name": "David Greminger", + "email": "dg@1up.io", + "homepage": "https://1up.io", + "role": "Developer" + } + ], + "description": "Integrates Flysystem filesystem abstraction library to your Symfony project.", + "homepage": "https://1up.io", + "keywords": [ + "Flysystem", + "abstraction", + "filesystem", + "symfony" + ], + "time": "2020-12-22T08:19:35+00:00" + }, + { + "name": "pagerfanta/pagerfanta", + "version": "v2.7.1", + "source": { + "type": "git", + "url": "https://github.com/BabDev/Pagerfanta.git", + "reference": "630f38d57c86b67565b644db9d270ffb6d67123f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/BabDev/Pagerfanta/zipball/630f38d57c86b67565b644db9d270ffb6d67123f", + "reference": "630f38d57c86b67565b644db9d270ffb6d67123f", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": "^7.2 || ^8.0", + "symfony/deprecation-contracts": "^2.1" + }, + "conflict": { + "twig/twig": "<1.35 || >=2.0,<2.5" + }, + "replace": { + "pagerfanta/core": "self.version", + "pagerfanta/doctrine-collections-adapter": "self.version", + "pagerfanta/doctrine-dbal-adapter": "self.version", + "pagerfanta/doctrine-mongodb-odm-adapter": "self.version", + "pagerfanta/doctrine-orm-adapter": "self.version", + "pagerfanta/doctrine-phpcr-odm-adapter": "self.version", + "pagerfanta/elastica-adapter": "self.version", + "pagerfanta/solarium-adapter": "self.version", + "pagerfanta/twig": "self.version" + }, + "require-dev": { + "dg/bypass-finals": "^1.2.2", + "doctrine/collections": "^1.4", + "doctrine/dbal": "^2.5 || ^3.0", + "doctrine/orm": "^2.5", + "doctrine/phpcr-odm": "^1.3", + "friendsofphp/php-cs-fixer": "^2.18.2", + "jackalope/jackalope-doctrine-dbal": "^1.3", + "mandango/mandango": "^1.0@dev", + "phpstan/extension-installer": "^1.0.4", + "phpstan/phpstan": "^0.12.79", + "phpstan/phpstan-phpunit": "^0.12.17", + "phpunit/phpunit": "^8.5 || ^9.0", + "propel/propel": "^2.0@dev", + "propel/propel1": "^1.7", + "ruflin/elastica": "^1.3 || ^2.0 || ^3.0 || ^5.0 || ^6.0 || ^7.0", + "solarium/solarium": "^2.3 || ^3.0 || ^4.0 || ^5.0 || ^6.0", + "twig/twig": "^1.35 || ^2.5 || ^3.0" + }, + "suggest": { + "twig/twig": "To integrate Pagerfanta with Twig" + }, + "type": "library", + "autoload": { + "psr-4": { + "Pagerfanta\\": "lib/Core/", + "Pagerfanta\\Adapter\\": "src/Adapter/", + "Pagerfanta\\Doctrine\\Collections\\": "lib/Adapter/Doctrine/Collections/", + "Pagerfanta\\Doctrine\\DBAL\\": "lib/Adapter/Doctrine/DBAL/", + "Pagerfanta\\Doctrine\\MongoDBODM\\": "lib/Adapter/Doctrine/MongoDBODM/", + "Pagerfanta\\Doctrine\\ORM\\": "lib/Adapter/Doctrine/ORM/", + "Pagerfanta\\Doctrine\\PHPCRODM\\": "lib/Adapter/Doctrine/PHPCRODM/", + "Pagerfanta\\Elastica\\": "lib/Adapter/Elastica/", + "Pagerfanta\\Solarium\\": "lib/Adapter/Solarium/", + "Pagerfanta\\Twig\\": "lib/Twig/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Pagination for PHP", + "keywords": [ + "page", + "pagination", + "paginator", + "paging" + ], + "funding": [ + { + "url": "https://github.com/mbabker", + "type": "github" + } + ], + "time": "2021-02-28T18:15:42+00:00" + }, + { + "name": "php-http/discovery", + "version": "1.13.0", + "source": { + "type": "git", + "url": "https://github.com/php-http/discovery.git", + "reference": "788f72d64c43dc361e7fcc7464c3d947c64984a7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/discovery/zipball/788f72d64c43dc361e7fcc7464c3d947c64984a7", + "reference": "788f72d64c43dc361e7fcc7464c3d947c64984a7", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "nyholm/psr7": "<1.0" + }, + "require-dev": { + "graham-campbell/phpspec-skip-example-extension": "^5.0", + "php-http/httplug": "^1.0 || ^2.0", + "php-http/message-factory": "^1.0", + "phpspec/phpspec": "^5.1 || ^6.1", + "puli/composer-plugin": "1.0.0-beta10" + }, + "suggest": { + "php-http/message": "Allow to use Guzzle, Diactoros or Slim Framework factories", + "puli/composer-plugin": "Sets up Puli which is recommended for Discovery to work. Check http://docs.php-http.org/en/latest/discovery.html for more details." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.9-dev" + } + }, + "autoload": { + "psr-4": { + "Http\\Discovery\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com" + } + ], + "description": "Finds installed HTTPlug implementations and PSR-7 message factories", + "homepage": "http://php-http.org", + "keywords": [ + "adapter", + "client", + "discovery", + "factory", + "http", + "message", + "psr7" + ], + "time": "2020-11-27T14:49:42+00:00" + }, + { + "name": "php-http/guzzle6-adapter", + "version": "v2.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-http/guzzle6-adapter.git", + "reference": "9d1a45eb1c59f12574552e81fb295e9e53430a56" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/guzzle6-adapter/zipball/9d1a45eb1c59f12574552e81fb295e9e53430a56", + "reference": "9d1a45eb1c59f12574552e81fb295e9e53430a56", + "shasum": "" + }, + "require": { + "guzzlehttp/guzzle": "^6.0", + "php": "^7.1 || ^8.0", + "php-http/httplug": "^2.0", + "psr/http-client": "^1.0" + }, + "provide": { + "php-http/async-client-implementation": "1.0", + "php-http/client-implementation": "1.0", + "psr/http-client-implementation": "1.0" + }, + "require-dev": { + "ext-curl": "*", + "php-http/client-integration-tests": "^2.0 || ^3.0", + "phpunit/phpunit": "^7.4 || ^8.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Http\\Adapter\\Guzzle6\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "David de Boer", + "email": "david@ddeboer.nl" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com" + } + ], + "description": "Guzzle 6 HTTP Adapter", + "homepage": "http://httplug.io", + "keywords": [ + "Guzzle", + "http" + ], + "time": "2021-03-02T10:52:33+00:00" + }, + { + "name": "php-http/httplug", + "version": "2.2.0", + "source": { + "type": "git", + "url": "https://github.com/php-http/httplug.git", + "reference": "191a0a1b41ed026b717421931f8d3bd2514ffbf9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/httplug/zipball/191a0a1b41ed026b717421931f8d3bd2514ffbf9", + "reference": "191a0a1b41ed026b717421931f8d3bd2514ffbf9", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0", + "php-http/promise": "^1.1", + "psr/http-client": "^1.0", + "psr/http-message": "^1.0" + }, + "require-dev": { + "friends-of-phpspec/phpspec-code-coverage": "^4.1", + "phpspec/phpspec": "^5.1 || ^6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Http\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Eric GELOEN", + "email": "geloen.eric@gmail.com" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://sagikazarmark.hu" + } + ], + "description": "HTTPlug, the HTTP client abstraction for PHP", + "homepage": "http://httplug.io", + "keywords": [ + "client", + "http" + ], + "time": "2020-07-13T15:43:23+00:00" + }, + { + "name": "php-http/message", + "version": "1.11.0", + "source": { + "type": "git", + "url": "https://github.com/php-http/message.git", + "reference": "fb0dbce7355cad4f4f6a225f537c34d013571f29" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/message/zipball/fb0dbce7355cad4f4f6a225f537c34d013571f29", + "reference": "fb0dbce7355cad4f4f6a225f537c34d013571f29", + "shasum": "" + }, + "require": { + "clue/stream-filter": "^1.5", + "php": "^7.1 || ^8.0", + "php-http/message-factory": "^1.0.2", + "psr/http-message": "^1.0" + }, + "provide": { + "php-http/message-factory-implementation": "1.0" + }, + "require-dev": { + "ergebnis/composer-normalize": "^2.6", + "ext-zlib": "*", + "guzzlehttp/psr7": "^1.0", + "laminas/laminas-diactoros": "^2.0", + "phpspec/phpspec": "^5.1 || ^6.3", + "slim/slim": "^3.0" + }, + "suggest": { + "ext-zlib": "Used with compressor/decompressor streams", + "guzzlehttp/psr7": "Used with Guzzle PSR-7 Factories", + "laminas/laminas-diactoros": "Used with Diactoros Factories", + "slim/slim": "Used with Slim Framework PSR-7 implementation" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.10-dev" + } + }, + "autoload": { + "psr-4": { + "Http\\Message\\": "src/" + }, + "files": [ + "src/filters.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com" + } + ], + "description": "HTTP Message related tools", + "homepage": "http://php-http.org", + "keywords": [ + "http", + "message", + "psr-7" + ], + "time": "2021-02-01T08:54:58+00:00" + }, + { + "name": "php-http/message-factory", + "version": "v1.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-http/message-factory.git", + "reference": "a478cb11f66a6ac48d8954216cfed9aa06a501a1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/message-factory/zipball/a478cb11f66a6ac48d8954216cfed9aa06a501a1", + "reference": "a478cb11f66a6ac48d8954216cfed9aa06a501a1", + "shasum": "" + }, + "require": { + "php": ">=5.4", + "psr/http-message": "^1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "psr-4": { + "Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com" + } + ], + "description": "Factory interfaces for PSR-7 HTTP Message", + "homepage": "http://php-http.org", + "keywords": [ + "factory", + "http", + "message", + "stream", + "uri" + ], + "time": "2015-12-19T14:08:53+00:00" + }, + { + "name": "php-http/promise", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/php-http/promise.git", + "reference": "4c4c1f9b7289a2ec57cde7f1e9762a5789506f88" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/promise/zipball/4c4c1f9b7289a2ec57cde7f1e9762a5789506f88", + "reference": "4c4c1f9b7289a2ec57cde7f1e9762a5789506f88", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "friends-of-phpspec/phpspec-code-coverage": "^4.3.2", + "phpspec/phpspec": "^5.1.2 || ^6.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1-dev" + } + }, + "autoload": { + "psr-4": { + "Http\\Promise\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Joel Wurtz", + "email": "joel.wurtz@gmail.com" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com" + } + ], + "description": "Promise used for asynchronous HTTP requests", + "homepage": "http://httplug.io", + "keywords": [ + "promise" + ], + "time": "2020-07-07T09:29:14+00:00" + }, + { + "name": "phpdocumentor/reflection-common", + "version": "2.2.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionCommon.git", + "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/1d01c49d4ed62f25aa84a747ad35d5a16924662b", + "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-2.x": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jaap van Otterdijk", + "email": "opensource@ijaap.nl" + } + ], + "description": "Common reflection classes used by phpdocumentor to reflect the code structure", + "homepage": "http://www.phpdoc.org", + "keywords": [ + "FQSEN", + "phpDocumentor", + "phpdoc", + "reflection", + "static analysis" + ], + "time": "2020-06-27T09:03:43+00:00" + }, + { + "name": "phpdocumentor/reflection-docblock", + "version": "5.2.2", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", + "reference": "069a785b2141f5bcf49f3e353548dc1cce6df556" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/069a785b2141f5bcf49f3e353548dc1cce6df556", + "reference": "069a785b2141f5bcf49f3e353548dc1cce6df556", + "shasum": "" + }, + "require": { + "ext-filter": "*", + "php": "^7.2 || ^8.0", + "phpdocumentor/reflection-common": "^2.2", + "phpdocumentor/type-resolver": "^1.3", + "webmozart/assert": "^1.9.1" + }, + "require-dev": { + "mockery/mockery": "~1.3.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + }, + { + "name": "Jaap van Otterdijk", + "email": "account@ijaap.nl" + } + ], + "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", + "time": "2020-09-03T19:13:55+00:00" + }, + { + "name": "phpdocumentor/type-resolver", + "version": "1.4.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/TypeResolver.git", + "reference": "6a467b8989322d92aa1c8bf2bebcc6e5c2ba55c0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/6a467b8989322d92aa1c8bf2bebcc6e5c2ba55c0", + "reference": "6a467b8989322d92aa1c8bf2bebcc6e5c2ba55c0", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0", + "phpdocumentor/reflection-common": "^2.0" + }, + "require-dev": { + "ext-tokenizer": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-1.x": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + } + ], + "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", + "time": "2020-09-17T18:55:26+00:00" + }, + { + "name": "phpoffice/phpspreadsheet", + "version": "1.17.1", + "source": { + "type": "git", + "url": "https://github.com/PHPOffice/PhpSpreadsheet.git", + "reference": "c55269cb06911575a126dc225a05c0e4626e5fb4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/c55269cb06911575a126dc225a05c0e4626e5fb4", + "reference": "c55269cb06911575a126dc225a05c0e4626e5fb4", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-dom": "*", + "ext-fileinfo": "*", + "ext-gd": "*", + "ext-iconv": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-simplexml": "*", + "ext-xml": "*", + "ext-xmlreader": "*", + "ext-xmlwriter": "*", + "ext-zip": "*", + "ext-zlib": "*", + "ezyang/htmlpurifier": "^4.13", + "maennchen/zipstream-php": "^2.1", + "markbaker/complex": "^1.5||^2.0", + "markbaker/matrix": "^1.2||^2.0", + "php": "^7.2||^8.0", + "psr/http-client": "^1.0", + "psr/http-factory": "^1.0", + "psr/simple-cache": "^1.0" + }, + "require-dev": { + "dompdf/dompdf": "^0.8.5", + "friendsofphp/php-cs-fixer": "^2.18", + "jpgraph/jpgraph": "^4.0", + "mpdf/mpdf": "^8.0", + "phpcompatibility/php-compatibility": "^9.3", + "phpunit/phpunit": "^8.5||^9.3", + "squizlabs/php_codesniffer": "^3.5", + "tecnickcom/tcpdf": "^6.3" + }, + "suggest": { + "dompdf/dompdf": "Option for rendering PDF with PDF Writer (doesn't yet support PHP8)", + "jpgraph/jpgraph": "Option for rendering charts, or including charts with PDF or HTML Writers", + "mpdf/mpdf": "Option for rendering PDF with PDF Writer", + "tecnickcom/tcpdf": "Option for rendering PDF with PDF Writer (doesn't yet support PHP8)" + }, + "type": "library", + "autoload": { + "psr-4": { + "PhpOffice\\PhpSpreadsheet\\": "src/PhpSpreadsheet" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Maarten Balliauw", + "homepage": "https://blog.maartenballiauw.be" + }, + { + "name": "Mark Baker", + "homepage": "https://markbakeruk.net" + }, + { + "name": "Franck Lefevre", + "homepage": "https://rootslabs.net" + }, + { + "name": "Erik Tilt" + }, + { + "name": "Adrien Crivelli" + } + ], + "description": "PHPSpreadsheet - Read, Create and Write Spreadsheet documents in PHP - Spreadsheet engine", + "homepage": "https://github.com/PHPOffice/PhpSpreadsheet", + "keywords": [ + "OpenXML", + "excel", + "gnumeric", + "ods", + "php", + "spreadsheet", + "xls", + "xlsx" + ], + "time": "2021-03-02T17:54:11+00:00" + }, + { + "name": "psr/cache", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/cache.git", + "reference": "d11b50ad223250cf17b86e38383413f5a6764bf8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/cache/zipball/d11b50ad223250cf17b86e38383413f5a6764bf8", + "reference": "d11b50ad223250cf17b86e38383413f5a6764bf8", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Cache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for caching libraries", + "keywords": [ + "cache", + "psr", + "psr-6" + ], + "time": "2016-08-06T20:24:11+00:00" + }, + { + "name": "psr/container", + "version": "1.1.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "8622567409010282b7aeebe4bb841fe98b58dcaf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/8622567409010282b7aeebe4bb841fe98b58dcaf", + "reference": "8622567409010282b7aeebe4bb841fe98b58dcaf", + "shasum": "" + }, + "require": { + "php": ">=7.2.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "time": "2021-03-05T17:36:06+00:00" + }, + { + "name": "psr/http-client", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-client.git", + "reference": "2dfb5f6c5eff0e91e20e913f8c5452ed95b86621" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-client/zipball/2dfb5f6c5eff0e91e20e913f8c5452ed95b86621", + "reference": "2dfb5f6c5eff0e91e20e913f8c5452ed95b86621", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0", + "psr/http-message": "^1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP clients", + "homepage": "https://github.com/php-fig/http-client", + "keywords": [ + "http", + "http-client", + "psr", + "psr-18" + ], + "time": "2020-06-29T06:28:15+00:00" + }, + { + "name": "psr/http-factory", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-factory.git", + "reference": "12ac7fcd07e5b077433f5f2bee95b3a771bf61be" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/12ac7fcd07e5b077433f5f2bee95b3a771bf61be", + "reference": "12ac7fcd07e5b077433f5f2bee95b3a771bf61be", + "shasum": "" + }, + "require": { + "php": ">=7.0.0", + "psr/http-message": "^1.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 interfaces for PSR-7 HTTP message factories", + "keywords": [ + "factory", + "http", + "message", + "psr", + "psr-17", + "psr-7", + "request", + "response" + ], + "time": "2019-04-30T12:38:16+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" + ], + "time": "2016-08-06T14:39:51+00:00" + }, + { + "name": "psr/log", + "version": "1.1.4", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "d49695b909c3b7628b6289db5479a1c204601f11" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/d49695b909c3b7628b6289db5479a1c204601f11", + "reference": "d49695b909c3b7628b6289db5479a1c204601f11", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "Psr/Log/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "time": "2021-05-03T11:20:27+00:00" + }, + { + "name": "psr/simple-cache", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/simple-cache.git", + "reference": "408d5eafb83c57f6365a3ca330ff23aa4a5fa39b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/simple-cache/zipball/408d5eafb83c57f6365a3ca330ff23aa4a5fa39b", + "reference": "408d5eafb83c57f6365a3ca330ff23aa4a5fa39b", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\SimpleCache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interfaces for simple caching", + "keywords": [ + "cache", + "caching", + "psr", + "psr-16", + "simple-cache" + ], + "time": "2017-10-23T01:57:42+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.", + "time": "2019-03-08T08:55:37+00:00" + }, + { + "name": "ramsey/collection", + "version": "1.1.3", + "source": { + "type": "git", + "url": "https://github.com/ramsey/collection.git", + "reference": "28a5c4ab2f5111db6a60b2b4ec84057e0f43b9c1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ramsey/collection/zipball/28a5c4ab2f5111db6a60b2b4ec84057e0f43b9c1", + "reference": "28a5c4ab2f5111db6a60b2b4ec84057e0f43b9c1", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8" + }, + "require-dev": { + "captainhook/captainhook": "^5.3", + "dealerdirect/phpcodesniffer-composer-installer": "^0.7.0", + "ergebnis/composer-normalize": "^2.6", + "fakerphp/faker": "^1.5", + "hamcrest/hamcrest-php": "^2", + "jangregor/phpstan-prophecy": "^0.8", + "mockery/mockery": "^1.3", + "phpstan/extension-installer": "^1", + "phpstan/phpstan": "^0.12.32", + "phpstan/phpstan-mockery": "^0.12.5", + "phpstan/phpstan-phpunit": "^0.12.11", + "phpunit/phpunit": "^8.5 || ^9", + "psy/psysh": "^0.10.4", + "slevomat/coding-standard": "^6.3", + "squizlabs/php_codesniffer": "^3.5", + "vimeo/psalm": "^4.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Ramsey\\Collection\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ben Ramsey", + "email": "ben@benramsey.com", + "homepage": "https://benramsey.com" + } + ], + "description": "A PHP 7.2+ library for representing and manipulating collections.", + "keywords": [ + "array", + "collection", + "hash", + "map", + "queue", + "set" + ], + "funding": [ + { + "url": "https://github.com/ramsey", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/ramsey/collection", + "type": "tidelift" + } + ], + "time": "2021-01-21T17:40:04+00:00" + }, + { + "name": "ramsey/uuid", + "version": "4.1.1", + "source": { + "type": "git", + "url": "https://github.com/ramsey/uuid.git", + "reference": "cd4032040a750077205918c86049aa0f43d22947" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ramsey/uuid/zipball/cd4032040a750077205918c86049aa0f43d22947", + "reference": "cd4032040a750077205918c86049aa0f43d22947", + "shasum": "" + }, + "require": { + "brick/math": "^0.8 || ^0.9", + "ext-json": "*", + "php": "^7.2 || ^8", + "ramsey/collection": "^1.0", + "symfony/polyfill-ctype": "^1.8" + }, + "replace": { + "rhumsaa/uuid": "self.version" + }, + "require-dev": { + "codeception/aspect-mock": "^3", + "dealerdirect/phpcodesniffer-composer-installer": "^0.6.2 || ^0.7.0", + "doctrine/annotations": "^1.8", + "goaop/framework": "^2", + "mockery/mockery": "^1.3", + "moontoast/math": "^1.1", + "paragonie/random-lib": "^2", + "php-mock/php-mock-mockery": "^1.3", + "php-mock/php-mock-phpunit": "^2.5", + "php-parallel-lint/php-parallel-lint": "^1.1", + "phpbench/phpbench": "^0.17.1", + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "^0.12", + "phpstan/phpstan-mockery": "^0.12", + "phpstan/phpstan-phpunit": "^0.12", + "phpunit/phpunit": "^8.5", + "psy/psysh": "^0.10.0", + "slevomat/coding-standard": "^6.0", + "squizlabs/php_codesniffer": "^3.5", + "vimeo/psalm": "3.9.4" + }, + "suggest": { + "ext-bcmath": "Enables faster math with arbitrary-precision integers using BCMath.", + "ext-ctype": "Enables faster processing of character classification using ctype functions.", + "ext-gmp": "Enables faster math with arbitrary-precision integers using GMP.", + "ext-uuid": "Enables the use of PeclUuidTimeGenerator and PeclUuidRandomGenerator.", + "paragonie/random-lib": "Provides RandomLib for use with the RandomLibAdapter", + "ramsey/uuid-doctrine": "Allows the use of Ramsey\\Uuid\\Uuid as Doctrine field type." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.x-dev" + } + }, + "autoload": { + "psr-4": { + "Ramsey\\Uuid\\": "src/" + }, + "files": [ + "src/functions.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A PHP library for generating and working with universally unique identifiers (UUIDs).", + "homepage": "https://github.com/ramsey/uuid", + "keywords": [ + "guid", + "identifier", + "uuid" + ], + "funding": [ + { + "url": "https://github.com/ramsey", + "type": "github" + } + ], + "time": "2020-08-18T17:17:46+00:00" + }, + { + "name": "sabre/dav", + "version": "4.1.5", + "source": { + "type": "git", + "url": "https://github.com/sabre-io/dav.git", + "reference": "c1afdc77a95efea6ee40c03c45f57c3c0c80ec22" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sabre-io/dav/zipball/c1afdc77a95efea6ee40c03c45f57c3c0c80ec22", + "reference": "c1afdc77a95efea6ee40c03c45f57c3c0c80ec22", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-date": "*", + "ext-dom": "*", + "ext-iconv": "*", + "ext-json": "*", + "ext-mbstring": "*", + "ext-pcre": "*", + "ext-simplexml": "*", + "ext-spl": "*", + "lib-libxml": ">=2.7.0", + "php": "^7.1.0 || ^8.0", + "psr/log": "^1.0", + "sabre/event": "^5.0", + "sabre/http": "^5.0.5", + "sabre/uri": "^2.0", + "sabre/vobject": "^4.2.1", + "sabre/xml": "^2.0.1" + }, + "require-dev": { + "evert/phpdoc-md": "~0.1.0", + "friendsofphp/php-cs-fixer": "^2.17.1", + "monolog/monolog": "^1.18", + "phpstan/phpstan": "^0.12", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.0" + }, + "suggest": { + "ext-curl": "*", + "ext-imap": "*", + "ext-pdo": "*" + }, + "bin": [ + "bin/sabredav", + "bin/naturalselection" + ], + "type": "library", + "autoload": { + "psr-4": { + "Sabre\\DAV\\": "lib/DAV/", + "Sabre\\DAVACL\\": "lib/DAVACL/", + "Sabre\\CalDAV\\": "lib/CalDAV/", + "Sabre\\CardDAV\\": "lib/CardDAV/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Evert Pot", + "email": "me@evertpot.com", + "homepage": "http://evertpot.com/", + "role": "Developer" + } + ], + "description": "WebDAV Framework for PHP", + "homepage": "http://sabre.io/", + "keywords": [ + "CalDAV", + "CardDAV", + "WebDAV", + "framework", + "iCalendar" + ], + "time": "2021-02-12T07:54:23+00:00" + }, + { + "name": "sabre/event", + "version": "5.1.2", + "source": { + "type": "git", + "url": "https://github.com/sabre-io/event.git", + "reference": "c120bec57c17b6251a496efc82b732418b49d50a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sabre-io/event/zipball/c120bec57c17b6251a496efc82b732418b49d50a", + "reference": "c120bec57c17b6251a496efc82b732418b49d50a", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "~2.16.1", + "phpstan/phpstan": "^0.12", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Sabre\\Event\\": "lib/" + }, + "files": [ + "lib/coroutine.php", + "lib/Loop/functions.php", + "lib/Promise/functions.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Evert Pot", + "email": "me@evertpot.com", + "homepage": "http://evertpot.com/", + "role": "Developer" + } + ], + "description": "sabre/event is a library for lightweight event-based programming", + "homepage": "http://sabre.io/event/", + "keywords": [ + "EventEmitter", + "async", + "coroutine", + "eventloop", + "events", + "hooks", + "plugin", + "promise", + "reactor", + "signal" + ], + "time": "2020-10-03T11:02:22+00:00" + }, + { + "name": "sabre/http", + "version": "5.1.1", + "source": { + "type": "git", + "url": "https://github.com/sabre-io/http.git", + "reference": "d0aafede6961df6195ce7a8dad49296b0aaee22e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sabre-io/http/zipball/d0aafede6961df6195ce7a8dad49296b0aaee22e", + "reference": "d0aafede6961df6195ce7a8dad49296b0aaee22e", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-curl": "*", + "ext-mbstring": "*", + "php": "^7.1 || ^8.0", + "sabre/event": ">=4.0 <6.0", + "sabre/uri": "^2.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "~2.16.1", + "phpstan/phpstan": "^0.12", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.0" + }, + "suggest": { + "ext-curl": " to make http requests with the Client class" + }, + "type": "library", + "autoload": { + "files": [ + "lib/functions.php" + ], + "psr-4": { + "Sabre\\HTTP\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Evert Pot", + "email": "me@evertpot.com", + "homepage": "http://evertpot.com/", + "role": "Developer" + } + ], + "description": "The sabre/http library provides utilities for dealing with http requests and responses. ", + "homepage": "https://github.com/fruux/sabre-http", + "keywords": [ + "http" + ], + "time": "2020-10-03T11:27:32+00:00" + }, + { + "name": "sabre/uri", + "version": "2.2.1", + "source": { + "type": "git", + "url": "https://github.com/sabre-io/uri.git", + "reference": "f502edffafea8d746825bd5f0b923a60fd2715ff" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sabre-io/uri/zipball/f502edffafea8d746825bd5f0b923a60fd2715ff", + "reference": "f502edffafea8d746825bd5f0b923a60fd2715ff", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "~2.16.1", + "phpstan/phpstan": "^0.12", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.0" + }, + "type": "library", + "autoload": { + "files": [ + "lib/functions.php" + ], + "psr-4": { + "Sabre\\Uri\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Evert Pot", + "email": "me@evertpot.com", + "homepage": "http://evertpot.com/", + "role": "Developer" + } + ], + "description": "Functions for making sense out of URIs.", + "homepage": "http://sabre.io/uri/", + "keywords": [ + "rfc3986", + "uri", + "url" + ], + "time": "2020-10-03T10:33:23+00:00" + }, + { + "name": "sabre/vobject", + "version": "4.3.5", + "source": { + "type": "git", + "url": "https://github.com/sabre-io/vobject.git", + "reference": "d8a0a9ae215a8acfb51afc29101c7344670b9c83" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sabre-io/vobject/zipball/d8a0a9ae215a8acfb51afc29101c7344670b9c83", + "reference": "d8a0a9ae215a8acfb51afc29101c7344670b9c83", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": "^7.1 || ^8.0", + "sabre/xml": "^2.1" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "~2.17.1", + "phpstan/phpstan": "^0.12", + "phpunit/php-invoker": "^2.0 || ^3.1", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.0" + }, + "suggest": { + "hoa/bench": "If you would like to run the benchmark scripts" + }, + "bin": [ + "bin/vobject", + "bin/generate_vcards" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Sabre\\VObject\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Evert Pot", + "email": "me@evertpot.com", + "homepage": "http://evertpot.com/", + "role": "Developer" + }, + { + "name": "Dominik Tobschall", + "email": "dominik@fruux.com", + "homepage": "http://tobschall.de/", + "role": "Developer" + }, + { + "name": "Ivan Enderlin", + "email": "ivan.enderlin@hoa-project.net", + "homepage": "http://mnt.io/", + "role": "Developer" + } + ], + "description": "The VObject library for PHP allows you to easily parse and manipulate iCalendar and vCard objects", + "homepage": "http://sabre.io/vobject/", + "keywords": [ + "availability", + "freebusy", + "iCalendar", + "ical", + "ics", + "jCal", + "jCard", + "recurrence", + "rfc2425", + "rfc2426", + "rfc2739", + "rfc4770", + "rfc5545", + "rfc5546", + "rfc6321", + "rfc6350", + "rfc6351", + "rfc6474", + "rfc6638", + "rfc6715", + "rfc6868", + "vCalendar", + "vCard", + "vcf", + "xCal", + "xCard" + ], + "time": "2021-02-12T06:28:04+00:00" + }, + { + "name": "sabre/xml", + "version": "2.2.3", + "source": { + "type": "git", + "url": "https://github.com/sabre-io/xml.git", + "reference": "c3b959f821c19b36952ec4a595edd695c216bfc6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sabre-io/xml/zipball/c3b959f821c19b36952ec4a595edd695c216bfc6", + "reference": "c3b959f821c19b36952ec4a595edd695c216bfc6", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-xmlreader": "*", + "ext-xmlwriter": "*", + "lib-libxml": ">=2.6.20", + "php": "^7.1 || ^8.0", + "sabre/uri": ">=1.0,<3.0.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "~2.16.1", + "phpstan/phpstan": "^0.12", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Sabre\\Xml\\": "lib/" + }, + "files": [ + "lib/Deserializer/functions.php", + "lib/Serializer/functions.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Evert Pot", + "email": "me@evertpot.com", + "homepage": "http://evertpot.com/", + "role": "Developer" + }, + { + "name": "Markus Staab", + "email": "markus.staab@redaxo.de", + "role": "Developer" + } + ], + "description": "sabre/xml is an XML library that you may not hate.", + "homepage": "https://sabre.io/xml/", + "keywords": [ + "XMLReader", + "XMLWriter", + "dom", + "xml" + ], + "time": "2020-10-03T10:08:14+00:00" + }, + { + "name": "symfony/amazon-mailer", + "version": "v4.4.21", + "source": { + "type": "git", + "url": "https://github.com/symfony/amazon-mailer.git", + "reference": "62b7f7af65a89cae6aca887ac14e165c7cb836e3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/amazon-mailer/zipball/62b7f7af65a89cae6aca887ac14e165c7cb836e3", + "reference": "62b7f7af65a89cae6aca887ac14e165c7cb836e3", + "shasum": "" + }, + "require": { + "php": ">=7.1.3", + "symfony/mailer": "^4.4.21|^5.2.6" + }, + "require-dev": { + "symfony/http-client": "^4.3|^5.0" + }, + "type": "symfony-bridge", + "autoload": { + "psr-4": { + "Symfony\\Component\\Mailer\\Bridge\\Amazon\\": "" + }, + "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 Amazon Mailer Bridge", + "homepage": "https://symfony.com", + "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-03-12T11:23:44+00:00" + }, + { + "name": "symfony/asset", + "version": "v4.4.22", + "source": { + "type": "git", + "url": "https://github.com/symfony/asset.git", + "reference": "aeedecee0bce60320521083efaf6c359ad710e20" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/asset/zipball/aeedecee0bce60320521083efaf6c359ad710e20", + "reference": "aeedecee0bce60320521083efaf6c359ad710e20", + "shasum": "" + }, + "require": { + "php": ">=7.1.3" + }, + "require-dev": { + "symfony/http-foundation": "^3.4|^4.0|^5.0", + "symfony/http-kernel": "^3.4|^4.0|^5.0" + }, + "suggest": { + "symfony/http-foundation": "" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Asset\\": "" + }, + "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": "Manages URL generation and versioning of web assets such as CSS stylesheets, JavaScript files and image files", + "homepage": "https://symfony.com", + "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-04-07T15:47:03+00:00" + }, + { + "name": "symfony/cache", + "version": "v4.4.22", + "source": { + "type": "git", + "url": "https://github.com/symfony/cache.git", + "reference": "0da1df9b1a31f328f1711b5cd922c38a15c5fc74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/cache/zipball/0da1df9b1a31f328f1711b5cd922c38a15c5fc74", + "reference": "0da1df9b1a31f328f1711b5cd922c38a15c5fc74", + "shasum": "" + }, + "require": { + "php": ">=7.1.3", + "psr/cache": "^1.0|^2.0", + "psr/log": "~1.0", + "symfony/cache-contracts": "^1.1.7|^2", + "symfony/service-contracts": "^1.1|^2", + "symfony/var-exporter": "^4.2|^5.0" + }, + "conflict": { + "doctrine/dbal": "<2.6", + "symfony/dependency-injection": "<3.4", + "symfony/http-kernel": "<4.4|>=5.0", + "symfony/var-dumper": "<4.4" + }, + "provide": { + "psr/cache-implementation": "1.0|2.0", + "psr/simple-cache-implementation": "1.0", + "symfony/cache-implementation": "1.0|2.0" + }, + "require-dev": { + "cache/integration-tests": "dev-master", + "doctrine/cache": "^1.6", + "doctrine/dbal": "^2.6|^3.0", + "predis/predis": "^1.1", + "psr/simple-cache": "^1.0", + "symfony/config": "^4.2|^5.0", + "symfony/dependency-injection": "^3.4|^4.1|^5.0", + "symfony/filesystem": "^4.4|^5.0", + "symfony/http-kernel": "^4.4", + "symfony/var-dumper": "^4.4|^5.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Cache\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "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": "Provides an extended PSR-6, PSR-16 (and tags) implementation", + "homepage": "https://symfony.com", + "keywords": [ + "caching", + "psr6" + ], + "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-04-23T07:09:57+00:00" + }, + { + "name": "symfony/cache-contracts", + "version": "v2.4.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/cache-contracts.git", + "reference": "c0446463729b89dd4fa62e9aeecc80287323615d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/cache-contracts/zipball/c0446463729b89dd4fa62e9aeecc80287323615d", + "reference": "c0446463729b89dd4fa62e9aeecc80287323615d", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "psr/cache": "^1.0|^2.0|^3.0" + }, + "suggest": { + "symfony/cache-implementation": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.4-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Cache\\": "" + } + }, + "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": "Generic abstractions related to caching", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "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-03-23T23:28:01+00:00" + }, + { + "name": "symfony/config", + "version": "v4.4.22", + "source": { + "type": "git", + "url": "https://github.com/symfony/config.git", + "reference": "f6d8318c14e4be81525ae47b30e618f0bed4c7b3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/config/zipball/f6d8318c14e4be81525ae47b30e618f0bed4c7b3", + "reference": "f6d8318c14e4be81525ae47b30e618f0bed4c7b3", + "shasum": "" + }, + "require": { + "php": ">=7.1.3", + "symfony/filesystem": "^3.4|^4.0|^5.0", + "symfony/polyfill-ctype": "~1.8" + }, + "conflict": { + "symfony/finder": "<3.4" + }, + "require-dev": { + "symfony/event-dispatcher": "^3.4|^4.0|^5.0", + "symfony/finder": "^3.4|^4.0|^5.0", + "symfony/messenger": "^4.1|^5.0", + "symfony/service-contracts": "^1.1|^2", + "symfony/yaml": "^3.4|^4.0|^5.0" + }, + "suggest": { + "symfony/yaml": "To use the yaml reference dumper" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Config\\": "" + }, + "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": "Helps you find, load, combine, autofill and validate configuration values of any kind", + "homepage": "https://symfony.com", + "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-04-07T15:47:03+00:00" + }, + { + "name": "symfony/console", + "version": "v4.4.22", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "36bbd079b69b94bcc9c9c9e1e37ca3b1e7971625" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/36bbd079b69b94bcc9c9c9e1e37ca3b1e7971625", + "reference": "36bbd079b69b94bcc9c9c9e1e37ca3b1e7971625", + "shasum": "" + }, + "require": { + "php": ">=7.1.3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/polyfill-php73": "^1.8", + "symfony/polyfill-php80": "^1.15", + "symfony/service-contracts": "^1.1|^2" + }, + "conflict": { + "symfony/dependency-injection": "<3.4", + "symfony/event-dispatcher": "<4.3|>=5", + "symfony/lock": "<4.4", + "symfony/process": "<3.3" + }, + "provide": { + "psr/log-implementation": "1.0" + }, + "require-dev": { + "psr/log": "~1.0", + "symfony/config": "^3.4|^4.0|^5.0", + "symfony/dependency-injection": "^3.4|^4.0|^5.0", + "symfony/event-dispatcher": "^4.3", + "symfony/lock": "^4.4|^5.0", + "symfony/process": "^3.4|^4.0|^5.0", + "symfony/var-dumper": "^4.3|^5.0" + }, + "suggest": { + "psr/log": "For using the console logger", + "symfony/event-dispatcher": "", + "symfony/lock": "", + "symfony/process": "" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Console\\": "" + }, + "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": "Eases the creation of beautiful and testable command line interfaces", + "homepage": "https://symfony.com", + "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-04-16T17:32:19+00:00" + }, + { + "name": "symfony/debug", + "version": "v4.4.22", + "source": { + "type": "git", + "url": "https://github.com/symfony/debug.git", + "reference": "45b2136377cca5f10af858968d6079a482bca473" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/debug/zipball/45b2136377cca5f10af858968d6079a482bca473", + "reference": "45b2136377cca5f10af858968d6079a482bca473", + "shasum": "" + }, + "require": { + "php": ">=7.1.3", + "psr/log": "~1.0", + "symfony/polyfill-php80": "^1.15" + }, + "conflict": { + "symfony/http-kernel": "<3.4" + }, + "require-dev": { + "symfony/http-kernel": "^3.4|^4.0|^5.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Debug\\": "" + }, + "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": "Provides tools to ease debugging PHP code", + "homepage": "https://symfony.com", + "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-04-02T07:50:12+00:00" + }, + { + "name": "symfony/dependency-injection", + "version": "v4.4.22", + "source": { + "type": "git", + "url": "https://github.com/symfony/dependency-injection.git", + "reference": "778b140b3e8f6890f43dc2c978e58e69f188909a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/778b140b3e8f6890f43dc2c978e58e69f188909a", + "reference": "778b140b3e8f6890f43dc2c978e58e69f188909a", + "shasum": "" + }, + "require": { + "php": ">=7.1.3", + "psr/container": "^1.0", + "symfony/service-contracts": "^1.1.6|^2" + }, + "conflict": { + "symfony/config": "<4.3|>=5.0", + "symfony/finder": "<3.4", + "symfony/proxy-manager-bridge": "<3.4", + "symfony/yaml": "<3.4" + }, + "provide": { + "psr/container-implementation": "1.0", + "symfony/service-implementation": "1.0|2.0" + }, + "require-dev": { + "symfony/config": "^4.3", + "symfony/expression-language": "^3.4|^4.0|^5.0", + "symfony/yaml": "^3.4|^4.0|^5.0" + }, + "suggest": { + "symfony/config": "", + "symfony/expression-language": "For using expressions in service container configuration", + "symfony/finder": "For using double-star glob patterns or when GLOB_BRACE portability is required", + "symfony/proxy-manager-bridge": "Generate service proxies to lazy load them", + "symfony/yaml": "" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\DependencyInjection\\": "" + }, + "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": "Allows you to standardize and centralize the way objects are constructed in your application", + "homepage": "https://symfony.com", + "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-04-07T15:47:03+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v2.4.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "5f38c8804a9e97d23e0c8d63341088cd8a22d627" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/5f38c8804a9e97d23e0c8d63341088cd8a22d627", + "reference": "5f38c8804a9e97d23e0c8d63341088cd8a22d627", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.4-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "files": [ + "function.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": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "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-03-23T23:28:01+00:00" + }, + { + "name": "symfony/doctrine-bridge", + "version": "v4.4.22", + "source": { + "type": "git", + "url": "https://github.com/symfony/doctrine-bridge.git", + "reference": "9d42bb12f287bf2378500f745174aae519a2c922" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/doctrine-bridge/zipball/9d42bb12f287bf2378500f745174aae519a2c922", + "reference": "9d42bb12f287bf2378500f745174aae519a2c922", + "shasum": "" + }, + "require": { + "doctrine/event-manager": "~1.0", + "doctrine/persistence": "^1.3|^2", + "php": ">=7.1.3", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.0", + "symfony/service-contracts": "^1.1|^2" + }, + "conflict": { + "phpunit/phpunit": "<4.8.35|<5.4.3,>=5.0", + "symfony/dependency-injection": "<3.4", + "symfony/form": "<4.4", + "symfony/http-kernel": "<4.3.7", + "symfony/messenger": "<4.3", + "symfony/security-core": "<4.4", + "symfony/validator": "<4.4.2|<5.0.2,>=5.0" + }, + "require-dev": { + "composer/package-versions-deprecated": "^1.8", + "doctrine/annotations": "^1.10.4", + "doctrine/cache": "~1.6", + "doctrine/collections": "~1.0", + "doctrine/data-fixtures": "^1.1", + "doctrine/dbal": "^2.6|^3.0", + "doctrine/orm": "^2.6.3", + "symfony/config": "^4.2|^5.0", + "symfony/dependency-injection": "^3.4|^4.0|^5.0", + "symfony/expression-language": "^3.4|^4.0|^5.0", + "symfony/form": "^4.4.11|^5.0.11", + "symfony/http-kernel": "^4.3.7", + "symfony/messenger": "^4.4|^5.0", + "symfony/property-access": "^3.4|^4.0|^5.0", + "symfony/property-info": "^3.4|^4.0|^5.0", + "symfony/proxy-manager-bridge": "^3.4|^4.0|^5.0", + "symfony/security-core": "^4.4|^5.0", + "symfony/stopwatch": "^3.4|^4.0|^5.0", + "symfony/translation": "^3.4|^4.0|^5.0", + "symfony/validator": "^4.4.2|^5.0.2", + "symfony/var-dumper": "^3.4|^4.0|^5.0" + }, + "suggest": { + "doctrine/data-fixtures": "", + "doctrine/dbal": "", + "doctrine/orm": "", + "symfony/form": "", + "symfony/property-info": "", + "symfony/validator": "" + }, + "type": "symfony-bridge", + "autoload": { + "psr-4": { + "Symfony\\Bridge\\Doctrine\\": "" + }, + "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": "Provides integration for Doctrine with various Symfony components", + "homepage": "https://symfony.com", + "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-04-16T13:04:32+00:00" + }, + { + "name": "symfony/dotenv", + "version": "v4.4.20", + "source": { + "type": "git", + "url": "https://github.com/symfony/dotenv.git", + "reference": "4952e5ce9e6df3d737b9e9c337bddf781180a213" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/dotenv/zipball/4952e5ce9e6df3d737b9e9c337bddf781180a213", + "reference": "4952e5ce9e6df3d737b9e9c337bddf781180a213", + "shasum": "" + }, + "require": { + "php": ">=7.1.3" + }, + "require-dev": { + "symfony/process": "^3.4.2|^4.0|^5.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Dotenv\\": "" + }, + "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": "Registers environment variables from a .env file", + "homepage": "https://symfony.com", + "keywords": [ + "dotenv", + "env", + "environment" + ], + "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-01-27T09:09:26+00:00" + }, + { + "name": "symfony/error-handler", + "version": "v4.4.22", + "source": { + "type": "git", + "url": "https://github.com/symfony/error-handler.git", + "reference": "76603a8df8e001436df80758eb03a8baa5324175" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/error-handler/zipball/76603a8df8e001436df80758eb03a8baa5324175", + "reference": "76603a8df8e001436df80758eb03a8baa5324175", + "shasum": "" + }, + "require": { + "php": ">=7.1.3", + "psr/log": "~1.0", + "symfony/debug": "^4.4.5", + "symfony/polyfill-php80": "^1.15", + "symfony/var-dumper": "^4.4|^5.0" + }, + "require-dev": { + "symfony/http-kernel": "^4.4|^5.0", + "symfony/serializer": "^4.4|^5.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\ErrorHandler\\": "" + }, + "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": "Provides tools to manage errors and ease debugging PHP code", + "homepage": "https://symfony.com", + "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-04-02T07:50:12+00:00" + }, + { + "name": "symfony/event-dispatcher", + "version": "v4.4.20", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher.git", + "reference": "c352647244bd376bf7d31efbd5401f13f50dad0c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/c352647244bd376bf7d31efbd5401f13f50dad0c", + "reference": "c352647244bd376bf7d31efbd5401f13f50dad0c", + "shasum": "" + }, + "require": { + "php": ">=7.1.3", + "symfony/event-dispatcher-contracts": "^1.1" + }, + "conflict": { + "symfony/dependency-injection": "<3.4" + }, + "provide": { + "psr/event-dispatcher-implementation": "1.0", + "symfony/event-dispatcher-implementation": "1.1" + }, + "require-dev": { + "psr/log": "~1.0", + "symfony/config": "^3.4|^4.0|^5.0", + "symfony/dependency-injection": "^3.4|^4.0|^5.0", + "symfony/error-handler": "~3.4|~4.4", + "symfony/expression-language": "^3.4|^4.0|^5.0", + "symfony/http-foundation": "^3.4|^4.0|^5.0", + "symfony/service-contracts": "^1.1|^2", + "symfony/stopwatch": "^3.4|^4.0|^5.0" + }, + "suggest": { + "symfony/dependency-injection": "", + "symfony/http-kernel": "" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\EventDispatcher\\": "" + }, + "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": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", + "homepage": "https://symfony.com", + "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-01-27T09:09:26+00:00" + }, + { + "name": "symfony/event-dispatcher-contracts", + "version": "v1.1.9", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher-contracts.git", + "reference": "84e23fdcd2517bf37aecbd16967e83f0caee25a7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/84e23fdcd2517bf37aecbd16967e83f0caee25a7", + "reference": "84e23fdcd2517bf37aecbd16967e83f0caee25a7", + "shasum": "" + }, + "require": { + "php": ">=7.1.3" + }, + "suggest": { + "psr/event-dispatcher": "", + "symfony/event-dispatcher-implementation": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\EventDispatcher\\": "" + } + }, + "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": "Generic abstractions related to dispatching event", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "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": "2020-07-06T13:19:58+00:00" + }, + { + "name": "symfony/expression-language", + "version": "v4.4.22", + "source": { + "type": "git", + "url": "https://github.com/symfony/expression-language.git", + "reference": "0dd911bbb99d7210a8f38d8de4a7964ff4a06533" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/expression-language/zipball/0dd911bbb99d7210a8f38d8de4a7964ff4a06533", + "reference": "0dd911bbb99d7210a8f38d8de4a7964ff4a06533", + "shasum": "" + }, + "require": { + "php": ">=7.1.3", + "symfony/cache": "^3.4|^4.0|^5.0", + "symfony/service-contracts": "^1.1|^2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\ExpressionLanguage\\": "" + }, + "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": "Provides an engine that can compile and evaluate expressions", + "homepage": "https://symfony.com", + "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-04-07T15:47:03+00:00" + }, + { + "name": "symfony/filesystem", + "version": "v4.4.22", + "source": { + "type": "git", + "url": "https://github.com/symfony/filesystem.git", + "reference": "f0f06656a18304cdeacb2c4c0113a2b78a2b4c2a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/f0f06656a18304cdeacb2c4c0113a2b78a2b4c2a", + "reference": "f0f06656a18304cdeacb2c4c0113a2b78a2b4c2a", + "shasum": "" + }, + "require": { + "php": ">=7.1.3", + "symfony/polyfill-ctype": "~1.8" + }, + "type": "library", + "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": "Provides basic utilities for the filesystem", + "homepage": "https://symfony.com", + "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-04-01T10:24:12+00:00" + }, + { + "name": "symfony/finder", + "version": "v4.4.20", + "source": { + "type": "git", + "url": "https://github.com/symfony/finder.git", + "reference": "2543795ab1570df588b9bbd31e1a2bd7037b94f6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/finder/zipball/2543795ab1570df588b9bbd31e1a2bd7037b94f6", + "reference": "2543795ab1570df588b9bbd31e1a2bd7037b94f6", + "shasum": "" + }, + "require": { + "php": ">=7.1.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Finder\\": "" + }, + "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": "Finds files and directories via an intuitive fluent interface", + "homepage": "https://symfony.com", + "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-12T10:48:09+00:00" + }, + { + "name": "symfony/flex", + "version": "v1.12.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/flex.git", + "reference": "e472606b4b3173564f0edbca8f5d32b52fc4f2c9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/flex/zipball/e472606b4b3173564f0edbca8f5d32b52fc4f2c9", + "reference": "e472606b4b3173564f0edbca8f5d32b52fc4f2c9", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^1.0|^2.0", + "php": ">=7.1" + }, + "require-dev": { + "composer/composer": "^1.0.2|^2.0", + "symfony/dotenv": "^4.4|^5.0", + "symfony/filesystem": "^4.4|^5.0", + "symfony/phpunit-bridge": "^4.4|^5.0", + "symfony/process": "^3.4|^4.4|^5.0" + }, + "type": "composer-plugin", + "extra": { + "branch-alias": { + "dev-main": "1.12-dev" + }, + "class": "Symfony\\Flex\\Flex" + }, + "autoload": { + "psr-4": { + "Symfony\\Flex\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien.potencier@gmail.com" + } + ], + "description": "Composer plugin for Symfony", + "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-16T14:05:05+00:00" + }, + { + "name": "symfony/form", + "version": "v4.4.22", + "source": { + "type": "git", + "url": "https://github.com/symfony/form.git", + "reference": "d77279decdd78157a2a3812bc4a75ec565781019" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/form/zipball/d77279decdd78157a2a3812bc4a75ec565781019", + "reference": "d77279decdd78157a2a3812bc4a75ec565781019", + "shasum": "" + }, + "require": { + "php": ">=7.1.3", + "symfony/event-dispatcher": "^4.3", + "symfony/intl": "^4.4|^5.0", + "symfony/options-resolver": "~4.3|^5.0", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.0", + "symfony/property-access": "^3.4.40|^4.4.8|^5.0.8", + "symfony/service-contracts": "^1.1|^2" + }, + "conflict": { + "phpunit/phpunit": "<4.8.35|<5.4.3,>=5.0", + "symfony/console": "<4.3", + "symfony/dependency-injection": "<3.4", + "symfony/doctrine-bridge": "<3.4", + "symfony/framework-bundle": "<3.4", + "symfony/http-kernel": "<4.4", + "symfony/intl": "<4.3", + "symfony/translation": "<4.2", + "symfony/twig-bridge": "<3.4.5|<4.0.5,>=4.0" + }, + "require-dev": { + "doctrine/collections": "~1.0", + "symfony/config": "^3.4|^4.0|^5.0", + "symfony/console": "^4.3|^5.0", + "symfony/dependency-injection": "^3.4|^4.0|^5.0", + "symfony/expression-language": "^3.4|^4.0|^5.0", + "symfony/http-foundation": "^3.4|^4.0|^5.0", + "symfony/http-kernel": "^4.4", + "symfony/security-csrf": "^3.4|^4.0|^5.0", + "symfony/translation": "^4.2|^5.0", + "symfony/validator": "^4.4.17|^5.1.9", + "symfony/var-dumper": "^4.3|^5.0" + }, + "suggest": { + "symfony/security-csrf": "For protecting forms against CSRF attacks.", + "symfony/twig-bridge": "For templating with Twig.", + "symfony/validator": "For form validation." + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Form\\": "" + }, + "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": "Allows to easily create, process and reuse HTML forms", + "homepage": "https://symfony.com", + "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-04-23T21:06:14+00:00" + }, + { + "name": "symfony/framework-bundle", + "version": "v4.4.22", + "source": { + "type": "git", + "url": "https://github.com/symfony/framework-bundle.git", + "reference": "98e855fd35dd2c4614f61d349b1fd7dd3622b9b9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/framework-bundle/zipball/98e855fd35dd2c4614f61d349b1fd7dd3622b9b9", + "reference": "98e855fd35dd2c4614f61d349b1fd7dd3622b9b9", + "shasum": "" + }, + "require": { + "ext-xml": "*", + "php": ">=7.1.3", + "symfony/cache": "^4.4|^5.0", + "symfony/config": "^4.3.4|^5.0", + "symfony/dependency-injection": "^4.4.1|^5.0.1", + "symfony/error-handler": "^4.4.1|^5.0.1", + "symfony/filesystem": "^3.4|^4.0|^5.0", + "symfony/finder": "^3.4|^4.0|^5.0", + "symfony/http-foundation": "^4.4|^5.0", + "symfony/http-kernel": "^4.4", + "symfony/polyfill-mbstring": "~1.0", + "symfony/routing": "^4.4.12|^5.1.4" + }, + "conflict": { + "doctrine/persistence": "<1.3", + "phpdocumentor/reflection-docblock": "<3.0|>=3.2.0,<3.2.2", + "phpdocumentor/type-resolver": "<0.3.0|1.3.*", + "phpunit/phpunit": "<4.8.35|<5.4.3,>=5.0", + "symfony/asset": "<3.4", + "symfony/browser-kit": "<4.3", + "symfony/console": "<4.4.21", + "symfony/dom-crawler": "<4.3", + "symfony/dotenv": "<4.3.6", + "symfony/form": "<4.3.5", + "symfony/http-client": "<4.4", + "symfony/lock": "<4.4", + "symfony/mailer": "<4.4", + "symfony/messenger": "<4.4", + "symfony/mime": "<4.4", + "symfony/property-info": "<3.4", + "symfony/security-bundle": "<4.4", + "symfony/serializer": "<4.4", + "symfony/stopwatch": "<3.4", + "symfony/translation": "<4.4", + "symfony/twig-bridge": "<4.1.1", + "symfony/twig-bundle": "<4.4", + "symfony/validator": "<4.4", + "symfony/web-profiler-bundle": "<4.4", + "symfony/workflow": "<4.3.6" + }, + "require-dev": { + "doctrine/annotations": "^1.10.4", + "doctrine/cache": "~1.0", + "doctrine/persistence": "^1.3|^2.0", + "paragonie/sodium_compat": "^1.8", + "phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0", + "symfony/asset": "^3.4|^4.0|^5.0", + "symfony/browser-kit": "^4.3|^5.0", + "symfony/console": "^4.4.21|^5.0", + "symfony/css-selector": "^3.4|^4.0|^5.0", + "symfony/dom-crawler": "^4.3|^5.0", + "symfony/dotenv": "^4.3.6|^5.0", + "symfony/expression-language": "^3.4|^4.0|^5.0", + "symfony/form": "^4.3.5|^5.0", + "symfony/http-client": "^4.4|^5.0", + "symfony/lock": "^4.4|^5.0", + "symfony/mailer": "^4.4|^5.0", + "symfony/messenger": "^4.4|^5.0", + "symfony/mime": "^4.4|^5.0", + "symfony/polyfill-intl-icu": "~1.0", + "symfony/process": "^3.4|^4.0|^5.0", + "symfony/property-info": "^3.4|^4.0|^5.0", + "symfony/security-core": "^3.4|^4.4|^5.2", + "symfony/security-csrf": "^3.4|^4.0|^5.0", + "symfony/security-http": "^3.4|^4.0|^5.0", + "symfony/serializer": "^4.4|^5.0", + "symfony/stopwatch": "^3.4|^4.0|^5.0", + "symfony/templating": "^3.4|^4.0|^5.0", + "symfony/translation": "^4.4|^5.0", + "symfony/twig-bundle": "^4.4|^5.0", + "symfony/validator": "^4.4|^5.0", + "symfony/web-link": "^4.4|^5.0", + "symfony/workflow": "^4.3.6|^5.0", + "symfony/yaml": "^3.4|^4.0|^5.0", + "twig/twig": "^1.43|^2.13|^3.0.4" + }, + "suggest": { + "ext-apcu": "For best performance of the system caches", + "symfony/console": "For using the console commands", + "symfony/form": "For using forms", + "symfony/property-info": "For using the property_info service", + "symfony/serializer": "For using the serializer service", + "symfony/validator": "For using validation", + "symfony/web-link": "For using web links, features such as preloading, prefetching or prerendering", + "symfony/yaml": "For using the debug:config and lint:yaml commands" + }, + "type": "symfony-bundle", + "autoload": { + "psr-4": { + "Symfony\\Bundle\\FrameworkBundle\\": "" + }, + "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": "Provides a tight integration between Symfony components and the Symfony full-stack framework", + "homepage": "https://symfony.com", + "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-04-14T13:03:08+00:00" + }, + { + "name": "symfony/http-client", + "version": "v4.4.22", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-client.git", + "reference": "ad1e2d512ec080e78ebd65c01ab92bd78057d007" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-client/zipball/ad1e2d512ec080e78ebd65c01ab92bd78057d007", + "reference": "ad1e2d512ec080e78ebd65c01ab92bd78057d007", + "shasum": "" + }, + "require": { + "php": ">=7.1.3", + "psr/log": "^1.0", + "symfony/http-client-contracts": "^1.1.10|^2", + "symfony/polyfill-php73": "^1.11", + "symfony/service-contracts": "^1.0|^2" + }, + "provide": { + "php-http/async-client-implementation": "*", + "php-http/client-implementation": "*", + "psr/http-client-implementation": "1.0", + "symfony/http-client-implementation": "1.1|2.0" + }, + "require-dev": { + "guzzlehttp/promises": "^1.4", + "nyholm/psr7": "^1.0", + "php-http/httplug": "^1.0|^2.0", + "psr/http-client": "^1.0", + "symfony/dependency-injection": "^4.3|^5.0", + "symfony/http-kernel": "^4.4.13", + "symfony/process": "^4.2|^5.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpClient\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "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": "Provides powerful methods to fetch HTTP resources synchronously or asynchronously", + "homepage": "https://symfony.com", + "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-04-12T06:52:04+00:00" + }, + { + "name": "symfony/http-client-contracts", + "version": "v2.4.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-client-contracts.git", + "reference": "7e82f6084d7cae521a75ef2cb5c9457bbda785f4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/7e82f6084d7cae521a75ef2cb5c9457bbda785f4", + "reference": "7e82f6084d7cae521a75ef2cb5c9457bbda785f4", + "shasum": "" + }, + "require": { + "php": ">=7.2.5" + }, + "suggest": { + "symfony/http-client-implementation": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.4-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\HttpClient\\": "" + } + }, + "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": "Generic abstractions related to HTTP clients", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "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-04-11T23:07:08+00:00" + }, + { + "name": "symfony/http-foundation", + "version": "v4.4.22", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-foundation.git", + "reference": "1a6f87ef99d05b1bf5c865b4ef7992263e1cb081" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/1a6f87ef99d05b1bf5c865b4ef7992263e1cb081", + "reference": "1a6f87ef99d05b1bf5c865b4ef7992263e1cb081", + "shasum": "" + }, + "require": { + "php": ">=7.1.3", + "symfony/mime": "^4.3|^5.0", + "symfony/polyfill-mbstring": "~1.1", + "symfony/polyfill-php80": "^1.15" + }, + "require-dev": { + "predis/predis": "~1.0", + "symfony/expression-language": "^3.4|^4.0|^5.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpFoundation\\": "" + }, + "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": "Defines an object-oriented layer for the HTTP specification", + "homepage": "https://symfony.com", + "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-04-30T12:05:50+00:00" + }, + { + "name": "symfony/http-kernel", + "version": "v4.4.22", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-kernel.git", + "reference": "cd2e325fc34a4a5bbec91eecf69dda8ee8c5ea4f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/cd2e325fc34a4a5bbec91eecf69dda8ee8c5ea4f", + "reference": "cd2e325fc34a4a5bbec91eecf69dda8ee8c5ea4f", + "shasum": "" + }, + "require": { + "php": ">=7.1.3", + "psr/log": "~1.0", + "symfony/error-handler": "^4.4", + "symfony/event-dispatcher": "^4.4", + "symfony/http-client-contracts": "^1.1|^2", + "symfony/http-foundation": "^4.4|^5.0", + "symfony/polyfill-ctype": "^1.8", + "symfony/polyfill-php73": "^1.9", + "symfony/polyfill-php80": "^1.15" + }, + "conflict": { + "symfony/browser-kit": "<4.3", + "symfony/config": "<3.4", + "symfony/console": ">=5", + "symfony/dependency-injection": "<4.3", + "symfony/translation": "<4.2", + "twig/twig": "<1.43|<2.13,>=2" + }, + "provide": { + "psr/log-implementation": "1.0" + }, + "require-dev": { + "psr/cache": "^1.0|^2.0|^3.0", + "symfony/browser-kit": "^4.3|^5.0", + "symfony/config": "^3.4|^4.0|^5.0", + "symfony/console": "^3.4|^4.0", + "symfony/css-selector": "^3.4|^4.0|^5.0", + "symfony/dependency-injection": "^4.3|^5.0", + "symfony/dom-crawler": "^3.4|^4.0|^5.0", + "symfony/expression-language": "^3.4|^4.0|^5.0", + "symfony/finder": "^3.4|^4.0|^5.0", + "symfony/process": "^3.4|^4.0|^5.0", + "symfony/routing": "^3.4|^4.0|^5.0", + "symfony/stopwatch": "^3.4|^4.0|^5.0", + "symfony/templating": "^3.4|^4.0|^5.0", + "symfony/translation": "^4.2|^5.0", + "symfony/translation-contracts": "^1.1|^2", + "twig/twig": "^1.43|^2.13|^3.0.4" + }, + "suggest": { + "symfony/browser-kit": "", + "symfony/config": "", + "symfony/console": "", + "symfony/dependency-injection": "" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpKernel\\": "" + }, + "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": "Provides a structured process for converting a Request into a Response", + "homepage": "https://symfony.com", + "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-01T14:38:48+00:00" + }, + { + "name": "symfony/inflector", + "version": "v4.4.21", + "source": { + "type": "git", + "url": "https://github.com/symfony/inflector.git", + "reference": "9455097d23776a4a10c817d903271bc1ce7596ff" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/inflector/zipball/9455097d23776a4a10c817d903271bc1ce7596ff", + "reference": "9455097d23776a4a10c817d903271bc1ce7596ff", + "shasum": "" + }, + "require": { + "php": ">=7.1.3", + "symfony/polyfill-ctype": "~1.8" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Inflector\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Converts words between their singular and plural forms (English only)", + "homepage": "https://symfony.com", + "keywords": [ + "inflection", + "pluralize", + "singularize", + "string", + "symfony", + "words" + ], + "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-03-17T16:19:54+00:00" + }, + { + "name": "symfony/intl", + "version": "v4.4.22", + "source": { + "type": "git", + "url": "https://github.com/symfony/intl.git", + "reference": "a9e178284728e945c839d0a73d5343562cd3de3c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/intl/zipball/a9e178284728e945c839d0a73d5343562cd3de3c", + "reference": "a9e178284728e945c839d0a73d5343562cd3de3c", + "shasum": "" + }, + "require": { + "php": ">=7.1.3", + "symfony/polyfill-intl-icu": "~1.0" + }, + "require-dev": { + "symfony/filesystem": "^3.4|^4.0|^5.0" + }, + "suggest": { + "ext-intl": "to use the component with locales other than \"en\"" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Intl\\": "" + }, + "classmap": [ + "Resources/stubs" + ], + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + }, + { + "name": "Eriksen Costa", + "email": "eriksen.costa@infranology.com.br" + }, + { + "name": "Igor Wiedler", + "email": "igor@wiedler.ch" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides a PHP replacement layer for the C intl extension that includes additional data from the ICU library", + "homepage": "https://symfony.com", + "keywords": [ + "i18n", + "icu", + "internationalization", + "intl", + "l10n", + "localization" + ], + "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-04-23T21:06:14+00:00" + }, + { + "name": "symfony/mailer", + "version": "v4.4.22", + "source": { + "type": "git", + "url": "https://github.com/symfony/mailer.git", + "reference": "327107fc7fd82a9f30357c9babee4acd5a7efd04" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/mailer/zipball/327107fc7fd82a9f30357c9babee4acd5a7efd04", + "reference": "327107fc7fd82a9f30357c9babee4acd5a7efd04", + "shasum": "" + }, + "require": { + "egulias/email-validator": "^2.1.10|^3", + "php": ">=7.1.3", + "psr/log": "~1.0", + "symfony/event-dispatcher": "^4.3", + "symfony/mime": "^4.4.21|^5.2.6", + "symfony/service-contracts": "^1.1|^2" + }, + "conflict": { + "symfony/http-kernel": "<4.4", + "symfony/sendgrid-mailer": "<4.4" + }, + "require-dev": { + "symfony/amazon-mailer": "^4.4|^5.0", + "symfony/google-mailer": "^4.4|^5.0", + "symfony/http-client-contracts": "^1.1|^2", + "symfony/mailchimp-mailer": "^4.4|^5.0", + "symfony/mailgun-mailer": "^4.4|^5.0", + "symfony/messenger": "^4.4|^5.0", + "symfony/postmark-mailer": "^4.4|^5.0", + "symfony/sendgrid-mailer": "^4.4|^5.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Mailer\\": "" + }, + "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": "Helps sending emails", + "homepage": "https://symfony.com", + "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-04-16T12:10:02+00:00" + }, + { + "name": "symfony/mime", + "version": "v4.4.22", + "source": { + "type": "git", + "url": "https://github.com/symfony/mime.git", + "reference": "36f2e59c90762bb09170553130a4dc1934cada58" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/mime/zipball/36f2e59c90762bb09170553130a4dc1934cada58", + "reference": "36f2e59c90762bb09170553130a4dc1934cada58", + "shasum": "" + }, + "require": { + "php": ">=7.1.3", + "symfony/polyfill-intl-idn": "^1.10", + "symfony/polyfill-mbstring": "^1.0" + }, + "conflict": { + "egulias/email-validator": "~3.0.0", + "symfony/mailer": "<4.4" + }, + "require-dev": { + "egulias/email-validator": "^2.1.10|^3.1", + "symfony/dependency-injection": "^3.4|^4.1|^5.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Mime\\": "" + }, + "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": "Allows manipulating MIME messages", + "homepage": "https://symfony.com", + "keywords": [ + "mime", + "mime-type" + ], + "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-04-27T14:58:50+00:00" + }, + { + "name": "symfony/monolog-bridge", + "version": "v4.4.22", + "source": { + "type": "git", + "url": "https://github.com/symfony/monolog-bridge.git", + "reference": "cdf4b4cdf9ffbc47fc8f3612a291e6b4db1b9e7e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/monolog-bridge/zipball/cdf4b4cdf9ffbc47fc8f3612a291e6b4db1b9e7e", + "reference": "cdf4b4cdf9ffbc47fc8f3612a291e6b4db1b9e7e", + "shasum": "" + }, + "require": { + "monolog/monolog": "^1.25.1", + "php": ">=7.1.3", + "symfony/http-kernel": "^4.3", + "symfony/service-contracts": "^1.1|^2" + }, + "conflict": { + "symfony/console": "<3.4", + "symfony/http-foundation": "<3.4" + }, + "require-dev": { + "symfony/console": "^3.4|^4.0|^5.0", + "symfony/http-client": "^4.4|^5.0", + "symfony/security-core": "^3.4|^4.0|^5.0", + "symfony/var-dumper": "^3.4|^4.0|^5.0" + }, + "suggest": { + "symfony/console": "For the possibility to show log messages in console commands depending on verbosity settings.", + "symfony/http-kernel": "For using the debugging handlers together with the response life cycle of the HTTP kernel.", + "symfony/var-dumper": "For using the debugging handlers like the console handler or the log server handler." + }, + "type": "symfony-bridge", + "autoload": { + "psr-4": { + "Symfony\\Bridge\\Monolog\\": "" + }, + "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": "Provides integration for Monolog with various Symfony components", + "homepage": "https://symfony.com", + "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-04-07T15:47:03+00:00" + }, + { + "name": "symfony/monolog-bundle", + "version": "v3.7.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/monolog-bundle.git", + "reference": "4054b2e940a25195ae15f0a49ab0c51718922eb4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/monolog-bundle/zipball/4054b2e940a25195ae15f0a49ab0c51718922eb4", + "reference": "4054b2e940a25195ae15f0a49ab0c51718922eb4", + "shasum": "" + }, + "require": { + "monolog/monolog": "~1.22 || ~2.0", + "php": ">=7.1.3", + "symfony/config": "~4.4 || ^5.0", + "symfony/dependency-injection": "^4.4 || ^5.0", + "symfony/http-kernel": "~4.4 || ^5.0", + "symfony/monolog-bridge": "~4.4 || ^5.0" + }, + "require-dev": { + "symfony/console": "~4.4 || ^5.0", + "symfony/phpunit-bridge": "^5.1", + "symfony/yaml": "~4.4 || ^5.0" + }, + "type": "symfony-bundle", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Bundle\\MonologBundle\\": "" + }, + "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 MonologBundle", + "homepage": "https://symfony.com", + "keywords": [ + "log", + "logging" + ], + "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-03-31T07:20:47+00:00" + }, + { + "name": "symfony/options-resolver", + "version": "v4.4.20", + "source": { + "type": "git", + "url": "https://github.com/symfony/options-resolver.git", + "reference": "cd8c6a2778d5f8b5e8fc4b7abdf126790b5d5095" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/cd8c6a2778d5f8b5e8fc4b7abdf126790b5d5095", + "reference": "cd8c6a2778d5f8b5e8fc4b7abdf126790b5d5095", + "shasum": "" + }, + "require": { + "php": ">=7.1.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\OptionsResolver\\": "" + }, + "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": "Provides an improved replacement for the array_replace PHP function", + "homepage": "https://symfony.com", + "keywords": [ + "config", + "configuration", + "options" + ], + "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-01-27T09:09:26+00:00" + }, + { + "name": "symfony/polyfill-intl-icu", + "version": "v1.22.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-icu.git", + "reference": "af1842919c7e7364aaaa2798b29839e3ba168588" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-icu/zipball/af1842919c7e7364aaaa2798b29839e3ba168588", + "reference": "af1842919c7e7364aaaa2798b29839e3ba168588", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "suggest": { + "ext-intl": "For best performance and support of other locales than \"en\"" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.22-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Icu\\": "" + }, + "classmap": [ + "Resources/stubs" + ], + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "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 ICU-related data and classes", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "icu", + "intl", + "polyfill", + "portable", + "shim" + ], + "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-01-22T09:19:47+00:00" + }, + { + "name": "symfony/polyfill-intl-idn", + "version": "v1.22.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-idn.git", + "reference": "2d63434d922daf7da8dd863e7907e67ee3031483" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/2d63434d922daf7da8dd863e7907e67ee3031483", + "reference": "2d63434d922daf7da8dd863e7907e67ee3031483", + "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.22-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" + ], + "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-01-22T09:19:47+00:00" + }, + { + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.22.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "43a0283138253ed1d48d352ab6d0bdb3f809f248" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/43a0283138253ed1d48d352ab6d0bdb3f809f248", + "reference": "43a0283138253ed1d48d352ab6d0bdb3f809f248", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.22-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" + ], + "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-01-22T09:19:47+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.22.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "5232de97ee3b75b0360528dae24e73db49566ab1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/5232de97ee3b75b0360528dae24e73db49566ab1", + "reference": "5232de97ee3b75b0360528dae24e73db49566ab1", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.22-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + }, + "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 for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "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-01-22T09:19:47+00:00" + }, + { + "name": "symfony/polyfill-php72", + "version": "v1.22.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php72.git", + "reference": "cc6e6f9b39fe8075b3dabfbaf5b5f645ae1340c9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/cc6e6f9b39fe8075b3dabfbaf5b5f645ae1340c9", + "reference": "cc6e6f9b39fe8075b3dabfbaf5b5f645ae1340c9", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.22-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "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" + ], + "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-01-07T16:49:33+00:00" + }, + { + "name": "symfony/polyfill-php73", + "version": "v1.22.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php73.git", + "reference": "a678b42e92f86eca04b7fa4c0f6f19d097fb69e2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/a678b42e92f86eca04b7fa4c0f6f19d097fb69e2", + "reference": "a678b42e92f86eca04b7fa4c0f6f19d097fb69e2", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.22-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Php73\\": "" + }, + "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 backporting some PHP 7.3+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "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-01-07T16:49:33+00:00" + }, + { + "name": "symfony/polyfill-php80", + "version": "v1.22.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "dc3063ba22c2a1fd2f45ed856374d79114998f91" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/dc3063ba22c2a1fd2f45ed856374d79114998f91", + "reference": "dc3063ba22c2a1fd2f45ed856374d79114998f91", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.22-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Php80\\": "" + }, + "files": [ + "bootstrap.php" + ], + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "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-01-07T16:49:33+00:00" + }, + { + "name": "symfony/property-access", + "version": "v4.4.20", + "source": { + "type": "git", + "url": "https://github.com/symfony/property-access.git", + "reference": "94a1d9837396c71a0d8c31686c16693a15651622" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/property-access/zipball/94a1d9837396c71a0d8c31686c16693a15651622", + "reference": "94a1d9837396c71a0d8c31686c16693a15651622", + "shasum": "" + }, + "require": { + "php": ">=7.1.3", + "symfony/inflector": "^3.4|^4.0|^5.0" + }, + "require-dev": { + "symfony/cache": "^3.4|^4.0|^5.0" + }, + "suggest": { + "psr/cache-implementation": "To cache access methods." + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\PropertyAccess\\": "" + }, + "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": "Provides functions to read and write from/to an object or array using a simple string notation", + "homepage": "https://symfony.com", + "keywords": [ + "access", + "array", + "extraction", + "index", + "injection", + "object", + "property", + "property path", + "reflection" + ], + "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-01-27T09:09:26+00:00" + }, + { + "name": "symfony/property-info", + "version": "v4.4.22", + "source": { + "type": "git", + "url": "https://github.com/symfony/property-info.git", + "reference": "a910e1a728b90797d9e549737c8fc640b99a2873" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/property-info/zipball/a910e1a728b90797d9e549737c8fc640b99a2873", + "reference": "a910e1a728b90797d9e549737c8fc640b99a2873", + "shasum": "" + }, + "require": { + "php": ">=7.1.3", + "symfony/inflector": "^3.4|^4.0|^5.0" + }, + "conflict": { + "phpdocumentor/reflection-docblock": "<3.0|>=3.2.0,<3.2.2", + "phpdocumentor/type-resolver": "<0.3.0|1.3.*", + "symfony/dependency-injection": "<3.4" + }, + "require-dev": { + "doctrine/annotations": "^1.10.4", + "phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0", + "symfony/cache": "^3.4|^4.0|^5.0", + "symfony/dependency-injection": "^3.4|^4.0|^5.0", + "symfony/serializer": "^3.4|^4.0|^5.0" + }, + "suggest": { + "phpdocumentor/reflection-docblock": "To use the PHPDoc", + "psr/cache-implementation": "To cache results", + "symfony/doctrine-bridge": "To use Doctrine metadata", + "symfony/serializer": "To use Serializer metadata" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\PropertyInfo\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Kévin Dunglas", + "email": "dunglas@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Extracts information about PHP class' properties using metadata of popular sources", + "homepage": "https://symfony.com", + "keywords": [ + "doctrine", + "phpdoc", + "property", + "symfony", + "type", + "validator" + ], + "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-04-13T22:23:01+00:00" + }, + { + "name": "symfony/routing", + "version": "v4.4.22", + "source": { + "type": "git", + "url": "https://github.com/symfony/routing.git", + "reference": "049e7c5c41f98511959668791b4adc0898a821b3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/routing/zipball/049e7c5c41f98511959668791b4adc0898a821b3", + "reference": "049e7c5c41f98511959668791b4adc0898a821b3", + "shasum": "" + }, + "require": { + "php": ">=7.1.3" + }, + "conflict": { + "symfony/config": "<4.2", + "symfony/dependency-injection": "<3.4", + "symfony/yaml": "<3.4" + }, + "require-dev": { + "doctrine/annotations": "^1.10.4", + "psr/log": "~1.0", + "symfony/config": "^4.2|^5.0", + "symfony/dependency-injection": "^3.4|^4.0|^5.0", + "symfony/expression-language": "^3.4|^4.0|^5.0", + "symfony/http-foundation": "^3.4|^4.0|^5.0", + "symfony/yaml": "^3.4|^4.0|^5.0" + }, + "suggest": { + "doctrine/annotations": "For using the annotation loader", + "symfony/config": "For using the all-in-one router or any loader", + "symfony/expression-language": "For using expression matching", + "symfony/http-foundation": "For using a Symfony Request object", + "symfony/yaml": "For using the YAML loader" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Routing\\": "" + }, + "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": "Maps an HTTP request to a set of configuration variables", + "homepage": "https://symfony.com", + "keywords": [ + "router", + "routing", + "uri", + "url" + ], + "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-04-11T12:59:39+00:00" + }, + { + "name": "symfony/security", + "version": "v4.4.22", + "source": { + "type": "git", + "url": "https://github.com/symfony/security.git", + "reference": "d697f9a70fd344e9d005920cefd8868e77347051" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/security/zipball/d697f9a70fd344e9d005920cefd8868e77347051", + "reference": "d697f9a70fd344e9d005920cefd8868e77347051", + "shasum": "" + }, + "require": { + "php": ">=7.1.3", + "symfony/event-dispatcher-contracts": "^1.1|^2", + "symfony/http-foundation": "^3.4.40|^4.4.7|^5.0.7", + "symfony/http-kernel": "^4.4", + "symfony/property-access": "^3.4|^4.0|^5.0", + "symfony/service-contracts": "^1.1|^2" + }, + "conflict": { + "symfony/event-dispatcher": ">=5", + "symfony/ldap": "<4.4" + }, + "replace": { + "symfony/security-core": "self.version", + "symfony/security-csrf": "self.version", + "symfony/security-guard": "self.version", + "symfony/security-http": "self.version" + }, + "require-dev": { + "psr/container": "^1.0|^2.0", + "psr/log": "~1.0", + "symfony/expression-language": "^3.4|^4.0|^5.0", + "symfony/finder": "^3.4|^4.0|^5.0", + "symfony/ldap": "^4.4|^5.0", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-intl-icu": "~1.0", + "symfony/routing": "^3.4|^4.0|^5.0", + "symfony/translation": "^4.4|^5.0", + "symfony/validator": "^3.4.31|^4.3.4|^5.0" + }, + "suggest": { + "psr/container-implementation": "To instantiate the Security class", + "symfony/expression-language": "For using the expression voter", + "symfony/form": "", + "symfony/ldap": "For using the LDAP user and authentication providers", + "symfony/routing": "For using the HttpUtils class to create sub-requests, redirect the user, and match URLs", + "symfony/validator": "For using the user password constraint" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Security\\": "" + }, + "exclude-from-classmap": [ + "/Core/Tests/", + "/Csrf/Tests/", + "/Guard/Tests/", + "/Http/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": "Provides a complete security system for your web application", + "homepage": "https://symfony.com", + "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-04-30T14:11:22+00:00" + }, + { + "name": "symfony/security-bundle", + "version": "v4.4.22", + "source": { + "type": "git", + "url": "https://github.com/symfony/security-bundle.git", + "reference": "e9cf8e6bd48d1dab8dc223ce4d30ff0d9340e1cd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/security-bundle/zipball/e9cf8e6bd48d1dab8dc223ce4d30ff0d9340e1cd", + "reference": "e9cf8e6bd48d1dab8dc223ce4d30ff0d9340e1cd", + "shasum": "" + }, + "require": { + "ext-xml": "*", + "php": ">=7.1.3", + "symfony/config": "^4.2|^5.0", + "symfony/dependency-injection": "^4.4|^5.0", + "symfony/http-kernel": "^4.4", + "symfony/security-core": "^4.4", + "symfony/security-csrf": "^4.2|^5.0", + "symfony/security-guard": "^4.2|^5.0", + "symfony/security-http": "^4.4.5" + }, + "conflict": { + "symfony/browser-kit": "<4.2", + "symfony/console": "<3.4", + "symfony/framework-bundle": "<4.4", + "symfony/ldap": "<4.4", + "symfony/twig-bundle": "<4.4" + }, + "require-dev": { + "doctrine/doctrine-bundle": "^1.5|^2.0", + "symfony/asset": "^3.4|^4.0|^5.0", + "symfony/browser-kit": "^4.2|^5.0", + "symfony/console": "^3.4|^4.0|^5.0", + "symfony/css-selector": "^3.4|^4.0|^5.0", + "symfony/dom-crawler": "^3.4|^4.0|^5.0", + "symfony/expression-language": "^3.4|^4.0|^5.0", + "symfony/form": "^3.4|^4.0|^5.0", + "symfony/framework-bundle": "^4.4|^5.0", + "symfony/process": "^3.4|^4.0|^5.0", + "symfony/serializer": "^4.4|^5.0", + "symfony/translation": "^3.4|^4.0|^5.0", + "symfony/twig-bridge": "^3.4|^4.0|^5.0", + "symfony/twig-bundle": "^4.4|^5.0", + "symfony/validator": "^3.4|^4.0|^5.0", + "symfony/yaml": "^3.4|^4.0|^5.0", + "twig/twig": "^1.43|^2.13|^3.0.4" + }, + "type": "symfony-bundle", + "autoload": { + "psr-4": { + "Symfony\\Bundle\\SecurityBundle\\": "" + }, + "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": "Provides a tight integration of the Security component into the Symfony full-stack framework", + "homepage": "https://symfony.com", + "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-04-12T14:25:43+00:00" + }, + { + "name": "symfony/serializer", + "version": "v4.4.22", + "source": { + "type": "git", + "url": "https://github.com/symfony/serializer.git", + "reference": "9db284ce4b1194797ad2ac6ad5406c5b416a9bb4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/serializer/zipball/9db284ce4b1194797ad2ac6ad5406c5b416a9bb4", + "reference": "9db284ce4b1194797ad2ac6ad5406c5b416a9bb4", + "shasum": "" + }, + "require": { + "php": ">=7.1.3", + "symfony/polyfill-ctype": "~1.8" + }, + "conflict": { + "phpdocumentor/reflection-docblock": "<3.0|>=3.2.0,<3.2.2", + "phpdocumentor/type-resolver": "<0.3.0|1.3.*", + "symfony/dependency-injection": "<3.4", + "symfony/property-access": "<3.4", + "symfony/property-info": "<3.4", + "symfony/yaml": "<3.4" + }, + "require-dev": { + "doctrine/annotations": "^1.10.4", + "doctrine/cache": "~1.0", + "phpdocumentor/reflection-docblock": "^3.2|^4.0|^5.0", + "symfony/cache": "^3.4|^4.0|^5.0", + "symfony/config": "^3.4|^4.0|^5.0", + "symfony/dependency-injection": "^3.4|^4.0|^5.0", + "symfony/error-handler": "^4.4|^5.0", + "symfony/http-foundation": "^3.4|^4.0|^5.0", + "symfony/mime": "^4.4|^5.0", + "symfony/property-access": "^3.4.41|^4.4.9|^5.0.9", + "symfony/property-info": "^3.4.13|~4.0|^5.0", + "symfony/validator": "^3.4|^4.0|^5.0", + "symfony/yaml": "^3.4|^4.0|^5.0" + }, + "suggest": { + "doctrine/annotations": "For using the annotation mapping. You will also need doctrine/cache.", + "doctrine/cache": "For using the default cached annotation reader and metadata cache.", + "psr/cache-implementation": "For using the metadata cache.", + "symfony/config": "For using the XML mapping loader.", + "symfony/http-foundation": "For using a MIME type guesser within the DataUriNormalizer.", + "symfony/property-access": "For using the ObjectNormalizer.", + "symfony/property-info": "To deserialize relations.", + "symfony/yaml": "For using the default YAML mapping loader." + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Serializer\\": "" + }, + "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": "Handles serializing and deserializing data structures, including object graphs, into array structures or other formats like XML and JSON.", + "homepage": "https://symfony.com", + "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-04-13T06:32:41+00:00" + }, + { + "name": "symfony/serializer-pack", + "version": "v1.0.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/serializer-pack.git", + "reference": "61173947057d5e1bf1c79e2a6ab6a8430be0602e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/serializer-pack/zipball/61173947057d5e1bf1c79e2a6ab6a8430be0602e", + "reference": "61173947057d5e1bf1c79e2a6ab6a8430be0602e", + "shasum": "" + }, + "require": { + "doctrine/annotations": "^1.0", + "phpdocumentor/reflection-docblock": "*", + "symfony/property-access": "*", + "symfony/property-info": "*", + "symfony/serializer": "*" + }, + "type": "symfony-pack", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A pack for the Symfony serializer", + "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": "2020-10-19T08:52:16+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "v2.4.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "f040a30e04b57fbcc9c6cbcf4dbaa96bd318b9bb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/f040a30e04b57fbcc9c6cbcf4dbaa96bd318b9bb", + "reference": "f040a30e04b57fbcc9c6cbcf4dbaa96bd318b9bb", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "psr/container": "^1.1" + }, + "suggest": { + "symfony/service-implementation": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.4-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Service\\": "" + } + }, + "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": "Generic abstractions related to writing services", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "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-04-01T10:43:52+00:00" + }, + { + "name": "symfony/stopwatch", + "version": "v4.4.20", + "source": { + "type": "git", + "url": "https://github.com/symfony/stopwatch.git", + "reference": "c5572f6494fc20668a73b77684d8dc77e534d8cf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/stopwatch/zipball/c5572f6494fc20668a73b77684d8dc77e534d8cf", + "reference": "c5572f6494fc20668a73b77684d8dc77e534d8cf", + "shasum": "" + }, + "require": { + "php": ">=7.1.3", + "symfony/service-contracts": "^1.0|^2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Stopwatch\\": "" + }, + "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": "Provides a way to profile code", + "homepage": "https://symfony.com", + "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-01-27T09:09:26+00:00" + }, + { + "name": "symfony/translation", + "version": "v4.4.21", + "source": { + "type": "git", + "url": "https://github.com/symfony/translation.git", + "reference": "eb8f5428cc3b40d6dffe303b195b084f1c5fbd14" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/translation/zipball/eb8f5428cc3b40d6dffe303b195b084f1c5fbd14", + "reference": "eb8f5428cc3b40d6dffe303b195b084f1c5fbd14", + "shasum": "" + }, + "require": { + "php": ">=7.1.3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/translation-contracts": "^1.1.6|^2" + }, + "conflict": { + "symfony/config": "<3.4", + "symfony/dependency-injection": "<3.4", + "symfony/http-kernel": "<4.4", + "symfony/yaml": "<3.4" + }, + "provide": { + "symfony/translation-implementation": "1.0|2.0" + }, + "require-dev": { + "psr/log": "~1.0", + "symfony/config": "^3.4|^4.0|^5.0", + "symfony/console": "^3.4|^4.0|^5.0", + "symfony/dependency-injection": "^3.4|^4.0|^5.0", + "symfony/finder": "~2.8|~3.0|~4.0|^5.0", + "symfony/http-kernel": "^4.4", + "symfony/intl": "^3.4|^4.0|^5.0", + "symfony/service-contracts": "^1.1.2|^2", + "symfony/yaml": "^3.4|^4.0|^5.0" + }, + "suggest": { + "psr/log-implementation": "To use logging capability in translator", + "symfony/config": "", + "symfony/yaml": "" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Translation\\": "" + }, + "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": "Provides tools to internationalize your application", + "homepage": "https://symfony.com", + "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-03-23T16:25:01+00:00" + }, + { + "name": "symfony/translation-contracts", + "version": "v2.4.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/translation-contracts.git", + "reference": "95c812666f3e91db75385749fe219c5e494c7f95" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/95c812666f3e91db75385749fe219c5e494c7f95", + "reference": "95c812666f3e91db75385749fe219c5e494c7f95", + "shasum": "" + }, + "require": { + "php": ">=7.2.5" + }, + "suggest": { + "symfony/translation-implementation": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.4-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Translation\\": "" + } + }, + "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": "Generic abstractions related to translation", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "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-03-23T23:28:01+00:00" + }, + { + "name": "symfony/twig-bridge", + "version": "v4.4.22", + "source": { + "type": "git", + "url": "https://github.com/symfony/twig-bridge.git", + "reference": "48b4ae9cf1b42d37710ea5857770c13f0b9d5579" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/twig-bridge/zipball/48b4ae9cf1b42d37710ea5857770c13f0b9d5579", + "reference": "48b4ae9cf1b42d37710ea5857770c13f0b9d5579", + "shasum": "" + }, + "require": { + "php": ">=7.1.3", + "symfony/translation-contracts": "^1.1|^2", + "twig/twig": "^1.43|^2.13|^3.0.4" + }, + "conflict": { + "symfony/console": "<3.4", + "symfony/form": "<4.4", + "symfony/http-foundation": "<4.3", + "symfony/translation": "<4.2", + "symfony/workflow": "<4.3" + }, + "require-dev": { + "egulias/email-validator": "^2.1.10|^3", + "symfony/asset": "^3.4|^4.0|^5.0", + "symfony/console": "^3.4|^4.0|^5.0", + "symfony/dependency-injection": "^3.4|^4.0|^5.0", + "symfony/error-handler": "^4.4|^5.0", + "symfony/expression-language": "^3.4|^4.0|^5.0", + "symfony/finder": "^3.4|^4.0|^5.0", + "symfony/form": "^4.4.17", + "symfony/http-foundation": "^4.3|^5.0", + "symfony/http-kernel": "^4.4", + "symfony/intl": "^4.4|^5.0", + "symfony/mime": "^4.3|^5.0", + "symfony/polyfill-intl-icu": "~1.0", + "symfony/routing": "^3.4|^4.0|^5.0", + "symfony/security-acl": "^2.8|^3.0", + "symfony/security-core": "^3.0|^4.0|^5.0", + "symfony/security-csrf": "^3.4|^4.0|^5.0", + "symfony/security-http": "^3.4|^4.0|^5.0", + "symfony/stopwatch": "^3.4|^4.0|^5.0", + "symfony/templating": "^3.4|^4.0|^5.0", + "symfony/translation": "^4.2.1|^5.0", + "symfony/web-link": "^4.4|^5.0", + "symfony/workflow": "^4.3|^5.0", + "symfony/yaml": "^3.4|^4.0|^5.0", + "twig/cssinliner-extra": "^2.12|^3", + "twig/inky-extra": "^2.12|^3", + "twig/markdown-extra": "^2.12|^3" + }, + "suggest": { + "symfony/asset": "For using the AssetExtension", + "symfony/expression-language": "For using the ExpressionExtension", + "symfony/finder": "", + "symfony/form": "For using the FormExtension", + "symfony/http-kernel": "For using the HttpKernelExtension", + "symfony/routing": "For using the RoutingExtension", + "symfony/security-core": "For using the SecurityExtension", + "symfony/security-csrf": "For using the CsrfExtension", + "symfony/security-http": "For using the LogoutUrlExtension", + "symfony/stopwatch": "For using the StopwatchExtension", + "symfony/templating": "For using the TwigEngine", + "symfony/translation": "For using the TranslationExtension", + "symfony/var-dumper": "For using the DumpExtension", + "symfony/web-link": "For using the WebLinkExtension", + "symfony/yaml": "For using the YamlExtension" + }, + "type": "symfony-bridge", + "autoload": { + "psr-4": { + "Symfony\\Bridge\\Twig\\": "" + }, + "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": "Provides integration for Twig with various Symfony components", + "homepage": "https://symfony.com", + "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-04-07T15:47:03+00:00" + }, + { + "name": "symfony/twig-bundle", + "version": "v4.4.20", + "source": { + "type": "git", + "url": "https://github.com/symfony/twig-bundle.git", + "reference": "7cee73b45e3bd963a0ab4184f1041dcdc85b6e86" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/twig-bundle/zipball/7cee73b45e3bd963a0ab4184f1041dcdc85b6e86", + "reference": "7cee73b45e3bd963a0ab4184f1041dcdc85b6e86", + "shasum": "" + }, + "require": { + "php": ">=7.1.3", + "symfony/http-foundation": "^4.3|^5.0", + "symfony/http-kernel": "^4.4", + "symfony/polyfill-ctype": "~1.8", + "symfony/twig-bridge": "^4.4|^5.0", + "twig/twig": "^1.43|^2.13|^3.0.4" + }, + "conflict": { + "symfony/dependency-injection": "<4.1", + "symfony/framework-bundle": "<4.4", + "symfony/translation": "<4.2" + }, + "require-dev": { + "doctrine/annotations": "^1.10.4", + "doctrine/cache": "~1.0", + "symfony/asset": "^3.4|^4.0|^5.0", + "symfony/dependency-injection": "^4.2.5|^5.0", + "symfony/expression-language": "^3.4|^4.0|^5.0", + "symfony/finder": "^3.4|^4.0|^5.0", + "symfony/form": "^3.4|^4.0|^5.0", + "symfony/framework-bundle": "^4.4|^5.0", + "symfony/routing": "^3.4|^4.0|^5.0", + "symfony/stopwatch": "^3.4|^4.0|^5.0", + "symfony/templating": "^3.4|^4.0|^5.0", + "symfony/translation": "^4.2|^5.0", + "symfony/web-link": "^3.4|^4.0|^5.0", + "symfony/yaml": "^3.4|^4.0|^5.0" + }, + "type": "symfony-bundle", + "autoload": { + "psr-4": { + "Symfony\\Bundle\\TwigBundle\\": "" + }, + "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": "Provides a tight integration of Twig into the Symfony full-stack framework", + "homepage": "https://symfony.com", + "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-01-27T09:09:26+00:00" + }, + { + "name": "symfony/validator", + "version": "v4.4.22", + "source": { + "type": "git", + "url": "https://github.com/symfony/validator.git", + "reference": "65525b93ebc48c2992271f435e1391bbb049367a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/validator/zipball/65525b93ebc48c2992271f435e1391bbb049367a", + "reference": "65525b93ebc48c2992271f435e1391bbb049367a", + "shasum": "" + }, + "require": { + "php": ">=7.1.3", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.0", + "symfony/translation-contracts": "^1.1|^2" + }, + "conflict": { + "doctrine/lexer": "<1.0.2", + "phpunit/phpunit": "<4.8.35|<5.4.3,>=5.0", + "symfony/dependency-injection": "<3.4", + "symfony/http-kernel": "<4.4", + "symfony/intl": "<4.3", + "symfony/translation": ">=5.0", + "symfony/yaml": "<3.4" + }, + "require-dev": { + "doctrine/annotations": "^1.10.4", + "doctrine/cache": "~1.0", + "egulias/email-validator": "^2.1.10|^3", + "symfony/cache": "^3.4|^4.0|^5.0", + "symfony/config": "^3.4|^4.0|^5.0", + "symfony/dependency-injection": "^3.4|^4.0|^5.0", + "symfony/expression-language": "^3.4|^4.0|^5.0", + "symfony/http-client": "^4.3|^5.0", + "symfony/http-foundation": "^4.1|^5.0", + "symfony/http-kernel": "^4.4", + "symfony/intl": "^4.3|^5.0", + "symfony/mime": "^4.4|^5.0", + "symfony/property-access": "^3.4|^4.0|^5.0", + "symfony/property-info": "^3.4|^4.0|^5.0", + "symfony/translation": "^4.2", + "symfony/yaml": "^3.4|^4.0|^5.0" + }, + "suggest": { + "doctrine/annotations": "For using the annotation mapping. You will also need doctrine/cache.", + "doctrine/cache": "For using the default cached annotation reader.", + "egulias/email-validator": "Strict (RFC compliant) email validation", + "psr/cache-implementation": "For using the mapping cache.", + "symfony/config": "", + "symfony/expression-language": "For using the Expression validator", + "symfony/http-foundation": "", + "symfony/intl": "", + "symfony/property-access": "For accessing properties within comparison constraints", + "symfony/property-info": "To automatically add NotNull and Type constraints", + "symfony/translation": "For translating validation errors.", + "symfony/yaml": "" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Validator\\": "" + }, + "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": "Provides tools to validate values", + "homepage": "https://symfony.com", + "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-04-14T09:41:13+00:00" + }, + { + "name": "symfony/var-dumper", + "version": "v4.4.22", + "source": { + "type": "git", + "url": "https://github.com/symfony/var-dumper.git", + "reference": "c194bcedde6295f3ec3e9eba1f5d484ea97c41a7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/c194bcedde6295f3ec3e9eba1f5d484ea97c41a7", + "reference": "c194bcedde6295f3ec3e9eba1f5d484ea97c41a7", + "shasum": "" + }, + "require": { + "php": ">=7.1.3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/polyfill-php72": "~1.5", + "symfony/polyfill-php80": "^1.15" + }, + "conflict": { + "phpunit/phpunit": "<4.8.35|<5.4.3,>=5.0", + "symfony/console": "<3.4" + }, + "require-dev": { + "ext-iconv": "*", + "symfony/console": "^3.4|^4.0|^5.0", + "symfony/process": "^4.4|^5.0", + "twig/twig": "^1.43|^2.13|^3.0.4" + }, + "suggest": { + "ext-iconv": "To convert non-UTF-8 strings to UTF-8 (or symfony/polyfill-iconv in case ext-iconv cannot be used).", + "ext-intl": "To show region name in time zone dump", + "symfony/console": "To use the ServerDumpCommand and/or the bin/var-dump-server script" + }, + "bin": [ + "Resources/bin/var-dump-server" + ], + "type": "library", + "autoload": { + "files": [ + "Resources/functions/dump.php" + ], + "psr-4": { + "Symfony\\Component\\VarDumper\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "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": "Provides mechanisms for walking through any arbitrary PHP variable", + "homepage": "https://symfony.com", + "keywords": [ + "debug", + "dump" + ], + "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-04-19T13:36:17+00:00" + }, + { + "name": "symfony/var-exporter", + "version": "v4.4.22", + "source": { + "type": "git", + "url": "https://github.com/symfony/var-exporter.git", + "reference": "ef3054c7e878fe0837ef9ac2c5ecfddfd27dd9e9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/var-exporter/zipball/ef3054c7e878fe0837ef9ac2c5ecfddfd27dd9e9", + "reference": "ef3054c7e878fe0837ef9ac2c5ecfddfd27dd9e9", + "shasum": "" + }, + "require": { + "php": ">=7.1.3" + }, + "require-dev": { + "symfony/var-dumper": "^4.4.9|^5.0.9" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\VarExporter\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "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": "Allows exporting any serializable PHP data structure to plain PHP code", + "homepage": "https://symfony.com", + "keywords": [ + "clone", + "construct", + "export", + "hydrate", + "instantiate", + "serialize" + ], + "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-04-01T10:24:12+00:00" + }, + { + "name": "symfony/yaml", + "version": "v4.4.22", + "source": { + "type": "git", + "url": "https://github.com/symfony/yaml.git", + "reference": "1c2fd24147961525eaefb65b11987cab75adab59" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/yaml/zipball/1c2fd24147961525eaefb65b11987cab75adab59", + "reference": "1c2fd24147961525eaefb65b11987cab75adab59", + "shasum": "" + }, + "require": { + "php": ">=7.1.3", + "symfony/polyfill-ctype": "~1.8" + }, + "conflict": { + "symfony/console": "<3.4" + }, + "require-dev": { + "symfony/console": "^3.4|^4.0|^5.0" + }, + "suggest": { + "symfony/console": "For validating YAML files using the lint command" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Yaml\\": "" + }, + "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": "Loads and dumps YAML files", + "homepage": "https://symfony.com", + "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-04-23T12:09:37+00:00" + }, + { + "name": "symfonycasts/reset-password-bundle", + "version": "v1.7.0", + "source": { + "type": "git", + "url": "https://github.com/SymfonyCasts/reset-password-bundle.git", + "reference": "368a4f64fc4f8174234fc91f9b22d5016257211b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/SymfonyCasts/reset-password-bundle/zipball/368a4f64fc4f8174234fc91f9b22d5016257211b", + "reference": "368a4f64fc4f8174234fc91f9b22d5016257211b", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/config": "^4.4 | ^5.0", + "symfony/dependency-injection": "^4.4 | ^5.0", + "symfony/deprecation-contracts": "^2.2", + "symfony/http-kernel": "^4.4 | ^5.0" + }, + "conflict": { + "doctrine/orm": "<2.7", + "symfony/framework-bundle": "<4.4", + "symfony/http-foundation": "<4.4" + }, + "require-dev": { + "doctrine/doctrine-bundle": "^2.0.3", + "doctrine/orm": "^2.7", + "friendsofphp/php-cs-fixer": "^2.17", + "symfony/framework-bundle": "^4.4 | ^5.0", + "symfony/phpunit-bridge": "^5.0", + "vimeo/psalm": "^4.3" + }, + "type": "symfony-bundle", + "autoload": { + "psr-4": { + "SymfonyCasts\\Bundle\\ResetPassword\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Symfony bundle that adds password reset functionality.", + "time": "2021-04-12T17:29:47+00:00" + }, + { + "name": "twig/twig", + "version": "v3.3.0", + "source": { + "type": "git", + "url": "https://github.com/twigphp/Twig.git", + "reference": "1f3b7e2c06cc05d42936a8ad508ff1db7975cdc5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/twigphp/Twig/zipball/1f3b7e2c06cc05d42936a8ad508ff1db7975cdc5", + "reference": "1f3b7e2c06cc05d42936a8ad508ff1db7975cdc5", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/polyfill-ctype": "^1.8", + "symfony/polyfill-mbstring": "^1.3" + }, + "require-dev": { + "psr/container": "^1.0", + "symfony/phpunit-bridge": "^4.4.9|^5.0.9" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.3-dev" + } + }, + "autoload": { + "psr-4": { + "Twig\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com", + "homepage": "http://fabien.potencier.org", + "role": "Lead Developer" + }, + { + "name": "Twig Team", + "role": "Contributors" + }, + { + "name": "Armin Ronacher", + "email": "armin.ronacher@active-4.com", + "role": "Project Founder" + } + ], + "description": "Twig, the flexible, fast, and secure template language for PHP", + "homepage": "https://twig.symfony.com", + "keywords": [ + "templating" + ], + "funding": [ + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/twig/twig", + "type": "tidelift" + } + ], + "time": "2021-02-08T09:54:36+00:00" + }, + { + "name": "webmozart/assert", + "version": "1.10.0", + "source": { + "type": "git", + "url": "https://github.com/webmozarts/assert.git", + "reference": "6964c76c7804814a842473e0c8fd15bab0f18e25" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webmozarts/assert/zipball/6964c76c7804814a842473e0c8fd15bab0f18e25", + "reference": "6964c76c7804814a842473e0c8fd15bab0f18e25", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "phpstan/phpstan": "<0.12.20", + "vimeo/psalm": "<4.6.1 || 4.6.2" + }, + "require-dev": { + "phpunit/phpunit": "^8.5.13" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.10-dev" + } + }, + "autoload": { + "psr-4": { + "Webmozart\\Assert\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Assertions to validate method input/output with nice error messages.", + "keywords": [ + "assert", + "check", + "validate" + ], + "time": "2021-03-09T10:59:23+00:00" + } + ], + "packages-dev": [ + { + "name": "doctrine/data-fixtures", + "version": "1.5.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/data-fixtures.git", + "reference": "51d3d4880d28951fff42a635a2389f8c63baddc5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/data-fixtures/zipball/51d3d4880d28951fff42a635a2389f8c63baddc5", + "reference": "51d3d4880d28951fff42a635a2389f8c63baddc5", + "shasum": "" + }, + "require": { + "doctrine/common": "^2.13|^3.0", + "doctrine/persistence": "^1.3.3|^2.0", + "php": "^7.2 || ^8.0" + }, + "conflict": { + "doctrine/phpcr-odm": "<1.3.0" + }, + "require-dev": { + "doctrine/coding-standard": "^8.2", + "doctrine/dbal": "^2.5.4", + "doctrine/mongodb-odm": "^1.3.0 || ^2.0.0", + "doctrine/orm": "^2.7.0", + "ext-sqlite3": "*", + "phpunit/phpunit": "^8.0" + }, + "suggest": { + "alcaeus/mongo-php-adapter": "For using MongoDB ODM 1.3 with PHP 7 (deprecated)", + "doctrine/mongodb-odm": "For loading MongoDB ODM fixtures", + "doctrine/orm": "For loading ORM fixtures", + "doctrine/phpcr-odm": "For loading PHPCR ODM fixtures" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Common\\DataFixtures\\": "lib/Doctrine/Common/DataFixtures" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + } + ], + "description": "Data Fixtures for all Doctrine Object Managers", + "homepage": "http://www.doctrine-project.org", + "keywords": [ + "database" + ], + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fdata-fixtures", + "type": "tidelift" + } + ], + "time": "2021-01-23T10:20:43+00:00" + }, + { + "name": "doctrine/doctrine-fixtures-bundle", + "version": "3.4.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/DoctrineFixturesBundle.git", + "reference": "870189619a7770f468ffb0b80925302e065a3b34" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/DoctrineFixturesBundle/zipball/870189619a7770f468ffb0b80925302e065a3b34", + "reference": "870189619a7770f468ffb0b80925302e065a3b34", + "shasum": "" + }, + "require": { + "doctrine/data-fixtures": "^1.3", + "doctrine/doctrine-bundle": "^1.11|^2.0", + "doctrine/orm": "^2.6.0", + "doctrine/persistence": "^1.3.7|^2.0", + "php": "^7.1 || ^8.0", + "symfony/config": "^3.4|^4.3|^5.0", + "symfony/console": "^3.4|^4.3|^5.0", + "symfony/dependency-injection": "^3.4|^4.3|^5.0", + "symfony/doctrine-bridge": "^3.4|^4.1|^5.0", + "symfony/http-kernel": "^3.4|^4.3|^5.0" + }, + "require-dev": { + "doctrine/coding-standard": "^6.0", + "phpunit/phpunit": "^7.4 || ^8.0 || ^9.2", + "symfony/phpunit-bridge": "^4.1|^5.0" + }, + "type": "symfony-bundle", + "autoload": { + "psr-4": { + "Doctrine\\Bundle\\FixturesBundle\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Doctrine Project", + "homepage": "http://www.doctrine-project.org" + }, + { + "name": "Symfony Community", + "homepage": "http://symfony.com/contributors" + } + ], + "description": "Symfony DoctrineFixturesBundle", + "homepage": "http://www.doctrine-project.org", + "keywords": [ + "Fixture", + "persistence" + ], + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fdoctrine-fixtures-bundle", + "type": "tidelift" + } + ], + "time": "2020-11-14T09:36:49+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v4.10.5", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "4432ba399e47c66624bc73c8c0f811e5c109576f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/4432ba399e47c66624bc73c8c0f811e5c109576f", + "reference": "4432ba399e47c66624bc73c8c0f811e5c109576f", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": ">=7.0" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^6.5 || ^7.0 || ^8.0 || ^9.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.9-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "time": "2021-05-03T19:11:20+00:00" + }, + { + "name": "phpstan/phpstan", + "version": "0.12.85", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpstan.git", + "reference": "20e6333c0067875ad7697cd8acdf245c6ef69d03" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/20e6333c0067875ad7697cd8acdf245c6ef69d03", + "reference": "20e6333c0067875ad7697cd8acdf245c6ef69d03", + "shasum": "" + }, + "require": { + "php": "^7.1|^8.0" + }, + "conflict": { + "phpstan/phpstan-shim": "*" + }, + "bin": [ + "phpstan", + "phpstan.phar" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "0.12-dev" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPStan - PHP Static Analysis Tool", + "funding": [ + { + "url": "https://github.com/ondrejmirtes", + "type": "github" + }, + { + "url": "https://www.patreon.com/phpstan", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpstan/phpstan", + "type": "tidelift" + } + ], + "time": "2021-04-27T14:13:16+00:00" + }, + { + "name": "phpstan/phpstan-deprecation-rules", + "version": "0.12.6", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpstan-deprecation-rules.git", + "reference": "46dbd43c2db973d2876d6653e53f5c2cc3a01fbb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan-deprecation-rules/zipball/46dbd43c2db973d2876d6653e53f5c2cc3a01fbb", + "reference": "46dbd43c2db973d2876d6653e53f5c2cc3a01fbb", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0", + "phpstan/phpstan": "^0.12.60" + }, + "require-dev": { + "phing/phing": "^2.16.3", + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/phpstan-phpunit": "^0.12", + "phpunit/phpunit": "^7.5.20" + }, + "type": "phpstan-extension", + "extra": { + "branch-alias": { + "dev-master": "0.12-dev" + }, + "phpstan": { + "includes": [ + "rules.neon" + ] + } + }, + "autoload": { + "psr-4": { + "PHPStan\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPStan rules for detecting usage of deprecated classes, methods, properties, constants and traits.", + "time": "2020-12-13T10:20:54+00:00" + }, + { + "name": "phpstan/phpstan-doctrine", + "version": "0.12.33", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpstan-doctrine.git", + "reference": "b76c21e7b85498399ba4a0147920ff413503e77a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan-doctrine/zipball/b76c21e7b85498399ba4a0147920ff413503e77a", + "reference": "b76c21e7b85498399ba4a0147920ff413503e77a", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0", + "phpstan/phpstan": "^0.12.56" + }, + "conflict": { + "doctrine/collections": "<1.0", + "doctrine/common": "<2.7", + "doctrine/mongodb-odm": "<1.2", + "doctrine/orm": "<2.5", + "doctrine/persistence": "<1.3" + }, + "require-dev": { + "doctrine/annotations": "^1.11.0", + "doctrine/collections": "^1.6", + "doctrine/common": "^2.7 || ^3.0", + "doctrine/dbal": "^2.11.0", + "doctrine/mongodb-odm": "^1.3 || ^2.1", + "doctrine/orm": "^2.5", + "doctrine/persistence": "^1.1 || ^2.0", + "phing/phing": "^2.16.3", + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/phpstan-phpunit": "^0.12.16", + "phpstan/phpstan-strict-rules": "^0.12.5", + "phpunit/phpunit": "^7.5.20", + "ramsey/uuid-doctrine": "^1.5.0" + }, + "type": "phpstan-extension", + "extra": { + "branch-alias": { + "dev-master": "0.12-dev" + }, + "phpstan": { + "includes": [ + "extension.neon", + "rules.neon" + ] + } + }, + "autoload": { + "psr-4": { + "PHPStan\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Doctrine extensions for PHPStan", + "time": "2021-03-07T12:28:23+00:00" + }, + { + "name": "phpstan/phpstan-phpunit", + "version": "0.12.18", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpstan-phpunit.git", + "reference": "ab44aec7cfb5cb267b8bc30a8caea86dd50d1f72" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan-phpunit/zipball/ab44aec7cfb5cb267b8bc30a8caea86dd50d1f72", + "reference": "ab44aec7cfb5cb267b8bc30a8caea86dd50d1f72", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0", + "phpstan/phpstan": "^0.12.60" + }, + "conflict": { + "phpunit/phpunit": "<7.0" + }, + "require-dev": { + "phing/phing": "^2.16.3", + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/phpstan-strict-rules": "^0.12.6", + "phpunit/phpunit": "^7.5.20" + }, + "type": "phpstan-extension", + "extra": { + "branch-alias": { + "dev-master": "0.12-dev" + }, + "phpstan": { + "includes": [ + "extension.neon", + "rules.neon" + ] + } + }, + "autoload": { + "psr-4": { + "PHPStan\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPUnit extensions and rules for PHPStan", + "time": "2021-03-06T11:51:27+00:00" + }, + { + "name": "phpstan/phpstan-strict-rules", + "version": "0.12.9", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpstan-strict-rules.git", + "reference": "0705fefc7c9168529fd130e341428f5f10f4f01d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan-strict-rules/zipball/0705fefc7c9168529fd130e341428f5f10f4f01d", + "reference": "0705fefc7c9168529fd130e341428f5f10f4f01d", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0", + "phpstan/phpstan": "^0.12.66" + }, + "require-dev": { + "phing/phing": "^2.16.3", + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/phpstan-phpunit": "^0.12.16", + "phpunit/phpunit": "^7.5.20" + }, + "type": "phpstan-extension", + "extra": { + "branch-alias": { + "dev-master": "0.12-dev" + }, + "phpstan": { + "includes": [ + "rules.neon" + ] + } + }, + "autoload": { + "psr-4": { + "PHPStan\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Extra strict and opinionated rules for PHPStan", + "time": "2021-01-13T08:50:28+00:00" + }, + { + "name": "phpstan/phpstan-symfony", + "version": "0.12.32", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpstan-symfony.git", + "reference": "f3fb189151b3a2cd99dbc5e7ae2b493d99111bf7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan-symfony/zipball/f3fb189151b3a2cd99dbc5e7ae2b493d99111bf7", + "reference": "f3fb189151b3a2cd99dbc5e7ae2b493d99111bf7", + "shasum": "" + }, + "require": { + "ext-simplexml": "*", + "php": "^7.1 || ^8.0", + "phpstan/phpstan": "^0.12.85" + }, + "conflict": { + "symfony/framework-bundle": "<3.0" + }, + "require-dev": { + "phing/phing": "^2.16.3", + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/phpstan-phpunit": "^0.12.16", + "phpstan/phpstan-strict-rules": "^0.12.5", + "phpunit/phpunit": "^7.5.20", + "symfony/config": "^4.2 || ^5.0", + "symfony/console": "^4.0 || ^5.0", + "symfony/framework-bundle": "^4.4 || ^5.0", + "symfony/http-foundation": "^4.0 || ^5.0", + "symfony/messenger": "^4.2 || ^5.0", + "symfony/serializer": "^4.0 || ^5.0" + }, + "type": "phpstan-extension", + "extra": { + "branch-alias": { + "dev-master": "0.12-dev" + }, + "phpstan": { + "includes": [ + "extension.neon", + "rules.neon" + ] + } + }, + "autoload": { + "psr-4": { + "PHPStan\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Lukáš Unger", + "email": "looky.msc@gmail.com", + "homepage": "https://lookyman.net" + } + ], + "description": "Symfony Framework extensions and rules for PHPStan", + "time": "2021-05-04T17:47:30+00:00" + }, + { + "name": "phpstan/phpstan-webmozart-assert", + "version": "0.12.12", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpstan-webmozart-assert.git", + "reference": "1657403194a43f83b5355bd400aa7fc63f0e6857" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan-webmozart-assert/zipball/1657403194a43f83b5355bd400aa7fc63f0e6857", + "reference": "1657403194a43f83b5355bd400aa7fc63f0e6857", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0", + "phpstan/phpstan": "^0.12.49" + }, + "require-dev": { + "phing/phing": "^2.16.3", + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/phpstan-phpunit": "^0.12.6", + "phpstan/phpstan-strict-rules": "^0.12.1", + "phpunit/phpunit": "^7.5.20", + "webmozart/assert": "^1.9.1" + }, + "type": "phpstan-extension", + "extra": { + "branch-alias": { + "dev-master": "0.12-dev" + }, + "phpstan": { + "includes": [ + "extension.neon" + ] + } + }, + "autoload": { + "psr-4": { + "PHPStan\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPStan webmozart/assert extension", + "time": "2021-01-31T12:24:28+00:00" + }, + { + "name": "symfony/browser-kit", + "version": "v4.4.22", + "source": { + "type": "git", + "url": "https://github.com/symfony/browser-kit.git", + "reference": "4c8b42b4aae93517e8f67d68c5cbe69413e3e3c1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/browser-kit/zipball/4c8b42b4aae93517e8f67d68c5cbe69413e3e3c1", + "reference": "4c8b42b4aae93517e8f67d68c5cbe69413e3e3c1", + "shasum": "" + }, + "require": { + "php": ">=7.1.3", + "symfony/dom-crawler": "^3.4|^4.0|^5.0" + }, + "require-dev": { + "symfony/css-selector": "^3.4|^4.0|^5.0", + "symfony/http-client": "^4.3|^5.0", + "symfony/mime": "^4.3|^5.0", + "symfony/process": "^3.4|^4.0|^5.0" + }, + "suggest": { + "symfony/process": "" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\BrowserKit\\": "" + }, + "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": "Simulates the behavior of a web browser, allowing you to make requests, click on links and submit forms programmatically", + "homepage": "https://symfony.com", + "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-04-08T07:40:10+00:00" + }, + { + "name": "symfony/css-selector", + "version": "v4.4.22", + "source": { + "type": "git", + "url": "https://github.com/symfony/css-selector.git", + "reference": "01c77324d1d47efbfd7891f62a7c256c69330115" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/01c77324d1d47efbfd7891f62a7c256c69330115", + "reference": "01c77324d1d47efbfd7891f62a7c256c69330115", + "shasum": "" + }, + "require": { + "php": ">=7.1.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\CssSelector\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Jean-François Simon", + "email": "jeanfrancois.simon@sensiolabs.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Converts CSS selectors to XPath expressions", + "homepage": "https://symfony.com", + "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-04-07T15:47:03+00:00" + }, + { + "name": "symfony/dom-crawler", + "version": "v4.4.20", + "source": { + "type": "git", + "url": "https://github.com/symfony/dom-crawler.git", + "reference": "be133557f1b0e6672367325b508e65da5513a311" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/be133557f1b0e6672367325b508e65da5513a311", + "reference": "be133557f1b0e6672367325b508e65da5513a311", + "shasum": "" + }, + "require": { + "php": ">=7.1.3", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "masterminds/html5": "<2.6" + }, + "require-dev": { + "masterminds/html5": "^2.6", + "symfony/css-selector": "^3.4|^4.0|^5.0" + }, + "suggest": { + "symfony/css-selector": "" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\DomCrawler\\": "" + }, + "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": "Eases DOM navigation for HTML and XML documents", + "homepage": "https://symfony.com", + "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-14T12:29:41+00:00" + }, + { + "name": "symfony/maker-bundle", + "version": "v1.30.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/maker-bundle.git", + "reference": "a395a85aa4ded6c1fa3da118d60329b64b6c2acd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/maker-bundle/zipball/a395a85aa4ded6c1fa3da118d60329b64b6c2acd", + "reference": "a395a85aa4ded6c1fa3da118d60329b64b6c2acd", + "shasum": "" + }, + "require": { + "doctrine/inflector": "^1.2|^2.0", + "nikic/php-parser": "^4.0", + "php": ">=7.1.3", + "symfony/config": "^4.0|^5.0", + "symfony/console": "^4.0|^5.0", + "symfony/dependency-injection": "^4.0|^5.0", + "symfony/deprecation-contracts": "^2.2", + "symfony/filesystem": "^4.0|^5.0", + "symfony/finder": "^4.0|^5.0", + "symfony/framework-bundle": "^4.0|^5.0", + "symfony/http-kernel": "^4.0|^5.0" + }, + "require-dev": { + "composer/semver": "^3.0@dev", + "doctrine/doctrine-bundle": "^1.8|^2.0", + "doctrine/orm": "^2.3", + "friendsofphp/php-cs-fixer": "^2.8", + "friendsoftwig/twigcs": "^4.1.0|^5.0.0", + "symfony/http-client": "^4.3|^5.0", + "symfony/phpunit-bridge": "^4.3|^5.0", + "symfony/process": "^4.0|^5.0", + "symfony/security-core": "^4.0|^5.0", + "symfony/yaml": "^4.0|^5.0" + }, + "type": "symfony-bundle", + "extra": { + "branch-alias": { + "dev-main": "1.0-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Bundle\\MakerBundle\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Maker helps you create empty commands, controllers, form classes, tests and more so you can forget about writing boilerplate code.", + "homepage": "https://symfony.com/doc/current/bundles/SymfonyMakerBundle/index.html", + "keywords": [ + "code generator", + "generator", + "scaffold", + "scaffolding" + ], + "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-03-23T13:53:38+00:00" + }, + { + "name": "symfony/phpunit-bridge", + "version": "v5.2.7", + "source": { + "type": "git", + "url": "https://github.com/symfony/phpunit-bridge.git", + "reference": "f530f0153f4a871b2c65dd6b295d7b8d03a16eac" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/phpunit-bridge/zipball/f530f0153f4a871b2c65dd6b295d7b8d03a16eac", + "reference": "f530f0153f4a871b2c65dd6b295d7b8d03a16eac", + "shasum": "" + }, + "require": { + "php": ">=5.5.9" + }, + "conflict": { + "phpunit/phpunit": "<4.8.35|<5.4.3,>=5.0|<6.4,>=6.0|9.1.2" + }, + "require-dev": { + "symfony/deprecation-contracts": "^2.1", + "symfony/error-handler": "^4.4|^5.0" + }, + "suggest": { + "symfony/error-handler": "For tracking deprecated interfaces usages at runtime with DebugClassLoader" + }, + "bin": [ + "bin/simple-phpunit" + ], + "type": "symfony-bridge", + "extra": { + "thanks": { + "name": "phpunit/phpunit", + "url": "https://github.com/sebastianbergmann/phpunit" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Bridge\\PhpUnit\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "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": "Provides utilities for PHPUnit, especially user deprecation notices management", + "homepage": "https://symfony.com", + "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-04-11T22:55:21+00:00" + }, + { + "name": "symfony/test-pack", + "version": "v1.0.7", + "source": { + "type": "git", + "url": "https://github.com/symfony/test-pack.git", + "reference": "e61756c97cbedae00b7cf43b87abcfadfeb2746c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/test-pack/zipball/e61756c97cbedae00b7cf43b87abcfadfeb2746c", + "reference": "e61756c97cbedae00b7cf43b87abcfadfeb2746c", + "shasum": "" + }, + "require": { + "symfony/browser-kit": "*", + "symfony/css-selector": "*", + "symfony/phpunit-bridge": "*" + }, + "type": "symfony-pack", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A pack for functional and end-to-end testing within a Symfony app", + "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": "2020-10-19T08:52:28+00:00" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": { + "consolibyte/quickbooks": 20 + }, + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": "^7.4", + "ext-ctype": "*", + "ext-gd": "*", + "ext-iconv": "*", + "ext-intl": "*", + "ext-sqlite3": "*", + "ext-json": "*" + }, + "platform-dev": [], + "plugin-api-version": "1.1.0" +} diff --git a/config/accounts_mapping.json b/config/accounts_mapping.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/config/accounts_mapping.json @@ -0,0 +1 @@ +{} diff --git a/config/bootstrap.php b/config/bootstrap.php new file mode 100644 index 0000000..55560fb --- /dev/null +++ b/config/bootstrap.php @@ -0,0 +1,23 @@ +=1.2) +if (is_array($env = @include dirname(__DIR__).'/.env.local.php') && (!isset($env['APP_ENV']) || ($_SERVER['APP_ENV'] ?? $_ENV['APP_ENV'] ?? $env['APP_ENV']) === $env['APP_ENV'])) { + (new Dotenv(false))->populate($env); +} else { + // load all the .env files + (new Dotenv(false))->loadEnv(dirname(__DIR__).'/.env'); +} + +$_SERVER += $_ENV; +$_SERVER['APP_ENV'] = $_ENV['APP_ENV'] = ($_SERVER['APP_ENV'] ?? $_ENV['APP_ENV'] ?? null) ?: 'dev'; +$_SERVER['APP_DEBUG'] = $_SERVER['APP_DEBUG'] ?? $_ENV['APP_DEBUG'] ?? 'prod' !== $_SERVER['APP_ENV']; +$_SERVER['APP_DEBUG'] = $_ENV['APP_DEBUG'] = (int) $_SERVER['APP_DEBUG'] || filter_var($_SERVER['APP_DEBUG'], FILTER_VALIDATE_BOOLEAN) ? '1' : '0'; diff --git a/config/bundles.php b/config/bundles.php new file mode 100644 index 0000000..cc8a0ad --- /dev/null +++ b/config/bundles.php @@ -0,0 +1,17 @@ + ['all' => true], + Doctrine\Bundle\DoctrineCacheBundle\DoctrineCacheBundle::class => ['all' => true], + Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true], + Oneup\FlysystemBundle\OneupFlysystemBundle::class => ['all' => true], + Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true], + Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true], + EasyCorp\Bundle\EasyAdminBundle\EasyAdminBundle::class => ['all' => true], + Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true], + Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true], + Doctrine\Bundle\FixturesBundle\DoctrineFixturesBundle::class => ['dev' => true, 'test' => true], + SymfonyCasts\Bundle\ResetPassword\SymfonyCastsResetPasswordBundle::class => ['all' => true], + Craue\FormFlowBundle\CraueFormFlowBundle::class => ['all' => true], + Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true], +]; diff --git a/config/packages/cache.yaml b/config/packages/cache.yaml new file mode 100644 index 0000000..7bd32b0 --- /dev/null +++ b/config/packages/cache.yaml @@ -0,0 +1,7 @@ +framework: + cache: + directory: '%kernel.cache_dir%/pools' + app: cache.adapter.filesystem + pools: + app.cache.currency_rate: + adapter: cache.app diff --git a/config/packages/dev/monolog.yaml b/config/packages/dev/monolog.yaml new file mode 100644 index 0000000..b1998da --- /dev/null +++ b/config/packages/dev/monolog.yaml @@ -0,0 +1,19 @@ +monolog: + handlers: + main: + type: stream + path: "%kernel.logs_dir%/%kernel.environment%.log" + level: debug + channels: ["!event"] + # uncomment to get logging in your browser + # you may have to allow bigger header sizes in your Web server configuration + #firephp: + # type: firephp + # level: info + #chromephp: + # type: chromephp + # level: info + console: + type: console + process_psr_3_messages: false + channels: ["!event", "!doctrine", "!console"] diff --git a/config/packages/dev/oneup_flysystem.yaml b/config/packages/dev/oneup_flysystem.yaml new file mode 100644 index 0000000..8275619 --- /dev/null +++ b/config/packages/dev/oneup_flysystem.yaml @@ -0,0 +1,12 @@ +# Read the documentation: https://github.com/1up-lab/OneupFlysystemBundle/tree/master/Resources/doc/index.md +oneup_flysystem: + adapters: + sheet_adapter: + local: + directory: '%kernel.project_dir%/var/flysystem' + filesystems: + sheet: + adapter: sheet_adapter + alias: League\Flysystem\FilesystemInterface + plugins: + - 'oneup_flysystem.plugin.list_files' diff --git a/config/packages/dev/routing.yaml b/config/packages/dev/routing.yaml new file mode 100644 index 0000000..4116679 --- /dev/null +++ b/config/packages/dev/routing.yaml @@ -0,0 +1,3 @@ +framework: + router: + strict_requirements: true diff --git a/config/packages/doctrine.yaml b/config/packages/doctrine.yaml new file mode 100644 index 0000000..fe98b72 --- /dev/null +++ b/config/packages/doctrine.yaml @@ -0,0 +1,35 @@ +parameters: + # Adds a fallback DATABASE_URL if the env var is not set. + # This allows you to run cache:warmup even if your + # environment variables are not available yet. + # You should not need to change this value. + env(DATABASE_URL): '' + +doctrine: + dbal: + url: '%env(resolve:DATABASE_URL)%' + + # IMPORTANT: You MUST configure your server version, + # either here or in the DATABASE_URL env var (see .env file) +# server_version: 'mariadb-10.2.22' + + # only needed for MySQL + charset: utf8mb4 + default_table_options: + collate: utf8mb4_unicode_ci + + # backtrace queries in profiler (increases memory usage per request) + #profiling_collect_backtrace: '%kernel.debug%' + orm: + auto_generate_proxy_classes: true + naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware + filters: + user_scope: App\Security\UserScopeFilter + auto_mapping: true + mappings: + App: + is_bundle: false + type: annotation + dir: '%kernel.project_dir%/src/Entity' + prefix: 'App\Entity' + alias: App diff --git a/config/packages/doctrine_migrations.yaml b/config/packages/doctrine_migrations.yaml new file mode 100644 index 0000000..70959a6 --- /dev/null +++ b/config/packages/doctrine_migrations.yaml @@ -0,0 +1,3 @@ +doctrine_migrations: + dir_name: '%kernel.project_dir%/src/Migrations' + namespace: DoctrineMigrations diff --git a/config/packages/easy_admin.yaml b/config/packages/easy_admin.yaml new file mode 100644 index 0000000..824244f --- /dev/null +++ b/config/packages/easy_admin.yaml @@ -0,0 +1,87 @@ +easy_admin: + site_name: '%env(resolve:APP_NAME)%' + design: + menu: + - { label: 'Documentation', url: '/documentation' } + - { label: 'Scheduling', url: '/scheduling/schedule', permission: 'ROLE_ADMIN' } + - { label: 'New Import', url: '/import/create' } + - { entity: 'QuickbooksCompany' } + - { entity: 'QuickbooksQueue' } + - { entity: 'QuickbooksAccount' } + entities: + QuickbooksCompany: + class: App\Entity\QuickbooksCompany + label: 'Company' + list: + actions: + - { label: 'QWC File', name: 'downloadQbwcConfig', icon: 'download'} + - { label: 'Sync Accounts', name: 'syncAccounts', icon: 'refresh'} + fields: + - 'companyName' + - 'qbUsername' + - { property: 'baseCurrency', label: 'Base currency' } + form: + fields: + - { property: 'companyName', label: 'Company Name', help: 'Specify short name of your company' } + - type: 'section' + label: 'Define a new password' + help: > + This password you will need to enter in Quickbooks Web Connector + - { property: 'qbPlainPassword', label: 'Password', type: 'password', help: 'You can not see the password after it is set' } + - type: 'section' + label: 'Set the base currency' + help: > + If you are not using multicurrency just leave it as US Dollar. + - property: 'baseCurrency' + label: 'Base currency' + help: 'Used for calculating exchange rate for the given txnDate when the currency differs from the home one' + - type: 'section' + label: 'Multi-tenancy (multiple companies on the same computer)' + help: > + Allows to restrict import to a particular company, so other companies will be rejected. + You can leave this field empty. It will be set automatically on the first data exchange. + - { property: 'qbCompanyFile', label: 'Company file', help: 'e.g: C:\Users\Public\Documents\Intuit\QuickBooks\Company Files\your-company-file.qbw' } + + QuickbooksQueue: + class: App\Entity\QuickbooksQueue + label: 'Queue' + disabled_actions: ['new', 'edit'] + list: + actions: ['show'] + batch_actions: + - { name: 'delete', ask_confirm: true } + fields: + - { property: 'ident', label: 'Identifier' } + - { property: 'companyName', label: 'Company Name' } + - { property: 'qbAction', label: 'Action' } + - { property: 'statusLabel', label: 'Status' } + - { property: 'msg', label: 'Message' } + - { property: 'enqueueDatetime', label: 'Scheduled At' } + - { property: 'dequeueDatetime', label: 'Processed At' } + show: + fields: + - { property: 'ident', label: 'Identifier' } + - { property: 'companyName', label: 'Company Name' } + - { property: 'qbAction', label: 'Action' } + - { property: 'statusLabel', label: 'Status' } + - { property: 'msg', label: 'Message' } + - { property: 'qbxml', label: 'XML' } + - { property: 'enqueueDatetime', label: 'Scheduled At' } + - { property: 'dequeueDatetime', label: 'Processed At' } + + QuickbooksAccount: + class: App\Entity\QuickbooksAccount + label: 'Chart Of Accounts' + disabled_actions: ['new', 'edit'] + list: + actions: ['show'] + batch_actions: + - { name: 'delete', ask_confirm: true } + fields: + - { property: 'id', label: 'ID' } + - { property: 'companyName', label: 'Company Name' } + - 'fullName' + - 'currency' + - 'accountType' + - 'specialAccountType' + - 'accountNumber' diff --git a/config/packages/framework.yaml b/config/packages/framework.yaml new file mode 100644 index 0000000..82240cb --- /dev/null +++ b/config/packages/framework.yaml @@ -0,0 +1,18 @@ +framework: + secret: '%env(APP_SECRET)%' + #csrf_protection: true + #http_method_override: true + + # Enables session support. Note that the session will ONLY be started if you read or write from it. + # Remove or comment this section to explicitly disable session support. + session: + handler_id: null + # 2 Days lifetime (172800 seconds) + cookie_lifetime: 172800 + cookie_secure: auto + cookie_samesite: lax + + #esi: true + #fragments: true + php_errors: + log: true diff --git a/config/packages/mailer.yaml b/config/packages/mailer.yaml new file mode 100644 index 0000000..56a650d --- /dev/null +++ b/config/packages/mailer.yaml @@ -0,0 +1,3 @@ +framework: + mailer: + dsn: '%env(MAILER_DSN)%' diff --git a/config/packages/nyholm_psr7.yaml b/config/packages/nyholm_psr7.yaml new file mode 100644 index 0000000..f135723 --- /dev/null +++ b/config/packages/nyholm_psr7.yaml @@ -0,0 +1,21 @@ +services: + # Register nyholm/psr7 services for autowiring with PSR-17 (HTTP factories) + Psr\Http\Message\RequestFactoryInterface: '@nyholm.psr7.psr17_factory' + Psr\Http\Message\ResponseFactoryInterface: '@nyholm.psr7.psr17_factory' + Psr\Http\Message\ServerRequestFactoryInterface: '@nyholm.psr7.psr17_factory' + Psr\Http\Message\StreamFactoryInterface: '@nyholm.psr7.psr17_factory' + Psr\Http\Message\UploadedFileFactoryInterface: '@nyholm.psr7.psr17_factory' + Psr\Http\Message\UriFactoryInterface: '@nyholm.psr7.psr17_factory' + + # Register nyholm/psr7 services for autowiring with HTTPlug factories + Http\Message\MessageFactory: '@nyholm.psr7.httplug_factory' + Http\Message\RequestFactory: '@nyholm.psr7.httplug_factory' + Http\Message\ResponseFactory: '@nyholm.psr7.httplug_factory' + Http\Message\StreamFactory: '@nyholm.psr7.httplug_factory' + Http\Message\UriFactory: '@nyholm.psr7.httplug_factory' + + nyholm.psr7.psr17_factory: + class: Nyholm\Psr7\Factory\Psr17Factory + + nyholm.psr7.httplug_factory: + class: Nyholm\Psr7\Factory\HttplugFactory diff --git a/config/packages/prod/doctrine.yaml b/config/packages/prod/doctrine.yaml new file mode 100644 index 0000000..084f59a --- /dev/null +++ b/config/packages/prod/doctrine.yaml @@ -0,0 +1,20 @@ +doctrine: + orm: + auto_generate_proxy_classes: false + metadata_cache_driver: + type: pool + pool: doctrine.system_cache_pool + query_cache_driver: + type: pool + pool: doctrine.system_cache_pool + result_cache_driver: + type: pool + pool: doctrine.result_cache_pool + +framework: + cache: + pools: + doctrine.result_cache_pool: + adapter: cache.app + doctrine.system_cache_pool: + adapter: cache.system diff --git a/config/packages/prod/monolog.yaml b/config/packages/prod/monolog.yaml new file mode 100644 index 0000000..c8a16ec --- /dev/null +++ b/config/packages/prod/monolog.yaml @@ -0,0 +1,26 @@ +monolog: + handlers: + main: + type: fingers_crossed + action_level: error + handler: nested + excluded_http_codes: [404, 405] + buffer_size: 50 # How many messages should be saved? Prevent memory leaks + nested: + type: stream + path: "%kernel.logs_dir%/%kernel.environment%.log" + level: debug + console: + type: console + process_psr_3_messages: false + channels: ["!event", "!doctrine"] + + # Uncomment to log deprecations + #deprecation: + # type: stream + # path: "%kernel.logs_dir%/%kernel.environment%.deprecations.log" + #deprecation_filter: + # type: filter + # handler: deprecation + # max_level: info + # channels: ["php"] diff --git a/config/packages/prod/oneup_flysystem.yaml b/config/packages/prod/oneup_flysystem.yaml new file mode 100644 index 0000000..7db1498 --- /dev/null +++ b/config/packages/prod/oneup_flysystem.yaml @@ -0,0 +1,31 @@ +# Read the documentation: https://github.com/1up-lab/OneupFlysystemBundle/tree/master/Resources/doc/index.md +oneup_flysystem: + adapters: + sheet_adapter: + local: + directory: '%kernel.project_dir%/var/flysystem' + filesystems: + sheet: + adapter: sheet_adapter + alias: League\Flysystem\FilesystemInterface + plugins: + - 'oneup_flysystem.plugin.list_files' + +#oneup_flysystem: +# adapters: +# sheet_adapter: +# webdav: +# client: app.webdav_client +# prefix: '%env(resolve:WEBDAV_PATH)%' +# filesystems: +# sheet: +# adapter: sheet_adapter +# alias: League\Flysystem\FilesystemInterface +# plugins: +# - 'oneup_flysystem.plugin.list_files' +# +#services: +# app.webdav_client: +# class: Sabre\DAV\Client +# arguments: +# - { baseUri: '%env(resolve:WEBDAV_URL)%', userName: '%env(resolve:WEBDAV_USERNAME)%', password: '%env(resolve:WEBDAV_PASSWORD)%' } diff --git a/config/packages/prod/routing.yaml b/config/packages/prod/routing.yaml new file mode 100644 index 0000000..b3e6a0a --- /dev/null +++ b/config/packages/prod/routing.yaml @@ -0,0 +1,3 @@ +framework: + router: + strict_requirements: null diff --git a/config/packages/reset_password.yaml b/config/packages/reset_password.yaml new file mode 100644 index 0000000..796ff0c --- /dev/null +++ b/config/packages/reset_password.yaml @@ -0,0 +1,2 @@ +symfonycasts_reset_password: + request_password_repository: App\Repository\ResetPasswordRequestRepository diff --git a/config/packages/routing.yaml b/config/packages/routing.yaml new file mode 100644 index 0000000..7e97762 --- /dev/null +++ b/config/packages/routing.yaml @@ -0,0 +1,3 @@ +framework: + router: + utf8: true diff --git a/config/packages/security.yaml b/config/packages/security.yaml new file mode 100644 index 0000000..9b4f32a --- /dev/null +++ b/config/packages/security.yaml @@ -0,0 +1,35 @@ +security: + encoders: + Symfony\Component\Security\Core\User\User: plaintext + App\Entity\User: + algorithm: auto + + # https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers + providers: + # used to reload user from session & other features (e.g. switch_user) + app_user_provider: + entity: + class: App\Entity\User + property: email + firewalls: + dev: + pattern: ^/(_(profiler|wdt)|css|images|js)/ + security: false + main: + anonymous: lazy + logout: + path: app_logout + guard: + authenticators: + - App\Security\LoginFormAuthenticator + + # Easy way to control access for large sections of your site + # Note: Only the *first* access control that matches will be used + access_control: + - { path: ^/qbwc, roles: IS_AUTHENTICATED_ANONYMOUSLY } + - { path: ^/login$, roles: IS_AUTHENTICATED_ANONYMOUSLY } + - { path: ^/reset-password, roles: IS_AUTHENTICATED_ANONYMOUSLY } + - { path: ^/register, roles: IS_AUTHENTICATED_ANONYMOUSLY } + - { path: ^/scheduling/schedule, roles: ROLE_ADMIN } + - { path: ^/scheduling/test-schedule, roles: ROLE_ADMIN } + - { path: ^/, roles: ROLE_USER } diff --git a/config/packages/test/framework.yaml b/config/packages/test/framework.yaml new file mode 100644 index 0000000..d051c84 --- /dev/null +++ b/config/packages/test/framework.yaml @@ -0,0 +1,4 @@ +framework: + test: true + session: + storage_id: session.storage.mock_file diff --git a/config/packages/test/monolog.yaml b/config/packages/test/monolog.yaml new file mode 100644 index 0000000..fc40641 --- /dev/null +++ b/config/packages/test/monolog.yaml @@ -0,0 +1,12 @@ +monolog: + handlers: + main: + type: fingers_crossed + action_level: error + handler: nested + excluded_http_codes: [404, 405] + channels: ["!event"] + nested: + type: stream + path: "%kernel.logs_dir%/%kernel.environment%.log" + level: debug diff --git a/config/packages/test/oneup_flysystem.yaml b/config/packages/test/oneup_flysystem.yaml new file mode 100644 index 0000000..90c6f4b --- /dev/null +++ b/config/packages/test/oneup_flysystem.yaml @@ -0,0 +1,12 @@ +# Read the documentation: https://github.com/1up-lab/OneupFlysystemBundle/tree/master/Resources/doc/index.md +oneup_flysystem: + adapters: + sheet_adapter: + local: + directory: '%kernel.project_dir%/tests/Functional/fixtures' + filesystems: + sheet: + adapter: sheet_adapter + alias: League\Flysystem\FilesystemInterface + plugins: + - 'oneup_flysystem.plugin.list_files' diff --git a/config/packages/test/routing.yaml b/config/packages/test/routing.yaml new file mode 100644 index 0000000..4116679 --- /dev/null +++ b/config/packages/test/routing.yaml @@ -0,0 +1,3 @@ +framework: + router: + strict_requirements: true diff --git a/config/packages/test/twig.yaml b/config/packages/test/twig.yaml new file mode 100644 index 0000000..8c6e0b4 --- /dev/null +++ b/config/packages/test/twig.yaml @@ -0,0 +1,2 @@ +twig: + strict_variables: true diff --git a/config/packages/test/validator.yaml b/config/packages/test/validator.yaml new file mode 100644 index 0000000..1e5ab78 --- /dev/null +++ b/config/packages/test/validator.yaml @@ -0,0 +1,3 @@ +framework: + validation: + not_compromised_password: false diff --git a/config/packages/translation.yaml b/config/packages/translation.yaml new file mode 100644 index 0000000..05a2b3d --- /dev/null +++ b/config/packages/translation.yaml @@ -0,0 +1,6 @@ +framework: + default_locale: en + translator: + default_path: '%kernel.project_dir%/translations' + fallbacks: + - en diff --git a/config/packages/twig.yaml b/config/packages/twig.yaml new file mode 100644 index 0000000..cd7340f --- /dev/null +++ b/config/packages/twig.yaml @@ -0,0 +1,9 @@ +twig: + default_path: '%kernel.project_dir%/templates' + debug: '%kernel.debug%' + strict_variables: '%kernel.debug%' + exception_controller: null + form_themes: ['form/bootstrap_4.html.twig'] +# form_themes: ['@EasyAdmin/form/bootstrap_4.html.twig'] +# form_themes: ['bootstrap_4_layout.html.twig'] +# form_themes: ['bootstrap_4_horizontal_layout.html.twig'] diff --git a/config/packages/validator.yaml b/config/packages/validator.yaml new file mode 100644 index 0000000..350786a --- /dev/null +++ b/config/packages/validator.yaml @@ -0,0 +1,8 @@ +framework: + validation: + email_validation_mode: html5 + + # Enables validator auto-mapping support. + # For instance, basic validation constraints will be inferred from Doctrine's metadata. + #auto_mapping: + # App\Entity\: [] diff --git a/config/routes.yaml b/config/routes.yaml new file mode 100644 index 0000000..7144a21 --- /dev/null +++ b/config/routes.yaml @@ -0,0 +1,45 @@ +app_homepage: + path: / + controller: Symfony\Bundle\FrameworkBundle\Controller\RedirectController::urlRedirectAction + defaults: + path: /import/create + +app_logout: + path: /logout + methods: GET + +app_admin_download_qbwc_config: + path: /download-qbwc-config + controller: App\Controller\AdminController::downloadQbwcConfigAction + +app_qbwc_server: + path: /qbwc + controller: App\Controller\QbwcController::server + +app_documentation: + path: /documentation + controller: App\Controller\DocumentationController::index + +app_schedule: + path: /scheduling/schedule + controller: App\Controller\SchedulingController::schedule + +app_schedule_accounts: + path: /scheduling/accounts + controller: App\Controller\SchedulingController::scheduleAccounts + +app_test_schedule: + path: /scheduling/test-schedule + controller: App\Controller\SchedulingController::testSchedule + +app_import_create: + path: /import/create + controller: App\Controller\ImportController::create + +app_import_sample: + path: /import/{type}/sample + controller: App\Controller\ImportController::sample + +app_import_scheduled: + path: /import/scheduled + controller: App\Controller\ImportController::scheduled diff --git a/config/routes/annotations.yaml b/config/routes/annotations.yaml new file mode 100644 index 0000000..e92efc5 --- /dev/null +++ b/config/routes/annotations.yaml @@ -0,0 +1,7 @@ +controllers: + resource: ../../src/Controller/ + type: annotation + +kernel: + resource: ../../src/Kernel.php + type: annotation diff --git a/config/routes/dev/framework.yaml b/config/routes/dev/framework.yaml new file mode 100644 index 0000000..bcbbf13 --- /dev/null +++ b/config/routes/dev/framework.yaml @@ -0,0 +1,3 @@ +_errors: + resource: '@FrameworkBundle/Resources/config/routing/errors.xml' + prefix: /_error diff --git a/config/routes/dev/twig.yaml b/config/routes/dev/twig.yaml new file mode 100644 index 0000000..f4ee839 --- /dev/null +++ b/config/routes/dev/twig.yaml @@ -0,0 +1,3 @@ +_errors: + resource: '@TwigBundle/Resources/config/routing/errors.xml' + prefix: /_error diff --git a/config/routes/easy_admin.yaml b/config/routes/easy_admin.yaml new file mode 100644 index 0000000..c7975d9 --- /dev/null +++ b/config/routes/easy_admin.yaml @@ -0,0 +1,4 @@ +easy_admin_bundle: + resource: 'App\Controller\AdminController' + prefix: /ea + type: annotation diff --git a/config/services.yaml b/config/services.yaml new file mode 100644 index 0000000..2654885 --- /dev/null +++ b/config/services.yaml @@ -0,0 +1,78 @@ +# This file is the entry point to configure your own services. +# Files in the packages/ subdirectory configure your dependencies. + +# Put parameters here that don't need to change on each machine where the app is deployed +# https://symfony.com/doc/current/best_practices/configuration.html#application-related-configuration +parameters: + 'env(APP_NAME)': 'Easy Quick Import' + 'env(PROJECT_URL)': 'https://%env(DOMAIN)%' +services: + # default configuration for services in *this* file + _defaults: + autowire: true # Automatically injects dependencies in your services. + autoconfigure: true # Automatically registers your services as commands, event subscribers, etc. + bind: + string $projectUrl: '%env(PROJECT_URL)%' + string $appName: '%env(APP_NAME)%' + + # makes classes in src/ available to be used as services + # this creates a service per class whose id is the fully-qualified class name + App\: + resource: '../src/*' + exclude: '../src/{DependencyInjection,Entity,Migrations,Tests,Kernel.php}' + + # controllers are imported separately to make sure services can be injected + # as action arguments even if you don't extend any base controller class + App\Controller\: + resource: '../src/Controller' + tags: ['controller.service_arguments'] + + # add more service definitions when explicit configuration is needed + # please note that last definitions always *replace* previous ones + + +# QuickBooks_WebConnector_QWC: +# class: 'QuickBooks_WebConnector_QWC' +# arguments: +# $name: 'QB Connector' +# $descrip: 'QB Connector' +# $appurl: '%env(PROJECT_URL)%/qbwc?XDEBUG_SESSION_START=1' +# $appsupport: '%env(PROJECT_URL)%/qbwc' +# $username: '%env(QB_USER)%' +# $fileid: '%env(QB_FILE_ID)%' +# $ownerid: '%env(QB_OWNER_ID)%' +# $run_every_n_seconds: 600 + + + App\Entity\QuickbooksAccountRepositoryInterface: '@App\Repository\QuickbooksAccountRepository' + + App\TransactionsConverter: + arguments: + $encoder: '@serializer.encoder.csv' + $accountsMappingPath: '%kernel.project_dir%/config/accounts_mapping.json' + + App\QuickbooksServer: + arguments: + $dsn: '%env(resolve:DATABASE_URL)%' + + App\SheetScheduler\Transformer\VendorTransformer: + tags: ['entity.transformer'] + + App\SheetScheduler\Transformer\VendorBillTransformer: + tags: ['entity.transformer'] + + App\SheetScheduler\Transformer\CustomerTransformer: + tags: ['entity.transformer'] + + App\SheetScheduler\Transformer\CustomerInvoiceTransformer: + tags: ['entity.transformer'] + + App\SheetScheduler\Transformer\TransactionTransformer: + tags: ['entity.transformer'] + + App\Currency\CurrencyExchangerInterface: '@App\Currency\CachedCurrencyExchanger' + + Symfony\Component\Mime\Address: + arguments: ['contact@easyquickimport.com', 'EasyQuickImport'] + + App\Entity\QuickbooksCompanyRepositoryInterface: '@App\Repository\QuickbooksCompanyRepository' diff --git a/config/services_test.yaml b/config/services_test.yaml new file mode 100644 index 0000000..2ba7846 --- /dev/null +++ b/config/services_test.yaml @@ -0,0 +1,11 @@ +services: + _defaults: + public: true + + # If you need to access services in a test, create an alias + # and then fetch that alias from the container. As a convention, + # aliases are prefixed with test. For example: + # + + App\Tests\Functional\FakeCurrencyExchanger: ~ + App\Currency\CurrencyExchangerInterface: '@App\Tests\Functional\FakeCurrencyExchanger' diff --git a/config/validator/validation.yaml b/config/validator/validation.yaml new file mode 100644 index 0000000..42038d8 --- /dev/null +++ b/config/validator/validation.yaml @@ -0,0 +1,136 @@ +App\SheetScheduler\CustomerInvoice: + constraints: + - Callback: validateQuantity + properties: + refNumber: + - Length: { max: 11 } + invoiceMemo: + - Length: { max: 4095 } + arAccount: + - Length: { max: 159 } + txnDate: + - Date: ~ + exchangeRate: + - Regex: { pattern: '/^\d+(\.\d+)?/' } #float + line1ItemName: + - NotBlank: ~ + line1Desc: + - Length: { max: 4095 } + line1Quantity: + - Regex: { pattern: '/^\d+(\.\d+)?/' } #float + line1Amount: + - Regex: { pattern: '/^\d+(\.\d+)?/' } #float + line1Rate: + - Regex: { pattern: '/^\d+(\.\d+)?/' } #float + +App\SheetScheduler\Customer: + properties: + customerFullName: + - Length: { max: 159 } + firstName: + - Length: { max: 25 } + lastName: + - Length: { max: 25 } + companyName: + - Length: { max: 41 } + terms: + - Length: { max: 31 } + currency: + - Length: { max: 64 } + addr1: + - Length: { max: 41 } + addr2: + - Length: { max: 41 } + city: + - Length: { max: 31 } + state: + - Length: { max: 21 } + postalcode: + - Length: { max: 13 } + country: + - Length: { max: 31 } + +App\SheetScheduler\VendorBill: + properties: + memo: + - Length: { max: 4095 } + apAccount: + - Length: { max: 159 } + refNumber: + - Length: { max: 20 } + txnDate: + - Date: ~ + exchangeRate: + - Regex: { pattern: '/^\d+(\.\d+)?/' } #float + line1AccountFullName: + - Length: { max: 159 } + line1Amount: + - Regex: { pattern: '/^\d+(\.\d+)?/' } #float + line1Memo: + - Length: { max: 4095 } + line2AccountFullName: + - Length: { max: 159 } + line2Amount: + - Regex: { pattern: '/^\d+(\.\d+)?/' } #float + line2Memo: + - Length: { max: 4095 } + +App\SheetScheduler\Vendor: + properties: + vendorFullname: + - Length: { max: 159 } + vendorCompanyName: + - Length: { max: 41 } + addr1: + - Length: { max: 41 } + addr2: + - Length: { max: 41 } + city: + - Length: { max: 31 } + state: + - Length: { max: 21 } + postalcode: + - Length: { max: 13 } + country: + - Length: { max: 31 } + vendorType: + - Length: { max: 159 } + terms: + - Length: { max: 31 } + currency: + - Length: { max: 64 } + +App\SheetScheduler\Transaction: + properties: + exchangeRate: + - Regex: { pattern: '/^\d+(\.\d+)?/' } #float + currency: + - Length: { max: 64 } + refNumber: + - Length: { max: 11 } + txnDate: + - Date: ~ + creditAccount: + - Length: { max: 159 } + creditMemo: + - Length: { max: 4095 } + creditAmount: + - Regex: { pattern: '/^\d+(\.\d+)?/' } #float + debitAccount: + - Length: { max: 159 } + debitMemo: + - Length: { max: 4095 } + debitAmount: + - Regex: { pattern: '/^\d+(\.\d+)?/' } #float + +App\Entity\QuickbooksCompany: + properties: + qbUsername: + - NotBlank: ~ + qbPassword: + - NotBlank: ~ + qbCompanyFile: + - Regex: { pattern: '/^.*\.qbw$/i' } + baseCurrency: + - Length: { max: 3 } + - Currency: ~ diff --git a/docker/.env.dist b/docker/.env.dist new file mode 100644 index 0000000..d5b3cf0 --- /dev/null +++ b/docker/.env.dist @@ -0,0 +1,16 @@ +APPLICATION=.. +ENVIRONMENT= +APP_ENV= +DOCKER_ENV= +PUID= +PGID= +REGISTRY= +REGISTRY_IMAGE= +REGISTRY_USER= +REGISTRY_PASSWORD= +DOCKER_HOST_IP=172.17.0.1 +TAG=latest +VIRTUAL_HOST= +COMPOSE_FILE= +COMPOSE_PROJECT_NAME= +COMPOSER_AUTH='{"github-oauth": {"github.com": "ea5829073b8d820ced6074df7a0bb69321c3213e"}}' diff --git a/docker/.gitignore b/docker/.gitignore new file mode 100644 index 0000000..8913abc --- /dev/null +++ b/docker/.gitignore @@ -0,0 +1,2 @@ +.env +.app_env diff --git a/docker/bin/build.sh b/docker/bin/build.sh new file mode 100755 index 0000000..30173cb --- /dev/null +++ b/docker/bin/build.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash + +set -e + +set -a +. ./.env +set +a + +echo "******DOCKER_ENV=${DOCKER_ENV} TAG: ${TAG} Host ID: " $(id -u) + +docker build --build-arg "COMPOSER_AUTH=${COMPOSER_AUTH}" \ + -t ${REGISTRY_IMAGE}/app_php_base:${TAG} \ + -t ${REGISTRY_IMAGE}/app_php_base:latest \ + -f ${APPLICATION}/Dockerfile \ + --target app_php_base \ + ${APPLICATION} + +if [ "${DOCKER_ENV}" == "prod" ]; then + docker build --build-arg "COMPOSER_AUTH=${COMPOSER_AUTH}" \ + -t ${REGISTRY_IMAGE}/app_php:${TAG} \ + -t ${REGISTRY_IMAGE}/app_php:latest \ + -f ${APPLICATION}/Dockerfile \ + --target app_php \ + --cache-from ${REGISTRY_IMAGE}/app_php_base:${TAG} \ + ${APPLICATION} + + docker build -t ${REGISTRY_IMAGE}/app_nginx:${TAG} \ + -t ${REGISTRY_IMAGE}/app_nginx:latest \ + -f ${APPLICATION}/Dockerfile \ + --target app_nginx \ + --cache-from ${REGISTRY_IMAGE}/app_php:${TAG} \ + ${APPLICATION} +fi diff --git a/docker/bin/copy-env.sh b/docker/bin/copy-env.sh new file mode 100755 index 0000000..d567ce8 --- /dev/null +++ b/docker/bin/copy-env.sh @@ -0,0 +1,84 @@ +#!/usr/bin/env bash + +APPLICATION=${CI_PROJECT_DIR:-..} +APP_ENV=${APP_ENV:-prod} +ENVIRONMENT=${ENVIRONMENT:-local} +DOCKER_ENV=${DOCKER_ENV:-prod} +COMPOSE_PROJECT_NAME=easyimport +GIT_TAG=${CI_COMMIT_TAG:-$(git describe --tags --exact-match || true)} +GIT_BRANCH=${CI_COMMIT_BRANCH:-$(git rev-parse --abbrev-ref HEAD)} +DATE_ISO=$(date -I'seconds') +VERSION=${GIT_TAG:-$GIT_BRANCH}-${DATE_ISO} + +echo "APP_ENV: ${APP_ENV} VERSION: ${VERSION}" + +TAG=${CI_COMMIT_REF_SLUG:-latest} +PUID=$(id -u) +PGID=$(id -g) + +REGISTRY=${CI_REGISTRY} +REGISTRY_IMAGE=${CI_REGISTRY_IMAGE} +REGISTRY_USER=${CI_REGISTRY_USER} +REGISTRY_PASSWORD=${CI_REGISTRY_PASSWORD} + +case "$DOCKER_ENV" in + "prod") + COMPOSE_FILE=docker-compose.prod.yml + ;; + "test") + COMPOSE_FILE=docker-compose.test.yml + ;; +esac + +# docker env file + +sed -e" \ + s#^DOCKER_ENV=.*#DOCKER_ENV=$DOCKER_ENV#; \ + s#APP_ENV=.*#APP_ENV=$APP_ENV#; \ + s#ENVIRONMENT=.*#ENVIRONMENT=$ENVIRONMENT#; \ + s#APPLICATION=.*#APPLICATION=$APPLICATION#; \ + s#PUID=.*#PUID=$PUID#; \ + s#PGID=.*#PGID=$PGID#; \ + s#REGISTRY=.*#REGISTRY=$REGISTRY#; \ + s#REGISTRY_IMAGE=.*#REGISTRY_IMAGE=$REGISTRY_IMAGE#; \ + s#REGISTRY_USER=.*#REGISTRY_USER=$REGISTRY_USER#; \ + s#REGISTRY_PASSWORD=.*#REGISTRY_PASSWORD=$REGISTRY_PASSWORD#; \ + s#TAG=.*#TAG=$TAG#; \ + s#COMPOSE_FILE=.*#COMPOSE_FILE=$COMPOSE_FILE#; \ + s#COMPOSE_PROJECT_NAME=.*#COMPOSE_PROJECT_NAME=$COMPOSE_PROJECT_NAME#; \ + +" .env.dist > .env + +if [ ! -z "$VIRTUAL_HOST" ] ; then + sed -i " \ + s#^VIRTUAL_HOST=.*#VIRTUAL_HOST=$VIRTUAL_HOST#; \ + " .env +fi + +# app env file + +sed -e" \ + s#^APP_ENV=.*#APP_ENV=$APP_ENV#; \ + s#^VERSION=.*#VERSION=$VERSION#; \ + s#COMPOSE_FILE=.*#COMPOSE_FILE=$COMPOSE_FILE#; \ + +" ${APPLICATION}/.env > .app_env + + +if [ ! -z "$DATABASE_URL" ] ; then + sed -i " \ + s#^DATABASE_URL=.*#DATABASE_URL=$DATABASE_URL#; \ + " .app_env +fi + +if [ ! -z "$ENVIRONMENT" ] ; then + sed -i " \ + s#^DEPLOYMENT=.*#DEPLOYMENT=$ENVIRONMENT#; \ + " .app_env +fi + +if [ ! -z "$MAILER_DSN" ] ; then + sed -i " \ + s#^MAILER_DSN=.*#MAILER_DSN=$MAILER_DSN#; \ + " .app_env +fi diff --git a/docker/bin/push.sh b/docker/bin/push.sh new file mode 100755 index 0000000..8ca9b1a --- /dev/null +++ b/docker/bin/push.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash + +set -e + +set -a +. ./.env +set +a + +echo ${REGISTRY_PASSWORD} | docker login ${REGISTRY} -u ${REGISTRY_USER} --password-stdin +echo Pushing... +docker push ${REGISTRY_IMAGE}/app_php:${TAG} +docker push ${REGISTRY_IMAGE}/app_php:latest +docker push ${REGISTRY_IMAGE}/app_nginx:${TAG} +docker push ${REGISTRY_IMAGE}/app_nginx:latest diff --git a/docker/docker-compose.dev.yml b/docker/docker-compose.dev.yml new file mode 100644 index 0000000..05284cc --- /dev/null +++ b/docker/docker-compose.dev.yml @@ -0,0 +1,39 @@ +version: '3.4' + +services: + php: + image: ${REGISTRY_IMAGE}/app_php:${TAG} + extra_hosts: + - 'db:${DATABASE_HOST}' +# environment: +# - DATABASE_URL=mysql://easyquickimport:hsPxn6ntuFb9rUWvb@mysql:3306/easyquickimport?serverVersion=mariadb-10.2.22 + networks: + - backend + volumes: + - ${APPLICATION}:/var/www/app +# +# mysql: +# image: leafney/alpine-mariadb:10.2.22 +# environment: +# - MYSQL_ROOT_PWD=hsPxn6ntuFb9rUWvb +# - MYSQL_USER=easyquickimport +# - MYSQL_USER_PWD=hsPxn6ntuFb9rUWvb +# - MYSQL_USER_DB=easyquickimport +# ports: +# - '3339:3306' +# networks: +# - backend + + nginx: + image: ${REGISTRY_IMAGE}/app_nginx:${TAG} + depends_on: + - php + ports: + - '8083:80' + networks: + - backend + volumes: + - ${APPLICATION}:/var/www/app + +networks: + backend: diff --git a/docker/docker-compose.prod.yml b/docker/docker-compose.prod.yml new file mode 100644 index 0000000..88f2688 --- /dev/null +++ b/docker/docker-compose.prod.yml @@ -0,0 +1,54 @@ +version: '3.4' + +services: + php: + image: ${REGISTRY_IMAGE}/app_php:${TAG} + env_file: + - .app_env + extra_hosts: + - 'db:${DATABASE_HOST}' + healthcheck: + interval: 10s + timeout: 3s + retries: 3 + start_period: 30s + networks: + - backend + restart: unless-stopped + deploy: + replicas: 1 + update_config: + order: start-first + placement: + constraints: + - node.role == manager + + nginx: + image: ${REGISTRY_IMAGE}/app_nginx:${TAG} + depends_on: + - php +# ports: +# - '8083:80' + networks: + - backend + - webproxy + restart: unless-stopped + labels: &nginx_labels + - "traefik.backend=easyquickimport-${ENVIRONMENT}-nginx" + - "traefik.docker.network=webproxy" + - "traefik.frontend.rule=Host:${VIRTUAL_HOST}" + - "traefik.enable=true" + - "traefik.port=80" + deploy: + replicas: 1 + update_config: + order: start-first + placement: + constraints: + - node.role == manager + labels: *nginx_labels + +networks: + backend: + webproxy: + external: true diff --git a/docker/docker-compose.test.yml b/docker/docker-compose.test.yml new file mode 100644 index 0000000..7332e44 --- /dev/null +++ b/docker/docker-compose.test.yml @@ -0,0 +1,26 @@ +version: '3.4' + +services: + php: + image: ${REGISTRY_IMAGE}/app_php_base:${TAG} + working_dir: /var/www/app + environment: + - DATABASE_URL=mysql://dev:dev@mysql:3306/qb_api + depends_on: + - mysql + networks: + - backend + volumes: + - ${APPLICATION}:/var/www/app + + mysql: + image: leafney/alpine-mariadb:10.2.22 + environment: + - MYSQL_USER=dev + - MYSQL_USER_PWD=dev + - MYSQL_USER_DB=qb_api + networks: + - backend + +networks: + backend: diff --git a/docker/nginx/conf.d/default.conf b/docker/nginx/conf.d/default.conf new file mode 100644 index 0000000..da44c1f --- /dev/null +++ b/docker/nginx/conf.d/default.conf @@ -0,0 +1,48 @@ +server { + listen 80 default; + server_name _; + + root /var/www/app/public; + + location / { + # try to serve file directly, fallback to index.php + try_files $uri /index.php$is_args$args; + } + + location ~ ^/index\.php(/|$) { + fastcgi_pass php:9000; + fastcgi_split_path_info ^(.+\.php)(/.*)$; + include fastcgi_params; + fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name; + fastcgi_param DOCUMENT_ROOT $realpath_root; + # Prevents URIs that include the front controller. This will 404: + # http://domain.tld/index.php/some-path + # Remove the internal directive to allow URIs like this + internal; + } + + # URL for health checks + location /nginx-health { + access_log off; + default_type text/plain; + return 200 "healthy\n"; + } + + # return 404 for all other php files not matching the front controller + # this prevents access to other php files you don't want to be accessible. + location ~ \.php$ { + return 404; + } + + gzip on; + gzip_buffers 16 8k; + gzip_comp_level 5; + gzip_http_version 1.1; + gzip_min_length 1100; + gzip_types text/plain text/css application/json application/ld+json application/javascript text/xml application/xml application/xml+rss text/javascript image/x-icon application/vnd.ms-fontobject font/opentype application/x-font-ttf; + gzip_vary on; + gzip_proxied any; # Compression for all requests. + ## No need for regexps. See + ## http://wiki.nginx.org/NginxHttpGzipModule#gzip_disable + gzip_disable msie6; +} diff --git a/docker/nginx/nginx.conf b/docker/nginx/nginx.conf new file mode 100644 index 0000000..362f7f7 --- /dev/null +++ b/docker/nginx/nginx.conf @@ -0,0 +1,32 @@ +user www-data www-data; +worker_processes 4; + +error_log /var/log/nginx/error.log warn; +pid /var/run/nginx.pid; + + +events { + worker_connections 1024; +} + + +http { + client_max_body_size 100m; + include /etc/nginx/mime.types; + default_type application/octet-stream; + + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + + access_log /var/log/nginx/access.log main; + + sendfile on; + #tcp_nopush on; + + keepalive_timeout 65; + + #gzip on; + + include /etc/nginx/conf.d/*.conf; +} diff --git a/docker/php/conf.d/app.dev.ini b/docker/php/conf.d/app.dev.ini new file mode 100644 index 0000000..39b1cd4 --- /dev/null +++ b/docker/php/conf.d/app.dev.ini @@ -0,0 +1,11 @@ +apc.enable_cli = 1 +date.timezone = UTC +session.auto_start = Off +short_open_tag = Off + +# https://symfony.com/doc/current/performance.html +opcache.interned_strings_buffer = 16 +opcache.max_accelerated_files = 20000 +opcache.memory_consumption = 256 +realpath_cache_size = 4096K +realpath_cache_ttl = 600 diff --git a/docker/php/conf.d/app.prod.ini b/docker/php/conf.d/app.prod.ini new file mode 100644 index 0000000..554d1f2 --- /dev/null +++ b/docker/php/conf.d/app.prod.ini @@ -0,0 +1,12 @@ +apc.enable_cli = 1 +date.timezone = UTC +session.auto_start = Off +short_open_tag = Off + +# https://symfony.com/doc/current/performance.html +opcache.interned_strings_buffer = 16 +opcache.max_accelerated_files = 20000 +opcache.memory_consumption = 256 +opcache.validate_timestamps = 0 +realpath_cache_size = 4096K +realpath_cache_ttl = 600 diff --git a/docker/php/docker-entrypoint.sh b/docker/php/docker-entrypoint.sh new file mode 100644 index 0000000..7b9b182 --- /dev/null +++ b/docker/php/docker-entrypoint.sh @@ -0,0 +1,36 @@ +#!/bin/sh +set -e + +# first arg is `-f` or `--some-option` +if [ "${1#-}" != "$1" ]; then + set -- php-fpm "$@" +fi + +if [ "$1" = 'php-fpm' ] || [ "$1" = 'php' ] || [ "$1" = 'bin/console' ]; then + PHP_INI_RECOMMENDED="$PHP_INI_DIR/php.ini-production" + if [ "$APP_ENV" != 'prod' ]; then + PHP_INI_RECOMMENDED="$PHP_INI_DIR/php.ini-development" + fi + ln -sf "$PHP_INI_RECOMMENDED" "$PHP_INI_DIR/php.ini" + + mkdir -p var/cache var/log + setfacl -R -m u:www-data:rwX -m u:"$(whoami)":rwX var + setfacl -dR -m u:www-data:rwX -m u:"$(whoami)":rwX var + + bin/console debug:container --env-vars + + if [ "$APP_ENV" != 'prod' ]; then + composer install --prefer-dist --no-progress --no-suggest --no-interaction + fi + + echo "Waiting for db to be ready..." + until bin/console doctrine:query:sql "SELECT 1" > /dev/null 2>&1; do + sleep 1 + done + + if ls -A src/Migrations/*.php > /dev/null 2>&1; then + bin/console doctrine:migrations:migrate --no-interaction + fi +fi + +exec docker-php-entrypoint "$@" diff --git a/docker/php/docker-healthcheck.sh b/docker/php/docker-healthcheck.sh new file mode 100644 index 0000000..f121eda --- /dev/null +++ b/docker/php/docker-healthcheck.sh @@ -0,0 +1,12 @@ +#!/bin/sh +set -e + +export SCRIPT_NAME=/ping +export SCRIPT_FILENAME=/ping +export REQUEST_METHOD=GET + +if cgi-fcgi -bind -connect 127.0.0.1:9000; then + exit 0 +fi + +exit 1 diff --git a/docker/readme.md b/docker/readme.md new file mode 100644 index 0000000..c0ea8a8 --- /dev/null +++ b/docker/readme.md @@ -0,0 +1,72 @@ +## Run in prod mode +``` +APP_ENV=prod bin/copy-env.sh +bin/build.sh +COMPOSE_FILE=docker-compose.prod.yml docker-compose down +COMPOSE_FILE=docker-compose.prod.yml docker-compose up +open http://localhost:8083/login + +#cleanup +docker volume rm easyquickimportdev_postgres +docker-compose down --remove-orphans +``` + +## Tests + +``` +cd docker +DOCKER_ENV=test APP_ENV=test bin/copy-env.sh +bin/build.sh +docker-compose run --rm php sh -c "bin/run-tests.sh" +#cleanup mysql +docker-compose down --remove-orphans +``` + + +### Push +``` +bin/push.sh +``` + +### Run +``` +eval $(docker-machine env scw0) +echo 'password' | docker login registry.optdeal.com -u karser --password-stdin +docker-compose pull +docker-compose down --remove-orphans; docker-compose up -d +docker-compose logs -f + +#clear cache (a bug) +docker exec -it -u app easyquickimport_app_1 sh +rm -rf ./var/cache/* +``` + +### Deployment + +``` +git remote add gitlab ssh://git@gitlab.dev.easyquickimport.com:2222/karser/trackmagic.git +git push gitlab master -f +#everything: +git fetch --prune +git remote set-head origin -d +git push --prune gitlab +refs/remotes/origin/*:refs/heads/* +refs/tags/*:refs/tags/* +``` + + +### Database in docker +``` +docker stop qb_api_mysql || true \ + && docker rm qb_api_mysql || true + +sudo rm -rf ./var/db/* + +docker run --name qb_api_mysql -d \ + -v /home/karser/dev/projects/easyimport/easyimport/var/db/:/var/lib/mysql \ + -p 3316:3306 \ + -e MYSQL_ROOT_PWD=123 \ + -e MYSQL_USER=dev \ + -e MYSQL_USER_PWD=dev \ + -e MYSQL_USER_DB=qb_api \ + --restart unless-stopped \ + leafney/alpine-mariadb:10.2.22 +``` diff --git a/phpstan-tests.neon b/phpstan-tests.neon new file mode 100644 index 0000000..8efecf8 --- /dev/null +++ b/phpstan-tests.neon @@ -0,0 +1,18 @@ +includes: + - vendor/phpstan/phpstan-doctrine/extension.neon + - vendor/phpstan/phpstan-symfony/extension.neon + - vendor/phpstan/phpstan-deprecation-rules/rules.neon + - vendor/phpstan/phpstan-webmozart-assert/extension.neon + - vendor/phpstan/phpstan-strict-rules/rules.neon + - vendor/phpstan/phpstan-phpunit/extension.neon + +parameters: + level: 3 + reportUnmatchedIgnoredErrors: false + inferPrivatePropertyTypeFromConstructor: true + paths: + - %currentWorkingDirectory%/tests + symfony: + container_xml_path: %rootDir%/../../../var/cache/test/srcApp_KernelTestDebugContainer.xml + autoload_files: + - vendor/bin/.phpunit/phpunit-7.4/vendor/autoload.php diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..328ddf5 --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,22 @@ +includes: + - vendor/phpstan/phpstan-doctrine/extension.neon + - vendor/phpstan/phpstan-symfony/extension.neon + - vendor/phpstan/phpstan-deprecation-rules/rules.neon + - vendor/phpstan/phpstan-webmozart-assert/extension.neon + - vendor/phpstan/phpstan-strict-rules/rules.neon + +parameters: + level: 8 + reportUnmatchedIgnoredErrors: false + inferPrivatePropertyTypeFromConstructor: true + checkMissingIterableValueType: false + paths: + - %currentWorkingDirectory%/src + ignoreErrors: + - '#Parameter \#\d \$\w+ of method QuickBooks_QBXML_Object#' + + excludes_analyse: + - %currentWorkingDirectory%/src/Migrations/* + + symfony: + container_xml_path: %rootDir%/../../../var/cache/dev/srcApp_KernelDevDebugContainer.xml diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..319adab --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + tests + + + + + + src + + + + + + + diff --git a/public/index.php b/public/index.php new file mode 100644 index 0000000..0e30370 --- /dev/null +++ b/public/index.php @@ -0,0 +1,27 @@ +handle($request); +$response->send(); +$kernel->terminate($request, $response); diff --git a/public/style.css b/public/style.css new file mode 100644 index 0000000..a2e5d1e --- /dev/null +++ b/public/style.css @@ -0,0 +1,169 @@ +/* + DEMO STYLE +*/ + +@import "https://fonts.googleapis.com/css?family=Poppins:300,400,500,600,700"; +body { + font-family: 'Poppins', sans-serif; + background: #fafafa; +} + +p { + font-family: 'Poppins', sans-serif; + font-size: 1.1em; + font-weight: 300; + line-height: 1.7em; + color: #999; +} + +a, +a:hover, +a:focus { + color: inherit; + text-decoration: none; + transition: all 0.3s; +} + +.navbar { + padding: 15px 10px; + background: #fff; + border: none; + border-radius: 0; + margin-bottom: 40px; + box-shadow: 1px 1px 3px rgba(0, 0, 0, 0.1); +} + +.navbar-btn { + box-shadow: none; + outline: none !important; + border: none; +} + +.line { + width: 100%; + height: 1px; + border-bottom: 1px dashed #ddd; + margin: 40px 0; +} + +/* --------------------------------------------------- + SIDEBAR STYLE +----------------------------------------------------- */ + +.wrapper { + display: flex; + width: 100%; + align-items: stretch; +} + +#sidebar { + min-width: 250px; + max-width: 250px; + background: #7386D5; + color: #fff; + transition: all 0.3s; +} + +#sidebar.active { + margin-left: -250px; +} + +#sidebar .sidebar-header { + padding: 20px; + background: #6d7fcc; +} + +#sidebar ul.components { + padding: 0 5px; + border-bottom: 1px solid #47748b; +} + +#sidebar ul p { + color: #fff; + padding: 10px; +} + +#sidebar ul li a { + padding: 10px; + font-size: 1.1em; + display: block; +} + +#sidebar ul li a:hover { + color: #7386D5; + background: #fff; +} + +#sidebar ul li.active>a, +a[aria-expanded="true"] { + color: #fff; + background: #6d7fcc; +} + +a[data-toggle="collapse"] { + position: relative; +} + +.dropdown-toggle::after { + display: block; + position: absolute; + top: 50%; + right: 20px; + transform: translateY(-50%); +} + +ul ul a { + font-size: 0.9em !important; + padding-left: 30px !important; + background: #6d7fcc; +} + +ul.CTAs { + padding: 20px 20px 0; +} + +ul.CTAs a { + text-align: center; + font-size: 0.9em !important; + display: block; + border-radius: 5px; + margin-bottom: 5px; +} + +a.download { + background: #fff; + color: #7386D5; +} + +a.article, +a.article:hover { + background: #6d7fcc !important; + color: #fff !important; +} + +/* --------------------------------------------------- + CONTENT STYLE +----------------------------------------------------- */ + +#content { + width: 100%; + padding: 20px; + min-height: 100vh; + transition: all 0.3s; +} + +/* --------------------------------------------------- + MEDIAQUERIES +----------------------------------------------------- */ + +@media (max-width: 768px) { + #sidebar { + margin-left: -250px; + } + #sidebar.active { + margin-left: 0; + } + #sidebarCollapse span { + display: none; + } +} \ No newline at end of file diff --git a/src/Accounts/AccountsUpdater.php b/src/Accounts/AccountsUpdater.php new file mode 100644 index 0000000..46226e9 --- /dev/null +++ b/src/Accounts/AccountsUpdater.php @@ -0,0 +1,86 @@ +quickbooksFormatter = $quickbooksFormatter; + $this->quickbooksServer = $quickbooksServer; + $this->em = $em; + } + + public function scheduleUpdate(string $username): bool + { + $account = new QuickBooks_QBXML_Object_Account(); + + $queryXml = $this->quickbooksFormatter->formatForOutput($account->asQBXML(QUICKBOOKS_QUERY_ACCOUNT)); + return $this->quickbooksServer->schedule($username, QUICKBOOKS_QUERY_ACCOUNT, self::ACCOUNTS_UPDATE_REQUEST_ID, $queryXml); + } + + public function update(string $qbUsername, string $xml): void + { + $parser = new \QuickBooks_XML_Parser($xml); + /** @var QuickBooks_XML_Document|false $doc */ + $doc = $parser->parse($errnum, $errmsg); + if ($doc === false) { + throw new RuntimeException("Unable to parse AccountsXML. {$errnum}:{$errmsg}"); + } + + $companyRepo = $this->em->getRepository(QuickbooksCompany::class); + $company = $companyRepo->findOneBy(['qbUsername' => $qbUsername]); + Assert::notNull($company); + $user = $company->getUser(); + Assert::notNull($user); + + /** @var QuickbooksAccountRepositoryInterface $repo */ + $repo = $this->em->getRepository(QuickbooksAccount::class); + $repo->deleteAll($company); + + $root = $doc->getRoot(); + $List = $root->getChildAt('QBXML/QBXMLMsgsRs/AccountQueryRs'); + foreach ($List->children() as $child) { + $accountXml = $child->getChildAt('AccountRet'); + /** @var QuickBooks_QBXML_Object_Account|false $dto */ + $dto = QuickBooks_QBXML_Object::fromXML($accountXml, QUICKBOOKS_QUERY_ACCOUNT); + Assert::isInstanceOf($dto, QuickBooks_QBXML_Object_Account::class); + $entity = $this->fromDto($dto); + $entity->setUser($user); + $entity->setCompany($company); + $this->em->persist($entity); + } + $this->em->flush(); + } + + private function fromDto(QuickBooks_QBXML_Object_Account $dto): QuickbooksAccount + { + $entity = new QuickbooksAccount(); + $entity->setFullName($dto->getFullName()); + $entity->setCurrency($dto->get('CurrencyRef FullName') ?? 'US Dollar'); + $entity->setAccountType($dto->getAccountType()); + $entity->setSpecialAccountType($dto->getSpecialAccountType()); + $entity->setAccountNumber($dto->getAccountNumber()); + return $entity; + } +} diff --git a/src/Accounts/UpdateAccountsSubscriber.php b/src/Accounts/UpdateAccountsSubscriber.php new file mode 100644 index 0000000..4d406fd --- /dev/null +++ b/src/Accounts/UpdateAccountsSubscriber.php @@ -0,0 +1,30 @@ +accountsUpdater = $accountsUpdater; + } + + public function onResponse(QuickbooksServerResponseEvent $event): void + { + if ($event->getIdent() === AccountsUpdater::ACCOUNTS_UPDATE_REQUEST_ID) { + $this->accountsUpdater->update($event->getUser(), $event->getXml()); + } + } + + public static function getSubscribedEvents(): array + { + return [ + QuickbooksServerResponseEvent::class => 'onResponse', + ]; + } +} diff --git a/src/Command/CreateUserCommand.php b/src/Command/CreateUserCommand.php new file mode 100644 index 0000000..a35eaf9 --- /dev/null +++ b/src/Command/CreateUserCommand.php @@ -0,0 +1,47 @@ +userService = $userService; + } + + protected function configure(): void + { + $this->addArgument('email', InputArgument::REQUIRED, 'The email of the user'); + $this->addOption('password', 'p',InputOption::VALUE_REQUIRED, 'The password of the user'); + $this->addOption('admin', null, InputOption::VALUE_NONE, 'Adds ROLE_ADMIN'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + /** @var string $email */ + $email = $input->getArgument('email'); + $password = $input->getOption('password'); + Assert::string($password); + $isAdmin = $input->getOption('admin'); + Assert::boolean($isAdmin); + $roles = [$isAdmin ? 'ROLE_ADMIN' : 'ROLE_USER']; + $user = $this->userService->createUser($email, $password, $roles); + + $output->writeln('The user has been created with id '.$user->getId()); + + return 0; + } +} diff --git a/src/Command/GetCurrencyRateCommand.php b/src/Command/GetCurrencyRateCommand.php new file mode 100644 index 0000000..a9084af --- /dev/null +++ b/src/Command/GetCurrencyRateCommand.php @@ -0,0 +1,50 @@ +currencyExchanger = $currencyExchanger; + } + + protected function configure(): void + { + $this->addArgument('target', InputArgument::REQUIRED, ''); + + $this->addOption('base', null,InputOption::VALUE_REQUIRED, 'HKD', 'HKD'); + $this->addOption('date', null,InputOption::VALUE_REQUIRED, 'Y-m-d', date('Y-m-d')); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $target = $input->getArgument('target'); + Assert::string($target); + + $base = $input->getOption('base'); + Assert::string($base); + + $date = $input->getOption('date'); + Assert::string($date); + + $rate = $this->currencyExchanger->getExchangeRate($base, $target, $date); + $output->writeln(sprintf('%.10f', $rate)); + + return 0; + + } +} diff --git a/src/Controller/.gitignore b/src/Controller/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/src/Controller/AdminController.php b/src/Controller/AdminController.php new file mode 100644 index 0000000..bbdfaef --- /dev/null +++ b/src/Controller/AdminController.php @@ -0,0 +1,67 @@ +server = $server; + $this->em = $em; + } + + public function createQuickbooksCompanyEntityFormBuilder(object $entity, string $view): FormBuilderInterface + { + $formBuilder = $this->createEntityFormBuilder($entity, $view); + $fields = $formBuilder->all(); + foreach ($fields as $fieldId => $field) { + if ($fieldId === 'baseCurrency') { + $map = new CurrencyMap(); + + $formBuilder->add($fieldId, ChoiceType::class, [ + 'placeholder' => 'select option', + 'required' => false, + 'choices' => $map->getFormChoices(), + ]); + } + } + + return $formBuilder; + } + + public function downloadQbwcConfigAction(?Request $request = null): Response + { + Assert::notNull($request = $this->request ?? $request); + $qbUsername = $request->query->get('id'); + $entity = $this->em->getRepository(QuickbooksCompany::class)->findOneBy(['qbUsername' => $qbUsername]); + Assert::isInstanceOf($entity, QuickbooksCompany::class, "Company with id {$qbUsername} not found"); + + return new Response($this->server->config($entity), 200, [ + 'Content-type' => 'text/xml', + 'Content-Disposition' => 'attachment; filename="'.$qbUsername.'.qwc"', + ]); + } + + public function syncAccountsAction(?Request $request = null): Response + { + Assert::notNull($request = $this->request ?? $request); + $qbUsername = $request->query->get('id'); + return $this->redirectToRoute('app_schedule_accounts', ['qbUsername' => $qbUsername]); + } +} diff --git a/src/Controller/DocumentationController.php b/src/Controller/DocumentationController.php new file mode 100644 index 0000000..7f1ed99 --- /dev/null +++ b/src/Controller/DocumentationController.php @@ -0,0 +1,14 @@ +render('documentation/index.html.twig'); + } +} diff --git a/src/Controller/ImportController.php b/src/Controller/ImportController.php new file mode 100644 index 0000000..f9b6730 --- /dev/null +++ b/src/Controller/ImportController.php @@ -0,0 +1,99 @@ +bind($import); + + // form of the current step + $form = $flow->createForm(); + + $entities = []; + $errors = []; + try { + if ($flow->isValid($form)) { + $flow->saveCurrentStepData($form); + + if ($flow->getCurrentStepLabel() === 'import_wizard_step.mapping') { + Assert::notNull($file = $import->getFile()); + $items = $sheetScheduler->loadFile($file, $import->getFieldsMapping()); + Assert::notNull($type = $import->getImportType()); + Assert::notNull($company = $import->getCompany()); + + $entities = $sheetScheduler->denormalize($type, $items); + Assert::notNull($dateFormat = $import->getDateFormat()); + $entities = $sheetScheduler->canonizeDate($entities, $dateFormat); + $sheetScheduler->validateAllEntities($company, $entities); + } + + if ($flow->nextStep()) { + // form for the next step + $form = $flow->createForm(); + } else { + // flow finished + $company = $import->getCompany(); + Assert::notNull($company); + $type = $import->getImportType(); + Assert::notNull($type); + $file = $import->getFile(); + Assert::notNull($file); + + $scheduled = $sheetScheduler->schedule($company, $type, $file, $import->getFieldsMapping(), $import->getDateFormat()); + $this->addFlash('success', "{$scheduled} record(s) have been scheduled"); + $flow->reset(); + + return $this->redirect($this->generateUrl('app_import_scheduled')); + } + } + } catch (ValidationsException $e) { + $errors = array_map(fn (AppException $nE) => nl2br($nE->getMessage()), $e->getExceptions()); + } catch (AppException $e) { + $errors = [nl2br($e->getMessage())]; + } + + return $this->render('import/create.html.twig', [ + 'form' => $form->createView(), + 'flow' => $flow, + 'entities' => $entities, + 'errors' => array_values(array_unique($errors)), + ]); + } + + public function scheduled(): Response + { + return $this->render('import/scheduled.html.twig', []); + } + + public function sample(SampleSheetGenerator $sampleSheetGenerator, string $type): Response + { + $content = $sampleSheetGenerator->generateSampleCsv($type); + return $this->downloadFileResponse($type.'-sample.csv', $content); + } + + private function downloadFileResponse(string $filename, string $content): Response + { + $response = new Response(); + $response->headers->set('Cache-Control', 'private'); + $response->headers->set('Content-type', 'text/csv'); + $response->headers->set('Content-Disposition', 'attachment; filename="' . $filename . '"'); + $response->headers->set('Content-length', (string)strlen($content)); + $response->setContent($content); + + return $response; + } +} diff --git a/src/Controller/QbwcController.php b/src/Controller/QbwcController.php new file mode 100644 index 0000000..a482ed5 --- /dev/null +++ b/src/Controller/QbwcController.php @@ -0,0 +1,30 @@ +server = $server; + } + + public function server(Request $request, string $appName): Response + { + $content = $request->getContent(); + $isXml = $request->getMethod() === 'POST' || $request->query->has('wsdl') || $request->query->has('WSDL'); + $contentType = $isXml ? 'text/xml' : 'text/plain'; + if ($isXml) { + $output = $this->server->qbwc($content); + } else { + $output = $appName; + } + return new Response($output, 200, ['Content-Type' => $contentType]); + } +} diff --git a/src/Controller/RegistrationController.php b/src/Controller/RegistrationController.php new file mode 100644 index 0000000..458485f --- /dev/null +++ b/src/Controller/RegistrationController.php @@ -0,0 +1,51 @@ +setRoles(['ROLE_USER']); + $form = $this->createForm(RegistrationFormType::class, $user); + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + $user->setPassword( + $passwordEncoder->encodePassword( + $user, + $form->get('plainPassword')->getData() + ) + ); + + $entityManager = $this->getDoctrine()->getManager(); + $entityManager->persist($user); + $entityManager->flush(); + + return $guardHandler->authenticateUserAndHandleSuccess( + $user, + $request, + $authenticator, + 'main' + ); + } + + return $this->render('registration/register.html.twig', [ + 'registrationForm' => $form->createView(), + ]); + } +} diff --git a/src/Controller/ResetPasswordController.php b/src/Controller/ResetPasswordController.php new file mode 100644 index 0000000..4ee88d9 --- /dev/null +++ b/src/Controller/ResetPasswordController.php @@ -0,0 +1,178 @@ +resetPasswordHelper = $resetPasswordHelper; + $this->fromAddress = $fromAddress; + } + + /** + * Display & process form to request a password reset. + * + * @Route("", name="app_forgot_password_request") + */ + public function request(Request $request, MailerInterface $mailer): Response + { + $form = $this->createForm(ResetPasswordRequestFormType::class); + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + return $this->processSendingPasswordResetEmail( + $form->get('email')->getData(), + $mailer + ); + } + + return $this->render('reset_password/request.html.twig', [ + 'requestForm' => $form->createView(), + ]); + } + + /** + * Confirmation page after a user has requested a password reset. + * + * @Route("/check-email", name="app_check_email") + */ + public function checkEmail(): Response + { + // We prevent users from directly accessing this page + if (!$this->canCheckEmail()) { + return $this->redirectToRoute('app_forgot_password_request'); + } + + return $this->render('reset_password/check_email.html.twig', [ + 'tokenLifetime' => $this->resetPasswordHelper->getTokenLifetime(), + ]); + } + + /** + * Validates and process the reset URL that the user clicked in their email. + * + * @Route("/reset/{token}", name="app_reset_password") + */ + public function reset(Request $request, UserPasswordEncoderInterface $passwordEncoder, ?string $token = null): Response + { + if (null !== $token) { + // We store the token in session and remove it from the URL, to avoid the URL being + // loaded in a browser and potentially leaking the token to 3rd party JavaScript. + $this->storeTokenInSession($token); + + return $this->redirectToRoute('app_reset_password'); + } + + $token = $this->getTokenFromSession(); + if (null === $token) { + throw $this->createNotFoundException('No reset password token found in the URL or in the session.'); + } + + try { + $user = $this->resetPasswordHelper->validateTokenAndFetchUser($token); + Assert::isInstanceOf($user, User::class); + } catch (ResetPasswordExceptionInterface $e) { + $this->addFlash('reset_password_error', sprintf( + 'There was a problem validating your reset request - %s', + $e->getReason() + )); + + return $this->redirectToRoute('app_forgot_password_request'); + } + + // The token is valid; allow the user to change their password. + $form = $this->createForm(ChangePasswordFormType::class); + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + // A password reset token should be used only once, remove it. + $this->resetPasswordHelper->removeResetRequest($token); + + // Encode the plain password, and set it. + $encodedPassword = $passwordEncoder->encodePassword( + $user, + $form->get('plainPassword')->getData() + ); + + $user->setPassword($encodedPassword); + $this->getDoctrine()->getManager()->flush(); + + // The session is cleaned up after the password has been changed. + $this->cleanSessionAfterReset(); + + return $this->redirectToRoute('app_homepage'); + } + + return $this->render('reset_password/reset.html.twig', [ + 'resetForm' => $form->createView(), + ]); + } + + private function processSendingPasswordResetEmail(string $emailFormData, MailerInterface $mailer): RedirectResponse + { + $user = $this->getDoctrine()->getRepository(User::class)->findOneBy([ + 'email' => $emailFormData, + ]); + + // Marks that you are allowed to see the app_check_email page. + $this->setCanCheckEmailInSession(); + + // Do not reveal whether a user account was found or not. + if (null === $user) { + return $this->redirectToRoute('app_check_email'); + } + + try { + $resetToken = $this->resetPasswordHelper->generateResetToken($user); + } catch (ResetPasswordExceptionInterface $e) { + $this->addFlash('reset_password_error', sprintf( + 'There was a problem handling your password reset request - %s', + $e->getReason() + )); + + return $this->redirectToRoute('app_forgot_password_request'); + } + + Assert::notNull($toAddress = $user->getEmail()); + $email = (new TemplatedEmail()) + ->from($this->fromAddress) + ->to($toAddress) + ->subject('Your password reset request') + ->htmlTemplate('reset_password/email.html.twig') + ->context([ + 'resetToken' => $resetToken, + 'tokenLifetime' => $this->resetPasswordHelper->getTokenLifetime(), + ]) + ; + + $mailer->send($email); + + return $this->redirectToRoute('app_check_email'); + } +} diff --git a/src/Controller/SchedulingController.php b/src/Controller/SchedulingController.php new file mode 100644 index 0000000..9910f58 --- /dev/null +++ b/src/Controller/SchedulingController.php @@ -0,0 +1,176 @@ +server = $server; + $this->em = $em; + } + + public function testSchedule(): Response + { +// $this->server->truncateQueue(); + $formatter = new QuickbooksFormatter(); + $this->server->schedule(null, QUICKBOOKS_QUERY_COMPANY, '100'); +// $this->server->schedule(null, QUICKBOOKS_ADD_CUSTOMER, '100', $formatter->getTestCustomerAdd()); +// $this->server->schedule(null, QUICKBOOKS_ADD_RECEIVE_PAYMENT, '100', $formatter->getTestPaymentReceiveAdd()); +// $this->server->schedule(null, QUICKBOOKS_ADD_INVOICE, '100', $formatter->getTestInvoiceAdd()); +// $this->server->schedule(null, QUICKBOOKS_ADD_VENDOR, '100', $formatter->getTestVendorAdd()); +// $this->server->schedule(null, QUICKBOOKS_ADD_BILL, '100', $formatter->getTestBillAdd()); +// $this->server->schedule(null, QUICKBOOKS_ADD_BILLPAYMENTCHECK, '100', $formatter->getTestBillPaymentCheckAdd()); +// $this->server->schedule(null, QUICKBOOKS_ADD_JOURNALENTRY, '100', $formatter->getTestJournalEntryAdd()); + + $this->addFlash('success', 'The queue tables have been truncated'); + + return $this->redirectToRoute('app_homepage'); + } + + public function schedule(Request $request, SheetScheduler $sheetScheduler, + TransactionsConverter $transactionsConverter, + SampleSheetGenerator $sampleSheetGenerator): Response + { + $truncateForm = $this->createForm(TruncateQueueType::class); + $truncateForm->handleRequest($request); + if ($truncateForm->isSubmitted() && $truncateForm->isValid()) { + /** @var bool $confirm */ + $confirm = $truncateForm->get('confirm')->getData(); + if ($confirm) { + $this->server->truncateQueue(); + $this->addFlash('success', 'The queue tables have been truncated'); + return $this->redirectToRoute('app_schedule'); + } + + } + + $scheduleForm = $this->createForm(ScheduleType::class); + $scheduleForm->handleRequest($request); + if ($scheduleForm->isSubmitted() && $scheduleForm->isValid()) { + $remoteFile = $scheduleForm->get('remote_file')->getData(); + /** @var UploadedFile|null $localFile */ + $localFile = $scheduleForm->get('local_file')->getData(); + try { + if ($remoteFile === null && $localFile === null) { + throw new RuntimeException('Either remote or local file must be submitted'); + } + $file = $localFile ?? $sheetScheduler->copyToLocal($remoteFile); + $type = $scheduleForm->get('type')->getData(); + /** @var QuickbooksCompany $user */ + $user = $scheduleForm->get('username')->getData(); + /** @var bool $dryRun */ + $dryRun = $scheduleForm->get('dry_run')->getData(); + if ($dryRun) { + $toSchedule = $sheetScheduler->dryRun($user, $type, $file); + return new Response($toSchedule, 200, [ + 'Content-Disposition' => 'attachment; filename="'.$type.'-dry-run.xml"', + 'Content-type' => 'text/xml', + ]); + } + $scheduled = $sheetScheduler->schedule($user, $type, $file); + $this->addFlash('success', "{$scheduled} record(s) have been scheduled"); + return $this->redirectToRoute('app_schedule'); + } catch (RuntimeException $e) { + $this->addFlash('error', nl2br($e->getMessage())); + return $this->redirectToRoute('app_schedule'); + } + } + + $sampleForm = $this->createForm(DownloadSampleSheetType::class); + $sampleForm->handleRequest($request); + if ($sampleForm->isSubmitted() && $sampleForm->isValid()) { + $type = $sampleForm->get('type')->getData(); + $content = $sampleSheetGenerator->generateSampleCsv($type); + return $this->downloadFileResponse($type.'-sample.csv', $content); + } + + $converterForm = $this->createForm(ConvertTransactionsType::class); + $converterForm->handleRequest($request); + if ($converterForm->isSubmitted() && $converterForm->isValid()) { + try { + $file = $converterForm->get('local_file')->getData(); + + /** @var QuickbooksCompany $user */ + $user = $converterForm->get('username')->getData(); + $username = $user->getQbUsername(); + Assert::notNull($username); + + $content = $transactionsConverter->convertWrapper($file, $username); + return $this->downloadFileResponse(time().'-transactions.csv', $content); + } catch (\Exception $e) { + $this->addFlash('error', nl2br($e->getMessage())); + return $this->redirectToRoute('app_schedule'); + } + } + + + return $this->render('scheduling/schedule.html.twig', [ + 'schedule_form' => $scheduleForm->createView(), + 'sample_form' => $sampleForm->createView(), + 'converter_form' => $converterForm->createView(), + 'truncate_form' => $truncateForm->createView(), + ]); + } + + public function scheduleAccounts(Request $request, AccountsUpdater $accountsUpdater, + QuickbooksCompanyRepositoryInterface $companyRepo): Response + { + $company = null !== ($username = $request->query->get('qbUsername')) ? $companyRepo->find($username) : null; + $scheduleAccountsUpdateForm = $this->createForm(ScheduleAccountsUpdateType::class, [ + 'company' => $company, + ]); + $scheduleAccountsUpdateForm->handleRequest($request); + if ($scheduleAccountsUpdateForm->isSubmitted() && $scheduleAccountsUpdateForm->isValid()) { + /** @var QuickbooksCompany $user */ + $user = $scheduleAccountsUpdateForm->get('company')->getData(); + $qbUsername = $user->getQbUsername(); + Assert::notNull($qbUsername); + if ($accountsUpdater->scheduleUpdate($qbUsername)) { + $this->addFlash('success', sprintf('Accounts update has been scheduled. Now go to QuickBooks and run Web Connector. Then go back and check Chart Of Accounts', + $this->generateUrl('easyadmin', ['entity' => 'QuickbooksAccount', 'action' => 'list']))); + } + return $this->redirectToRoute('app_schedule_accounts', ['qbUsername' => $qbUsername]); + } + + return $this->render('scheduling/accounts.html.twig', [ + 'schedule_accounts_update_form' => $scheduleAccountsUpdateForm->createView(), + ]); + } + + private function downloadFileResponse(string $filename, string $content): Response + { + $response = new Response(); + $response->headers->set('Cache-Control', 'private'); + $response->headers->set('Content-type', 'text/csv'); + $response->headers->set('Content-Disposition', 'attachment; filename="' . $filename . '"'); + $response->headers->set('Content-length', (string)strlen($content)); + $response->setContent($content); + + return $response; + } +} diff --git a/src/Controller/SecurityController.php b/src/Controller/SecurityController.php new file mode 100644 index 0000000..f106cbe --- /dev/null +++ b/src/Controller/SecurityController.php @@ -0,0 +1,36 @@ +getUser()) { + return $this->redirectToRoute('app_homepage'); + } + + // get the login error if there is one + $error = $authenticationUtils->getLastAuthenticationError(); + // last username entered by the user + $lastUsername = $authenticationUtils->getLastUsername(); + + return $this->render('security/login.html.twig', ['last_username' => $lastUsername, 'error' => $error]); + } + + /** + * @Route("/logout", name="app_logout") + */ + public function logout(): void + { + throw new \LogicException('This method can be blank - it will be intercepted by the logout key on your firewall.'); + } +} diff --git a/src/Currency/CachedCurrencyExchanger.php b/src/Currency/CachedCurrencyExchanger.php new file mode 100644 index 0000000..a490e15 --- /dev/null +++ b/src/Currency/CachedCurrencyExchanger.php @@ -0,0 +1,38 @@ +cache = $cache; + $this->decorated = $decorated; + } + + public function getExchangeRate(string $base, string $target, string $date): float + { + $key = strtolower(implode('|', [$base, $target, $date])); + try { + $value = $this->cache->get($key, function (ItemInterface $item) use ($base, $target, $date): float { +// $item->tag(["{$base}/{$target}", $date]); + return $this->decorated->getExchangeRate($base, $target, $date); + }); + } catch (InvalidArgumentException $e) { + throw new RuntimeException('Unable to obtain cached currency rate', $e->getCode(), $e); + } + + return $value; + } +} diff --git a/src/Currency/CurrencyAwareInterface.php b/src/Currency/CurrencyAwareInterface.php new file mode 100644 index 0000000..d7c8c00 --- /dev/null +++ b/src/Currency/CurrencyAwareInterface.php @@ -0,0 +1,14 @@ +requestFactory = $requestFactory; + } + + public function getExchangeRate(string $base, string $target, string $date): float + { + if ($base === $target) { + return 1.0; + } + $client = new Client(); + $exchangerEuroBased = new Chain([ + new EuropeanCentralBank($client, $this->requestFactory), + ]); + try { + $rateBase = $this->getInternalExchangeRate($exchangerEuroBased, "EUR/{$base}", $date); + $rateTarget = $this->getInternalExchangeRate($exchangerEuroBased, "EUR/{$target}", $date); + return $rateBase / $rateTarget; + } catch (\Throwable $e) { + throw new RuntimeException("Unable to get currency rate. Currency pair: {$base}/{$target}, date: {$date}. Error: {$e->getMessage()}"); + } + } + + public function getInternalExchangeRate(ExchangeRateService $exchanger, string $currencyPair, string $date): float + { + $query = new ExchangeRateQueryBuilder($currencyPair); + $query->setDate(new DateTime($date)); + return $exchanger->getExchangeRate($query->build())->getValue(); + } +} diff --git a/src/Currency/CurrencyExchangerInterface.php b/src/Currency/CurrencyExchangerInterface.php new file mode 100644 index 0000000..5a12742 --- /dev/null +++ b/src/Currency/CurrencyExchangerInterface.php @@ -0,0 +1,13 @@ + 'UAE Dirham', + 'AFN' => 'Afghan Afghani', + 'ALL' => 'Albanian Lek', + 'AMD' => 'Armenian Dram', + 'ANG' => 'Dutch Guilder', + 'AOA' => 'Angolan Kwanza', + 'ARS' => 'Argentine Peso', + 'AUD' => 'Australian Dollar', + 'AWG' => 'Aruban Florin', + 'AZN' => 'Azerbaijan Manat', + 'BAM' => 'Bosnian Mark', + 'BBD' => 'Barbadian Dollar', + 'BDT' => 'Bangladeshi Taka', + 'BGN' => 'Bulgarian Lev', + 'BHD' => 'Bahraini Dinar', + 'BIF' => 'Burundi Franc', + 'BMD' => 'Bermuda Dollar', + 'BND' => 'Brunei Dollar', + 'BOB' => 'Bolivian Boliviano', + 'BRL' => 'Brazilian Real', + 'BSD' => 'Bahamian Dollar', + 'BTC' => 'Bitcoin', + 'BTN' => 'Bhutanese Hgultrum', + 'BWP' => 'Botswana Pula', + 'BYN' => 'Belarussian Ruble', + 'BZD' => 'Belizean Dollar', + 'CAD' => 'Canadian Dollar', + 'CDF' => 'Congolese Franc', + 'CHK' => 'Swiss Franc', + 'CLP' => 'Chilean Peso', + 'CRC' => 'Costa Rica Colon', + 'CUP' => 'Cuban Peso', + 'CVE' => 'Cape Verde Escudo', + 'CZK' => 'Czech Koruna', + 'DJF' => 'Djibouti Franc', + 'DKK' => 'Danish Krone', + 'DOP' => 'Dominican Peso', + 'DZD' => 'Algerian Dinar', + 'EGP' => 'Egyptian Pound', + 'ERN' => 'Eritrean Nakfa', + 'EUR' => 'Euro', + 'ETB' => 'Ethiopian Birr', + 'ETH' => 'Ethereum', + 'FJD' => 'Fiji Dollar', + 'FKP' => 'Falkland Islands Pound', + 'GBP' => 'British Pound Sterling', + 'GEL' => 'Georgian Lari', + 'GHS' => 'Ghanaian Cedi', + 'GIP' => 'Gibraltar Pound', + 'GMD' => 'Gambian Dalasi', + 'GNF' => 'Guinea Franc', + 'GTQ' => 'Guatemalan Quetzal', + 'GYD' => 'Guyana Dollar', + 'HKD' => 'Hong Kong Dollar', + 'HNL' => 'Honduran Lempira', + 'HRK' => 'Croatian Kuna', + 'HTG' => 'Haiti Gourde', + 'HUF' => 'Hungarian Forint', + 'IDR' => 'Indonesian Rupiah', + 'ILS' => 'Israeli Shekel', + 'INR' => 'Indian Rupee', + 'IQD' => 'Iraqi Dinar', + 'IRR' => 'Iranian Rial', + 'ISK' => 'Iceland Krona', + 'JMD' => 'Jamaican Dollar', + 'JOD' => 'Jordanian Dinar', + 'JPY' => 'Japanese Yen', + 'KES' => 'Kenyan Shilling', + 'KGS' => 'Kyrgyzstani Som', + 'KHR' => 'Cambodian Riel', + 'KMF' => 'Comoro Franc', + 'KPW' => 'North Korean Won', + 'KRW' => 'South Korean Won', + 'KWD' => 'Kuwaiti Dinar', + 'KTD' => 'Cayman Islands Dollar', + 'KZT' => 'Kazakhstan Tenge', + 'LAK' => 'Lao Kip', + 'LBP' => 'Lebanese Pound', + 'LKR' => 'Sri Lankan Rupee', + 'LRD' => 'Liberian Dollar', + 'LSL' => 'Lesotho Loti', + 'LTC' => 'Litecoin', + 'LYD' => 'Libyan Dinar', + 'MAD' => 'Moroccan Dirham', + 'MDL' => 'Moldovan Leu', + 'MGA' => 'Malagasy Ariary', + 'MKD' => 'Macedonian Denar', + 'MMK' => 'Myanmar Kyat', + 'MNT' => 'Mongolian Tugrik', + 'MOP' => 'Macanese Pataca', + 'MRO' => 'Mauritanian Ouguiya', + 'MUR' => 'Mauritius Rupee', + 'MVR' => 'Maldives Rufiyaa', + 'MWK' => 'Malawian Kwacha', + 'MXN' => 'Mexican Peso', + 'MYR' => 'Malaysian Ringgit', + 'MZN' => 'Mozambique Metical', + 'NAD' => 'Namibian Dollar', + 'NGN' => 'Nigerian Naira', + 'NIO' => 'Nicaragua Cordoba', + 'NOK' => 'Norwegian Krone', + 'NPR' => 'Nepalese Rupee', + 'NZD' => 'New Zealand Dollar', + 'OMR' => 'Omani Rial', + 'PAB' => 'Panama Balboa', + 'PEN' => 'Peruvian Nuevo Sol', + 'PGK' => 'Papua New Guinean Kina', + 'PHP' => 'Philippine Peso', + 'PKR' => 'Pakistani Rupee', + 'PLN' => 'Polish Zloty', + 'PYG' => 'Paraguayan Guarani', + 'QAR' => 'Qatari Riyal', + 'RON' => 'Romanian Leu', + 'RSD' => 'Serbian Dinar', + 'RUB' => 'Russian Ruble/Rouble', + 'RWF' => 'Rwanda Franc', + 'SAR' => 'Saudi Riyal', + 'SBD' => 'Solomon Islands Dollar', + 'SCR' => 'Seychelles Rupee', + 'SDG' => 'Sudanese Pound', + 'SEK' => 'Swedish Krona', + 'SGD' => 'Singapore Dollar', + 'SHP' => 'St. Helena Pound', + 'SLL' => 'Sierra Leonean Leone', + 'SOS' => 'Somali Shilling', + 'SRD' => 'Surinam Dollar', + 'STD' => 'Sao Tome and Principe Dobra', + 'SVC' => 'El Salvador Colon', + 'SYP' => 'Syrian Pound', + 'SZL' => 'Swaziland Lilangeni', + 'THB' => 'Thai Baht', + 'TJS' => 'Tajikistani Somoni', + 'TMT' => 'Turkmenistani Manat', + 'TND' => 'Tunisian Dinar', + 'TOP' => 'Tonga Pa\'anga', + 'TRY' => 'Turkish Lira', + 'TTD' => 'Trinidad & Tobago Dollar', + 'TWD' => 'Taiwanese Dollar', + 'TZS' => 'Tanzanian Shilling', + 'UAH' => 'Ukrainian Hryvnia', + 'UGX' => 'Ugandan Shilling', + 'USD' => 'US Dollar', + 'UYU' => 'Uruguayan Peso', + 'UZS' => 'Uzbekistan Som', + 'VEF' => 'Venezuealan Bolivar Fuerte', + 'VND' => 'Vietnam Dong', + 'VUV' => 'Vanuatu Vatu', + 'WST' => 'Samoa Tala', + 'XAF' => 'CFA Franc', + 'XCD' => 'East Caribbean Dollar', + 'XOF' => 'CFA Franc', + 'XPF' => 'CFP Franc', + 'YER' => 'Yemeni Rial', + 'ZAR' => 'South African Rand', + 'ZMW' => 'Zambian Kwacha', + ]; + + const SYMBOLS = [ + 'HKD' => 'HK$', + 'RUB' => '₽', + 'EUR' => '€', + 'HUF' => 'Ft', + 'BGN' => 'лв', + 'USD' => '$', + ]; + + public function getFormChoices(): array + { + $choices = []; + foreach (self::CHOICES as $name => $label) { + $label .= ' - '.$name; + $choices[$label] = $name; + } + ksort($choices); + return $choices; + } + + public function findCurrency(?string $search): array + { + $search = $search ?? ''; + $search = strtolower($search); + foreach (self::CHOICES as $name => $label) { + if ($search === strtolower($label) || + $search === strtolower($name) || + $search === strtolower(self::SYMBOLS[$name] ?? '') + ) { + return [$name, $label]; + } + } + throw new RuntimeException("Unable to find currency for {$search}"); + } +} diff --git a/src/Currency/UpdateRateOnEntityScheduledSubscriber.php b/src/Currency/UpdateRateOnEntityScheduledSubscriber.php new file mode 100644 index 0000000..bebd306 --- /dev/null +++ b/src/Currency/UpdateRateOnEntityScheduledSubscriber.php @@ -0,0 +1,83 @@ +currencyExchanger = $currencyExchanger; + } + + public function onScheduled(EntityOnScheduledEvent $event): void + { + $user = $event->getUser(); + $base = $user->getBaseCurrency(); + if ($base === null || trim($base) === '') { + return; + } + foreach ($event->getEntities() as $entity) { + if (!$entity instanceof CurrencyAwareInterface || !$this->isEligible($entity)) { + continue; + } + $this->updateRate($entity, $base, $event->getLine()); + } + } + + private function updateRate(CurrencyAwareInterface $entity, string $base, int $line): void + { + $txnDate = $this->normalizeDate($entity->getTxnDate()); + $currencyMap = new CurrencyMap(); + + try { + [$target] = $currencyMap->findCurrency($entity->getCurrency()); + } catch (RuntimeException $e) { + throw new RuntimeException("Unable to find currency for {$entity->getCurrency()}", $e->getCode(), $e); + } + try { + $rate = $this->currencyExchanger->getExchangeRate($base, $target, $txnDate); + $entity->setExchangeRate((string)$rate); + } catch (RuntimeException $e) { + throw new RuntimeException("Unable to update currency rate for {$base}/{$target}, date: {$txnDate}, line {$line}", $e->getCode(), $e); + } + } + + private function isEligible(CurrencyAwareInterface $entity): bool + { + $exchangeRate = $entity->getExchangeRate(); + if (!($exchangeRate === null || trim($exchangeRate) === '')) { + return false; + } + $targetCurrency = $entity->getCurrency(); + if ($targetCurrency === null || trim($targetCurrency) === '') { + return false; + } + return true; + } + + private function normalizeDate(?string $date): string + { + if (($date !== null) && (trim($date) === '')) { + $date = null; + } + if ($date === null) { + $date = date('Y-m-d'); + } + + return $date; + } + + public static function getSubscribedEvents(): array + { + return [ + EntityOnScheduledEvent::class => ['onScheduled', EntityOnScheduledEvent::PRIORITY_UPDATE], + ]; + } +} diff --git a/src/DataFixtures/AppFixtures.php b/src/DataFixtures/AppFixtures.php new file mode 100644 index 0000000..daa71ac --- /dev/null +++ b/src/DataFixtures/AppFixtures.php @@ -0,0 +1,32 @@ +userService = $userService; + } + + public function load(ObjectManager $manager): void + { + $user = $this->userService->createUser('user@example.com', 'pass123'); + + $company = new QuickbooksCompany('3607a3e9-ee11-4787-bd43-cdcb048038ad'); + $company->setQbCompanyFile('C:\\Users\\Public\\Documents\\Intuit\\QuickBooks\\Company Files\\Acme Inc.qbw'); + $company->setCompanyName('Acme Inc'); + $company->setQbPassword('Ohkq3AO'); + $company->setUser($user); + $manager->persist($company); + + $manager->flush(); + } +} diff --git a/src/DependencyInjection/Compiler/AddEntityTransformers.php b/src/DependencyInjection/Compiler/AddEntityTransformers.php new file mode 100644 index 0000000..c934d68 --- /dev/null +++ b/src/DependencyInjection/Compiler/AddEntityTransformers.php @@ -0,0 +1,27 @@ +attachHandlers($container, TransformerResolver::class, 'entity.transformer'); + } + + private function attachHandlers(ContainerBuilder $container, string $service_name, string $tag): void + { + if (!$container->has($service_name)) { + return; + } + $manager = $container->findDefinition($service_name); + foreach ($container->findTaggedServiceIds($tag) as $id => $attr) { + $manager->addMethodCall('addEntityTransformer', array(new Reference($id))); + } + } +} diff --git a/src/EasyAdmin/EasyAdminSubscriber.php b/src/EasyAdmin/EasyAdminSubscriber.php new file mode 100644 index 0000000..7f2cd27 --- /dev/null +++ b/src/EasyAdmin/EasyAdminSubscriber.php @@ -0,0 +1,36 @@ +security = $security; + } + + public function setDefaultsToCompany(GenericEvent $event): void + { + $entity = $event->getSubject(); + + if ($entity instanceof QuickbooksCompany && ($user = $this->security->getUser()) instanceof User) { + $entity->setUser($user); + $event['entity'] = $entity; + } + } + + public static function getSubscribedEvents() + { + return [ + 'easy_admin.pre_persist' => ['setDefaultsToCompany'], + ]; + } +} diff --git a/src/Entity/.gitignore b/src/Entity/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/src/Entity/Import.php b/src/Entity/Import.php new file mode 100644 index 0000000..1ae5b26 --- /dev/null +++ b/src/Entity/Import.php @@ -0,0 +1,68 @@ +fieldsMapping ?? []; + } + + public function setFieldsMapping(?array $fieldsMapping): void + { + $this->fieldsMapping = $fieldsMapping; + } + + public function getCompany(): ?QuickbooksCompany + { + return $this->company; + } + + public function setCompany(?QuickbooksCompany $company): void + { + $this->company = $company; + } + + public function getImportType(): ?string + { + return $this->importType; + } + + public function setImportType(?string $importType): void + { + $this->importType = $importType; + } + + public function getDateFormat(): ?string + { + return $this->dateFormat; + } + + public function setDateFormat(?string $dateFormat): void + { + $this->dateFormat = $dateFormat; + } + + public function getFile(): ?UploadedFile + { + return $this->file; + } + + public function setFile(?UploadedFile $file): void + { + $this->file = $file; + } +} diff --git a/src/Entity/QuickbooksAccount.php b/src/Entity/QuickbooksAccount.php new file mode 100644 index 0000000..692334c --- /dev/null +++ b/src/Entity/QuickbooksAccount.php @@ -0,0 +1,203 @@ +id; + } + + public function getUser(): ?User + { + return $this->user; + } + + public function setUser(?User $user): void + { + $this->user = $user; + } + + public function getQbUsername(): ?string + { + return null !== $this->company ? $this->company->getQbUsername() : null; + } + + public function getCompanyName(): ?string + { + return null !== $this->company ? $this->company->getCompanyName() : null; + } + + public function getCompany(): ?QuickbooksCompany + { + return $this->company; + } + + public function setCompany(?QuickbooksCompany $company): void + { + $this->company = $company; + } + + /** + * @return string|null + */ + public function getFullName(): ?string + { + return $this->fullName; + } + + /** + * @param string|null $fullName + */ + public function setFullName(?string $fullName): void + { + $this->fullName = $fullName; + } + + /** + * @return string|null + */ + public function getCurrency(): ?string + { + return $this->currency; + } + + /** + * @param string|null $currency + */ + public function setCurrency(?string $currency): void + { + $this->currency = $currency; + } + + /** + * @return string|null + */ + public function getAccountType(): ?string + { + return $this->accountType; + } + + /** + * @param string|null $accountType + */ + public function setAccountType(?string $accountType): void + { + $this->accountType = $accountType; + } + + /** + * @return string|null + */ + public function getSpecialAccountType(): ?string + { + return $this->specialAccountType; + } + + /** + * @param string|null $specialAccountType + */ + public function setSpecialAccountType(?string $specialAccountType): void + { + $this->specialAccountType = $specialAccountType; + } + + /** + * @return string|null + */ + public function getAccountNumber(): ?string + { + return $this->accountNumber; + } + + /** + * @param string|null $accountNumber + */ + public function setAccountNumber(?string $accountNumber): void + { + $this->accountNumber = $accountNumber; + } +} diff --git a/src/Entity/QuickbooksAccountRepositoryInterface.php b/src/Entity/QuickbooksAccountRepositoryInterface.php new file mode 100644 index 0000000..33c378d --- /dev/null +++ b/src/Entity/QuickbooksAccountRepositoryInterface.php @@ -0,0 +1,17 @@ + + */ +interface QuickbooksAccountRepositoryInterface extends ObjectRepository +{ + public function deleteAll(QuickbooksCompany $company): int; + + public function getCurrency(string $username, string $accountName, ?string $accountType = null): ?string; + + public function findOneByName(string $username, string $accountName): ?QuickbooksAccount; +} diff --git a/src/Entity/QuickbooksCompany.php b/src/Entity/QuickbooksCompany.php new file mode 100644 index 0000000..d52f176 --- /dev/null +++ b/src/Entity/QuickbooksCompany.php @@ -0,0 +1,345 @@ +qbUsername = $qbUsername ?? Uuid::uuid4()->toString(); + $this->status = 'e'; + $this->baseCurrency = 'USD'; + $this->decimalSymbol = self::DEFAULT_DECIMAL_SYMBOL; + $this->digitGroupingSymbol = self::DEFAULT_DIGIT_GROUPING_SYMBOL; + $this->qbwcMinRunEveryNSeconds = 0; + $this->qbwcWaitBeforeNextUpdate = 0; + $this->writeDatetime = new \DateTime(); + $this->touchDatetime = new \DateTime(); + } + + const STATUS_LABELS = [ +// 'q' => 'Queued', +// 's' => 'Success', +// 'e' => 'Error', + ]; + + public function __toString(): string + { + return $this->companyName ?? $this->qbUsername ?? ''; + } + + public function getStatusLabel(): ?string + { + return self::STATUS_LABELS[$this->status] ?? $this->status; + } + + public function getUser(): ?User + { + return $this->user; + } + + public function setUser(?User $user): void + { + $this->user = $user; + if (null !== $user) { + $user->addCompany($this); + } + } + + public function isMultiCurrencyEnabled(): bool + { + return $this->multiCurrencyEnabled; + } + + public function setMultiCurrencyEnabled(bool $multiCurrencyEnabled): void + { + $this->multiCurrencyEnabled = $multiCurrencyEnabled; + } + + public function getDecimalSymbol(): ?string + { + return $this->decimalSymbol; + } + + public function setDecimalSymbol(?string $decimalSymbol): void + { + $this->decimalSymbol = $decimalSymbol; + } + + public function getDigitGroupingSymbol(): ?string + { + return $this->digitGroupingSymbol; + } + + public function setDigitGroupingSymbol(?string $digitGroupingSymbol): void + { + $this->digitGroupingSymbol = $digitGroupingSymbol; + } + + public function getCompanyName(): ?string + { + return $this->companyName; + } + + public function setCompanyName(?string $companyName): void + { + $this->companyName = $companyName; + } + + /** + * @return string|null + */ + public function getQbUsername(): ?string + { + return $this->qbUsername; + } + + /** + * @param string|null $qbUsername + */ + public function setQbUsername(?string $qbUsername): void + { + $this->qbUsername = $qbUsername !== null ? mb_strtolower($qbUsername) : null; + } + + public function getQbPlainPassword(): ?string + { + return null; + } + + public function setQbPlainPassword(?string $qbPlainPassword): void + { + $this->setQbPassword($qbPlainPassword); + } + + /** + * @return string|null + */ + public function getQbPassword(): ?string + { + return $this->qbPassword; + } + + /** + * @param string|null $qbPassword + */ + public function setQbPassword(?string $qbPassword): void + { + $this->qbPassword = null !== $qbPassword ? $this->_hash($qbPassword) : null; + } + + private function _hash(string $password): string + { + $func = QUICKBOOKS_HASH; + return $func($password . QUICKBOOKS_SALT); + } + + /** + * @return string|null + */ + public function getQbCompanyFile(): ?string + { + return $this->qbCompanyFile; + } + + /** + * @param string|null $qbCompanyFile + */ + public function setQbCompanyFile(?string $qbCompanyFile): void + { + $this->qbCompanyFile = $qbCompanyFile; + } + + /** + * @return int|null + */ + public function getQbwcWaitBeforeNextUpdate(): ?int + { + return $this->qbwcWaitBeforeNextUpdate; + } + + /** + * @param int|null $qbwcWaitBeforeNextUpdate + */ + public function setQbwcWaitBeforeNextUpdate(?int $qbwcWaitBeforeNextUpdate): void + { + $this->qbwcWaitBeforeNextUpdate = $qbwcWaitBeforeNextUpdate; + } + + /** + * @return int|null + */ + public function getQbwcMinRunEveryNSeconds(): ?int + { + return $this->qbwcMinRunEveryNSeconds; + } + + /** + * @param int|null $qbwcMinRunEveryNSeconds + */ + public function setQbwcMinRunEveryNSeconds(?int $qbwcMinRunEveryNSeconds): void + { + $this->qbwcMinRunEveryNSeconds = $qbwcMinRunEveryNSeconds; + } + + /** + * @return string|null + */ + public function getStatus(): ?string + { + return $this->status; + } + + /** + * @param string|null $status + */ + public function setStatus(?string $status): void + { + $this->status = $status; + } + + /** + * @return \DateTime|null + */ + public function getWriteDatetime(): ?\DateTime + { + return $this->writeDatetime; + } + + /** + * @param \DateTime|null $writeDatetime + */ + public function setWriteDatetime(?\DateTime $writeDatetime): void + { + $this->writeDatetime = $writeDatetime; + } + + /** + * @return \DateTime|null + */ + public function getTouchDatetime(): ?\DateTime + { + return $this->touchDatetime; + } + + /** + * @param \DateTime|null $touchDatetime + */ + public function setTouchDatetime(?\DateTime $touchDatetime): void + { + $this->touchDatetime = $touchDatetime; + } + + /** + * @return string|null + */ + public function getBaseCurrency(): ?string + { + return $this->baseCurrency; + } + + /** + * @param string|null $baseCurrency + */ + public function setBaseCurrency(?string $baseCurrency): void + { + $this->baseCurrency = $baseCurrency; + } + + public function getXml(): ?string + { + return $this->xml; + } + + public function setXml(?string $xml): void + { + $this->xml = $xml; + } +} diff --git a/src/Entity/QuickbooksCompanyRepositoryInterface.php b/src/Entity/QuickbooksCompanyRepositoryInterface.php new file mode 100644 index 0000000..4a470c5 --- /dev/null +++ b/src/Entity/QuickbooksCompanyRepositoryInterface.php @@ -0,0 +1,13 @@ + + */ +interface QuickbooksCompanyRepositoryInterface extends ObjectRepository +{ + +} diff --git a/src/Entity/QuickbooksQueue.php b/src/Entity/QuickbooksQueue.php new file mode 100644 index 0000000..3efc5a3 --- /dev/null +++ b/src/Entity/QuickbooksQueue.php @@ -0,0 +1,322 @@ + 'Queued', + 's' => 'Success', + 'i' => 'Processing', + 'e' => 'Error', + 'h' => 'Error', + ]; + + public function getQbUsername(): ?string + { + return null !== $this->company ? $this->company->getQbUsername() : null; + } + + public function getCompanyName(): ?string + { + return null !== $this->company ? $this->company->getCompanyName() : null; + } + + public function getCompany(): ?QuickbooksCompany + { + return $this->company; + } + + public function setCompany(?QuickbooksCompany $company): void + { + $this->company = $company; + } + + public function getStatusLabel(): ?string + { + return self::STATUS_LABELS[$this->qbStatus] ?? $this->qbStatus; + } + + /** + * @return int|null + */ + public function getQuickbooksQueueId(): ?int + { + return $this->quickbooksQueueId; + } + + /** + * @param int|null $quickbooksQueueId + */ + public function setQuickbooksQueueId(?int $quickbooksQueueId): void + { + $this->quickbooksQueueId = $quickbooksQueueId; + } + + /** + * @return int|null + */ + public function getQuickbooksTicketId(): ?int + { + return $this->quickbooksTicketId; + } + + /** + * @param int|null $quickbooksTicketId + */ + public function setQuickbooksTicketId(?int $quickbooksTicketId): void + { + $this->quickbooksTicketId = $quickbooksTicketId; + } + + /** + * @return string|null + */ + public function getQbAction(): ?string + { + return $this->qbAction; + } + + /** + * @param string|null $qbAction + */ + public function setQbAction(?string $qbAction): void + { + $this->qbAction = $qbAction; + } + + /** + * @return string|null + */ + public function getIdent(): ?string + { + return $this->ident; + } + + /** + * @param string|null $ident + */ + public function setIdent(?string $ident): void + { + $this->ident = $ident; + } + + /** + * @return string|null + */ + public function getExtra(): ?string + { + return $this->extra; + } + + /** + * @return array|null + */ + public function getExtraData(): ?array + { + if ($this->extra === null) { + return null; + } + return unserialize($this->extra, ['allowed_classes' => false]); + } + + /** + * @param string|null $extra + */ + public function setExtra(?string $extra): void + { + $this->extra = $extra; + } + + /** + * @return string|null + */ + public function getQbxml(): ?string + { + return $this->qbxml; + } + + /** + * @param string|null $qbxml + */ + public function setQbxml(?string $qbxml): void + { + $this->qbxml = $qbxml; + } + + /** + * @return int|null + */ + public function getPriority(): ?int + { + return $this->priority; + } + + /** + * @param int|null $priority + */ + public function setPriority(?int $priority): void + { + $this->priority = $priority; + } + + /** + * @return string|null + */ + public function getQbStatus(): ?string + { + return $this->qbStatus; + } + + /** + * @param string|null $qbStatus + */ + public function setQbStatus(?string $qbStatus): void + { + $this->qbStatus = $qbStatus; + } + + /** + * @return string|null + */ + public function getMsg(): ?string + { + return $this->msg; + } + + /** + * @param string|null $msg + */ + public function setMsg(?string $msg): void + { + $this->msg = $msg; + } + + /** + * @return \DateTime|null + */ + public function getEnqueueDatetime(): ?\DateTime + { + return $this->enqueueDatetime; + } + + /** + * @param \DateTime|null $enqueueDatetime + */ + public function setEnqueueDatetime(?\DateTime $enqueueDatetime): void + { + $this->enqueueDatetime = $enqueueDatetime; + } + + /** + * @return \DateTime|null + */ + public function getDequeueDatetime(): ?\DateTime + { + return $this->dequeueDatetime; + } + + /** + * @param \DateTime|null $dequeueDatetime + */ + public function setDequeueDatetime(?\DateTime $dequeueDatetime): void + { + $this->dequeueDatetime = $dequeueDatetime; + } +} diff --git a/src/Entity/ResetPasswordRequest.php b/src/Entity/ResetPasswordRequest.php new file mode 100644 index 0000000..036eead --- /dev/null +++ b/src/Entity/ResetPasswordRequest.php @@ -0,0 +1,39 @@ +user = $user; + $this->initialize($expiresAt, $selector, $hashedToken); + } + + public function getUser(): object + { + return $this->user; + } +} diff --git a/src/Entity/User.php b/src/Entity/User.php new file mode 100644 index 0000000..66eb884 --- /dev/null +++ b/src/Entity/User.php @@ -0,0 +1,195 @@ + + * @ORM\OneToMany(targetEntity="App\Entity\QuickbooksCompany", mappedBy="user") + */ + private Collection $companies; + + public function __construct() + { + $this->companies = new ArrayCollection(); + $this->createdAt = new \DateTime(); + } + + public function __toString(): string + { + return $this->email ?? ''; + } + + public function getId(): ?int + { + return $this->id; + } + + public function setId(?int $id): void + { + $this->id = $id; + } + + public function getEmail(): ?string + { + return $this->email; + } + + public function setEmail(string $email): self + { + $this->email = $email; + + return $this; + } + + public function getFirstName(): ?string + { + return $this->firstName; + } + + public function setFirstName(?string $firstName): void + { + $this->firstName = $firstName; + } + + public function getLastName(): ?string + { + return $this->lastName; + } + + public function setLastName(?string $lastName): void + { + $this->lastName = $lastName; + } + + public function getCreatedAt(): \DateTimeInterface + { + return $this->createdAt ?? new \DateTime(); + } + + public function setCreatedAt(?\DateTimeInterface $createdAt): void + { + $this->createdAt = $createdAt; + } + + /** + * @return Collection + */ + public function getCompanies(): Collection + { + return $this->companies; + } + + public function addCompany(QuickbooksCompany $company): void + { + if (!$this->companies->contains($company)) { + $this->companies[] = $company; + } + } + + /** + * A visual identifier that represents this user. + * + * @see UserInterface + */ + public function getUsername(): string + { + return (string) $this->email; + } + + /** + * @return array + */ + public function getRoles(): array + { + $roles = $this->roles; + // guarantee every user at least has ROLE_USER + $roles[] = 'ROLE_USER'; + + return array_unique($roles); + } + + public function setRoles(array $roles): self + { + $this->roles = $roles; + + return $this; + } + + /** + * @see UserInterface + */ + public function getPassword(): string + { + return (string) $this->password; + } + + public function setPassword(string $password): self + { + $this->password = $password; + + return $this; + } + + public function getSalt(): ?string + { + return null; + } + + /** + * @see UserInterface + */ + public function eraseCredentials(): void + { + } +} diff --git a/src/Event/QuickbooksServerResponseEvent.php b/src/Event/QuickbooksServerResponseEvent.php new file mode 100644 index 0000000..6183a7c --- /dev/null +++ b/src/Event/QuickbooksServerResponseEvent.php @@ -0,0 +1,147 @@ +requestId = $requestId; + $this->user = $user; + $this->action = $action; + $this->ident = $ident; + $this->extra = $extra; + $this->err = $err; + $this->lastActionTime = $lastActionTime; + $this->lastActionIdentTime = $lastActionIdentTime; + $this->xml = $xml; + $this->qbIdentifier = $qbIdentifier; + $this->callbackConfig = $callbackConfig; + $this->qbXml = $qbXml; + } + + /** + * @return string + */ + public function getRequestId(): string + { + return $this->requestId; + } + + /** + * @return string + */ + public function getUser(): string + { + return $this->user; + } + + /** + * @return string + */ + public function getAction(): string + { + return $this->action; + } + + /** + * @return string + */ + public function getIdent(): string + { + return $this->ident; + } + + /** + * @return mixed + */ + public function getExtra() + { + return $this->extra; + } + + /** + * @return string|null + */ + public function getErr(): ?string + { + return $this->err; + } + + /** + * @return int|null + */ + public function getLastActionTime(): ?int + { + return $this->lastActionTime; + } + + /** + * @return int|null + */ + public function getLastActionIdentTime(): ?int + { + return $this->lastActionIdentTime; + } + + /** + * @return string + */ + public function getXml(): string + { + return $this->xml; + } + + /** + * @return array + */ + public function getQbIdentifier(): array + { + return $this->qbIdentifier; + } + + /** + * @return array + */ + public function getCallbackConfig(): array + { + return $this->callbackConfig; + } + + /** + * @return string|null + */ + public function getQbXml(): ?string + { + return $this->qbXml; + } + + /** + * @param string|null $err + */ + public function setErr(?string $err): void + { + $this->err = $err; + } +} diff --git a/src/Event/QuickbooksServerSendRequestXmlEvent.php b/src/Event/QuickbooksServerSendRequestXmlEvent.php new file mode 100644 index 0000000..4fa43df --- /dev/null +++ b/src/Event/QuickbooksServerSendRequestXmlEvent.php @@ -0,0 +1,93 @@ +requestId = $requestId; + $this->qbUsername = $qbUsername; + $this->hook = $hook; + $this->err = $err; + $this->hookData = $hookData; + $this->callbackConfig = $callbackConfig; + $this->stopPropagation = false; + } + + public function isStopPropagation(): bool + { + return $this->stopPropagation; + } + + public function setStopPropagation(bool $stopPropagation): void + { + $this->stopPropagation = $stopPropagation; + } + + /** + * @return string|null + */ + public function getRequestId(): ?string + { + return $this->requestId; + } + + /** + * @return string + */ + public function getQbUsername(): string + { + return $this->qbUsername; + } + + /** + * @return string + */ + public function getHook(): string + { + return $this->hook; + } + + /** + * @return string + */ + public function getErr(): string + { + return $this->err; + } + + /** + * @param string $err + */ + public function setErr(string $err): void + { + $this->err = $err; + } + + /** + * @return array + */ + public function getHookData(): array + { + return $this->hookData; + } + + /** + * @return array + */ + public function getCallbackConfig(): array + { + return $this->callbackConfig; + } +} diff --git a/src/EventSubscriber/UpdateCompanySubscriber.php b/src/EventSubscriber/UpdateCompanySubscriber.php new file mode 100644 index 0000000..c523441 --- /dev/null +++ b/src/EventSubscriber/UpdateCompanySubscriber.php @@ -0,0 +1,87 @@ +companyRepo = $companyRepo; + $this->em = $em; + } + + public function onSendRequestXmlEvent(QuickbooksServerSendRequestXmlEvent $event): void + { + /** @var QuickbooksCompany|null $company */ + $company = $this->companyRepo->findOneBy(['qbUsername' => $event->getQbUsername()]); + $xml = $event->getHookData()['strHCPResponse'] ?? null; + if (null === $company || $xml === null || trim($xml) === '') { + return; + } + try { + $parser = new \QuickBooks_XML_Parser($xml); + } catch (\Throwable $e) { + return; + } + /** @var \QuickBooks_XML_Document|false $doc */ + $doc = $parser->parse($errnum, $errmsg); + if ($doc === false) { + return; + } + $company->setXml($xml); + if (null !== $prefDto = $this->getPrefDto($doc->getRoot())) { + $company->setMultiCurrencyEnabled($prefDto->getMultiCurrencyPreferencesIsMultiCurrencyOn()); + $symbol = $this->getDecimalSymbol($prefDto->getFinanceChargePreferencesAnnualInterestRate()) ?? $this->getDecimalSymbol($prefDto->getFinanceChargePreferencesMinFinanceCharge()); + $company->setDecimalSymbol($symbol); + $company->setDigitGroupingSymbol($this->getDigitGroupingSymbol($symbol)); + } + + $companyFilename = $event->getHookData()['strCompanyFileName'] ?? null; + if (null !== $companyFilename) { + $company->setQbCompanyFile($companyFilename); + } + + $this->em->flush(); + } + + public function getDecimalSymbol(?string $value): string + { + if (null !== $value) { + $value = preg_replace('/[^,.]+/', '', $value); + if (is_string($value) && $value !== '') { + return $value[-1]; + } + } + return QuickbooksCompany::DEFAULT_DECIMAL_SYMBOL; + } + + public static function getSubscribedEvents(): array + { + return [ + QuickbooksServerSendRequestXmlEvent::class => 'onSendRequestXmlEvent', + ]; + } + + private function getDigitGroupingSymbol(string $decimalSymbol): string + { + return $decimalSymbol === ',' ? '.' :QuickbooksCompany::DEFAULT_DIGIT_GROUPING_SYMBOL; + } + + private function getPrefDto(\QuickBooks_XML_Node $root): ?\QuickBooks_QBXML_Object_Preferences + { + $prefXml = $root->getChildAt('QBXML/QBXMLMsgsRs/PreferencesQueryRs/PreferencesRet'); + /** @var \QuickBooks_QBXML_Object_Preferences|false $prefDto */ + $prefDto = \QuickBooks_QBXML_Object::fromXML($prefXml, QUICKBOOKS_QUERY_PREFERENCES); + return $prefDto instanceof \QuickBooks_QBXML_Object_Preferences ? $prefDto : null; + } +} diff --git a/src/Exception/AppException.php b/src/Exception/AppException.php new file mode 100644 index 0000000..902d8b6 --- /dev/null +++ b/src/Exception/AppException.php @@ -0,0 +1,8 @@ +constraintViolationList = $constraintViolationList; + + if ('' !== $message) { + $message .= "\n"; + } + parent::__construct($message . $this->__toString(), $code, $previous); + } + + /** + * Gets constraint violations related to this exception. + */ + public function getConstraintViolationList(): ConstraintViolationListInterface + { + return $this->constraintViolationList; + } + + public function __toString(): string + { + $message = ''; + foreach ($this->constraintViolationList as $violation) { + if ('' !== $message) { + $message .= "\n"; + } + if ($propertyPath = $violation->getPropertyPath()) { + $message .= "$propertyPath: "; + } + + $message .= $violation->getMessage(); + } + + return $message; + } +} diff --git a/src/Exception/ValidationsException.php b/src/Exception/ValidationsException.php new file mode 100644 index 0000000..54da7db --- /dev/null +++ b/src/Exception/ValidationsException.php @@ -0,0 +1,25 @@ + $exceptions + */ + public function __construct(string $message, array $exceptions) + { + parent::__construct($message); + $this->exceptions = $exceptions; + } + + /** + * @return array + */ + public function getExceptions(): array + { + return $this->exceptions; + } +} diff --git a/src/Form/ChangePasswordFormType.php b/src/Form/ChangePasswordFormType.php new file mode 100644 index 0000000..9603af3 --- /dev/null +++ b/src/Form/ChangePasswordFormType.php @@ -0,0 +1,49 @@ +add('plainPassword', RepeatedType::class, [ + 'type' => PasswordType::class, + 'first_options' => [ + 'constraints' => [ + new NotBlank([ + 'message' => 'Please enter a password', + ]), + new Length([ + 'min' => 6, + 'minMessage' => 'Your password should be at least {{ limit }} characters', + // max length allowed by Symfony for security reasons + 'max' => 4096, + ]), + ], + 'label' => 'New password', + ], + 'second_options' => [ + 'label' => 'Repeat Password', + ], + 'invalid_message' => 'The password fields must match.', + // Instead of being set onto the object directly, + // this is read and encoded in the controller + 'mapped' => false, + ]) + ; + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([]); + } +} diff --git a/src/Form/ConvertTransactionsType.php b/src/Form/ConvertTransactionsType.php new file mode 100644 index 0000000..a67ffdd --- /dev/null +++ b/src/Form/ConvertTransactionsType.php @@ -0,0 +1,44 @@ +add('username', EntityType::class, [ + 'class' => QuickbooksCompany::class, + 'choice_label' => 'companyName', + 'choice_value' => 'qbUsername', + 'placeholder' => 'select option', + 'constraints' => [ + new NotBlank(), + ], + ]) + ->add('local_file', FileType::class, [ + 'label' => 'Upload file', + 'required' => false, + 'constraints' => [ + new File([ + 'maxSize' => '10m', + 'mimeTypes' => [ + 'text/plain', + ], + 'mimeTypesMessage' => 'Please upload a valid CSV document', + ]) + ], + ]) + ->add('submit', SubmitType::class, ['label' => 'Convert']) + ; + } +} diff --git a/src/Form/DownloadSampleSheetType.php b/src/Form/DownloadSampleSheetType.php new file mode 100644 index 0000000..0702172 --- /dev/null +++ b/src/Form/DownloadSampleSheetType.php @@ -0,0 +1,33 @@ +add('type', ChoiceType::class, [ + 'placeholder' => 'select option', + 'choices' => [ + SheetScheduler::TYPE_CUSTOMER_INVOICE => SheetScheduler::TYPE_CUSTOMER_INVOICE, + SheetScheduler::TYPE_CUSTOMER => SheetScheduler::TYPE_CUSTOMER, + SheetScheduler::TYPE_VENDOR_BILL => SheetScheduler::TYPE_VENDOR_BILL, + SheetScheduler::TYPE_VENDOR => SheetScheduler::TYPE_VENDOR, + SheetScheduler::TYPE_TRANSACTION => SheetScheduler::TYPE_TRANSACTION, + ], + 'constraints' => [ + new NotBlank(), + ], + ]) + ->add('submit', SubmitType::class, ['label' => 'Download']) + ; + } +} diff --git a/src/Form/Import/CreateImportFlow.php b/src/Form/Import/CreateImportFlow.php new file mode 100644 index 0000000..64d6cb9 --- /dev/null +++ b/src/Form/Import/CreateImportFlow.php @@ -0,0 +1,25 @@ + 'import_wizard_step.upload', + 'form_type' => ImportUploadFormType::class, + ], + [ + 'label' => 'import_wizard_step.mapping', + 'form_type' => MapFieldsForm::class, + ], + [ + 'label' => 'import_wizard_step.confirmation', + ], + ]; + } +} diff --git a/src/Form/Import/ImportUploadFormType.php b/src/Form/Import/ImportUploadFormType.php new file mode 100644 index 0000000..b0e47a0 --- /dev/null +++ b/src/Form/Import/ImportUploadFormType.php @@ -0,0 +1,107 @@ +add('company', EntityType::class, [ + 'class' => QuickbooksCompany::class, + 'choice_label' => 'companyName', + 'choice_value' => 'qbUsername', + 'placeholder' => 'select option', + 'constraints' => [ + new NotBlank(), + ], + ]) + ->add('importType', ChoiceType::class, [ + 'placeholder' => 'select option', + 'choices' => [ + SheetScheduler::TYPE_CUSTOMER_INVOICE => SheetScheduler::TYPE_CUSTOMER_INVOICE, + SheetScheduler::TYPE_CUSTOMER => SheetScheduler::TYPE_CUSTOMER, + SheetScheduler::TYPE_VENDOR_BILL => SheetScheduler::TYPE_VENDOR_BILL, + SheetScheduler::TYPE_VENDOR => SheetScheduler::TYPE_VENDOR, + SheetScheduler::TYPE_TRANSACTION => SheetScheduler::TYPE_TRANSACTION, + ], + 'constraints' => [ + new NotBlank(), + ], + ]) + ->add('file', FileType::class, [ + 'label' => 'Upload file', + 'constraints' => [ + new File([ + 'maxSize' => '100m', + 'mimeTypes' => [ + 'text/plain', + 'application/vnd.ms-excel', + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + 'application/vnd.openxmlformatsofficedocument.spreadsheetml.sheet', + 'vnd.openxmlformats-officedocument.spreadsheetml.sheet', + ], + 'mimeTypesMessage' => 'Please upload a valid CSV/XLS/XLSX document', + ]), + new NotBlank(), + ], + ]) + ->add('dateFormat', ChoiceType::class, [ + 'placeholder' => 'select option', + 'choices' => $this->getDateFormatChoices(), + 'constraints' => [ + new NotBlank(), + ], + 'data' => 'Y-m-d', + ]) + ; + } + + private function getDateFormatChoices(): array + { + $formats = [ + 'Y-m-d', // yyyy-MM-dd + 'Y.m.d', + 'M j, Y', + 'n/j/Y', // M/d/yyyy + 'n/j/y', // M/d/yy + 'm/d/y', // MM/dd/yy + 'm/d/Y', // MM/dd/yyyy + 'y/m/d', // yy/MM/dd + 'd-M-y', // dd-MMM-yy + 'Y/m/d', + 'd/m/y', + ]; + $date = new \DateTime('1999-03-31'); + $choices = array_combine(array_map( + fn(string $format) => $date->format($format).' ('.$format.')', + $formats), $formats); + Assert::isArray($choices); + return $choices; + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'data_class' => Import::class, + ]); + } + + public function getBlockPrefix(): string + { + return 'importUpload'; + } +} diff --git a/src/Form/Import/MapFieldsForm.php b/src/Form/Import/MapFieldsForm.php new file mode 100644 index 0000000..0aac4aa --- /dev/null +++ b/src/Form/Import/MapFieldsForm.php @@ -0,0 +1,31 @@ +add('fieldsMapping', SimpleMappingFormType::class, [ + 'import' => $import, + 'label' => false, + ]); + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'data_class' => Import::class, + ]); + } +} diff --git a/src/Form/Import/SimpleMappingFormType.php b/src/Form/Import/SimpleMappingFormType.php new file mode 100644 index 0000000..7c35c7f --- /dev/null +++ b/src/Form/Import/SimpleMappingFormType.php @@ -0,0 +1,110 @@ +fileDecoder = $fileDecoder; + $this->propertyInfoExtractor = $propertyInfoExtractor; + } + + public function buildForm(FormBuilderInterface $builder, array $options): void + { + /** @var Import|null $import */ + $import = $options['import'] ?? null; + Assert::isInstanceOf($import, Import::class); + + $this->buildMapping($builder, $import); + } + + private const REQUIRED_FIELDS = [ + CustomerInvoice::class => ['line1ItemName'], + Customer::class => [], + VendorBill::class => [], + Vendor::class => [], + Transaction::class => [], + ]; + + private function buildMapping(FormBuilderInterface $builder, Import $import): void + { + $type = $import->getImportType(); + Assert::string($type); + $class = SheetScheduler::CLASS_MAP[$type]; + $fields = $this->propertyInfoExtractor->getProperties($class) ?? []; + $choices = $this->getChoices($import); + foreach ($fields as $field) { + $required = isset(self::REQUIRED_FIELDS[$class]) && in_array($field, self::REQUIRED_FIELDS[$class], true); + $builder + ->add($field, ChoiceType::class, [ + 'placeholder' => 'Select field from your file', + 'choices' => $choices, + 'required' => $required, + 'constraints' => $required ? [new NotBlank()] : [], + 'data' => in_array($field, $choices, true) ? $field : null, + ]) + ; + } + } + + private function getChoices(Import $import): array + { + $uploadedFile = $import->getFile(); + Assert::notNull($uploadedFile); + $realPath = $uploadedFile->getRealPath(); + Assert::string($realPath); + $data = $this->fileDecoder->decodeFile($realPath, $uploadedFile->getMimeType()); + Assert::notEmpty($data); + $keys = array_keys($data[0]); + $labels = array_map([$this, 'concatLabel'], $keys, $data[0]); + $choices = array_combine($labels, $keys); + Assert::isArray($choices); + + return $choices; + } + + /** + * @param mixed $value + */ + public function concatLabel(string $key, $value): string + { + if ($value === null || $value === '') { + return $key; + } + if (is_array($value)) { + $value = implode(', ', array_filter($value)); + } + $maxValueLen = 30 - mb_strlen($key); + if (mb_strlen($value) > $maxValueLen) { + $value = mb_substr($value, 0, $maxValueLen - 3) . '...'; + } + return "{$key} ({$value})"; + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'import' => null, + ]); + } +} diff --git a/src/Form/RegistrationFormType.php b/src/Form/RegistrationFormType.php new file mode 100644 index 0000000..a31b4d7 --- /dev/null +++ b/src/Form/RegistrationFormType.php @@ -0,0 +1,44 @@ +add('email') + ->add('plainPassword', PasswordType::class, [ + // instead of being set onto the object directly, + // this is read and encoded in the controller + 'mapped' => false, + 'constraints' => [ + new NotBlank([ + 'message' => 'Please enter a password', + ]), + new Length([ + 'min' => 6, + 'minMessage' => 'Your password should be at least {{ limit }} characters', + // max length allowed by Symfony for security reasons + 'max' => 4096, + ]), + ], + ]) + ; + } + + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults([ + 'data_class' => User::class, + ]); + } +} diff --git a/src/Form/ResetPasswordRequestFormType.php b/src/Form/ResetPasswordRequestFormType.php new file mode 100644 index 0000000..15eea22 --- /dev/null +++ b/src/Form/ResetPasswordRequestFormType.php @@ -0,0 +1,30 @@ +add('email', EmailType::class, [ + 'constraints' => [ + new NotBlank([ + 'message' => 'Please enter your email', + ]), + ], + ]) + ; + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([]); + } +} diff --git a/src/Form/ScheduleAccountsUpdateType.php b/src/Form/ScheduleAccountsUpdateType.php new file mode 100644 index 0000000..8a6040d --- /dev/null +++ b/src/Form/ScheduleAccountsUpdateType.php @@ -0,0 +1,29 @@ +add('company', EntityType::class, [ + 'class' => QuickbooksCompany::class, + 'choice_label' => 'companyName', + 'choice_value' => 'qbUsername', + 'placeholder' => 'select option', + 'constraints' => [ + new NotBlank(), + ], + ]) + ->add('submit', SubmitType::class, ['label' => 'Schedule']) + ; + } +} diff --git a/src/Form/ScheduleType.php b/src/Form/ScheduleType.php new file mode 100644 index 0000000..c5792dc --- /dev/null +++ b/src/Form/ScheduleType.php @@ -0,0 +1,98 @@ +sheetFilesystem = $sheetFilesystem; + } + + public function buildForm(FormBuilderInterface $builder, array $options): void + { + $files = array_filter($this->sheetFilesystem->listFiles(), function($file): bool { + return preg_match('/\.(xls|csv)$/', $file['path']) === 1; + }); + + $builder + ->add('username', EntityType::class, [ + 'class' => QuickbooksCompany::class, + 'choice_label' => 'companyName', + 'choice_value' => 'qbUsername', + 'placeholder' => 'select option', + 'constraints' => [ + new NotBlank(), + ], + ]) + ->add('type', ChoiceType::class, [ + 'placeholder' => 'select option', + 'choices' => [ + SheetScheduler::TYPE_CUSTOMER_INVOICE => SheetScheduler::TYPE_CUSTOMER_INVOICE, + SheetScheduler::TYPE_CUSTOMER => SheetScheduler::TYPE_CUSTOMER, + SheetScheduler::TYPE_VENDOR_BILL => SheetScheduler::TYPE_VENDOR_BILL, + SheetScheduler::TYPE_VENDOR => SheetScheduler::TYPE_VENDOR, + SheetScheduler::TYPE_TRANSACTION => SheetScheduler::TYPE_TRANSACTION, + ], + 'constraints' => [ + new NotBlank(), + ], + ]) + ->add('remote_file', ChoiceType::class, [ + 'placeholder' => 'select option', + 'required' => false, + 'choices' => array_column($files, 'path', 'path'), + 'constraints' => [ +// new NotBlank(), + ], + ]) + ->add('local_file', FileType::class, [ + 'label' => 'Upload file', + 'required' => false, + 'constraints' => [ + new File([ + 'maxSize' => '100m', + 'mimeTypes' => [ + 'text/plain', + 'application/vnd.ms-excel', + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + 'application/vnd.openxmlformatsofficedocument.spreadsheetml.sheet', + 'vnd.openxmlformats-officedocument.spreadsheetml.sheet', + ], + 'mimeTypesMessage' => 'Please upload a valid CSV/XLS/XLSX document', + ]) + ], + ]) + ->add('dry_run', ChoiceType::class, [ + 'choices' => [ + 'Yes' => true, + 'No' => false, + ], + 'constraints' => [ + new NotNull(), + ], + ]) + ->add('submit', SubmitType::class, ['label' => 'Schedule']) + ; + } +} diff --git a/src/Form/TruncateQueueType.php b/src/Form/TruncateQueueType.php new file mode 100644 index 0000000..1739a8d --- /dev/null +++ b/src/Form/TruncateQueueType.php @@ -0,0 +1,28 @@ +add('confirm', ChoiceType::class, [ + 'choices' => [ + 'No' => false, + 'Yes' => true, + ], + 'constraints' => [ + new NotBlank(), + ], + ]) + ->add('submit', SubmitType::class, ['label' => 'Truncate queue']) + ; + } +} diff --git a/src/Kernel.php b/src/Kernel.php new file mode 100644 index 0000000..88be2d0 --- /dev/null +++ b/src/Kernel.php @@ -0,0 +1,60 @@ +getProjectDir().'/config/bundles.php'; + foreach ($contents as $class => $envs) { + if ($envs[$this->environment] ?? $envs['all'] ?? false) { + yield new $class(); + } + } + } + + public function getProjectDir(): string + { + return \dirname(__DIR__); + } + + protected function configureContainer(ContainerBuilder $container, LoaderInterface $loader): void + { + $container->addResource(new FileResource($this->getProjectDir().'/config/bundles.php')); + $container->setParameter('container.dumper.inline_class_loader', \PHP_VERSION_ID < 70400 || $this->debug); + $container->setParameter('container.dumper.inline_factories', true); + $confDir = $this->getProjectDir().'/config'; + + $loader->load($confDir.'/{packages}/*'.self::CONFIG_EXTS, 'glob'); + $loader->load($confDir.'/{packages}/'.$this->environment.'/*'.self::CONFIG_EXTS, 'glob'); + $loader->load($confDir.'/{services}'.self::CONFIG_EXTS, 'glob'); + $loader->load($confDir.'/{services}_'.$this->environment.self::CONFIG_EXTS, 'glob'); + } + + protected function configureRoutes(RouteCollectionBuilder $routes): void + { + $confDir = $this->getProjectDir().'/config'; + + $routes->import($confDir.'/{routes}/'.$this->environment.'/*'.self::CONFIG_EXTS, '/', 'glob'); + $routes->import($confDir.'/{routes}/*'.self::CONFIG_EXTS, '/', 'glob'); + $routes->import($confDir.'/{routes}'.self::CONFIG_EXTS, '/', 'glob'); + } + + protected function build(ContainerBuilder $container): void + { + $container->addCompilerPass(new AddEntityTransformers()); + } +} diff --git a/src/Migrations/.gitignore b/src/Migrations/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/src/Migrations/Version20190722114323.php b/src/Migrations/Version20190722114323.php new file mode 100644 index 0000000..a6d71c6 --- /dev/null +++ b/src/Migrations/Version20190722114323.php @@ -0,0 +1,35 @@ +abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.'); + + $this->addSql('CREATE TABLE quickbooks_config (quickbooks_config_id INT UNSIGNED AUTO_INCREMENT NOT NULL, qb_username VARCHAR(40) NOT NULL COLLATE utf8_general_ci, module VARCHAR(40) NOT NULL COLLATE utf8_general_ci, cfgkey VARCHAR(40) NOT NULL COLLATE utf8_general_ci, cfgval VARCHAR(40) NOT NULL COLLATE utf8_general_ci, cfgtype VARCHAR(40) NOT NULL COLLATE utf8_general_ci, cfgopts TEXT NOT NULL COLLATE utf8_general_ci, write_datetime DATETIME NOT NULL, mod_datetime DATETIME NOT NULL, PRIMARY KEY(quickbooks_config_id)) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = InnoDB COMMENT = \'\' '); + $this->addSql('CREATE TABLE quickbooks_log (quickbooks_log_id INT UNSIGNED AUTO_INCREMENT NOT NULL, quickbooks_ticket_id INT UNSIGNED DEFAULT NULL, batch INT UNSIGNED NOT NULL, msg TEXT NOT NULL COLLATE utf8_general_ci, log_datetime DATETIME NOT NULL, INDEX batch (batch), INDEX quickbooks_ticket_id (quickbooks_ticket_id), PRIMARY KEY(quickbooks_log_id)) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = InnoDB COMMENT = \'\' '); + $this->addSql('CREATE TABLE quickbooks_oauthv1 (quickbooks_oauthv1_id INT UNSIGNED AUTO_INCREMENT NOT NULL, app_username VARCHAR(255) NOT NULL COLLATE utf8_general_ci, app_tenant VARCHAR(255) NOT NULL COLLATE utf8_general_ci, oauth_request_token VARCHAR(255) DEFAULT NULL COLLATE utf8_general_ci, oauth_request_token_secret VARCHAR(255) DEFAULT NULL COLLATE utf8_general_ci, oauth_access_token VARCHAR(255) DEFAULT NULL COLLATE utf8_general_ci, oauth_access_token_secret VARCHAR(255) DEFAULT NULL COLLATE utf8_general_ci, qb_realm VARCHAR(32) DEFAULT NULL COLLATE utf8_general_ci, qb_flavor VARCHAR(12) DEFAULT NULL COLLATE utf8_general_ci, qb_user VARCHAR(64) DEFAULT NULL COLLATE utf8_general_ci, request_datetime DATETIME NOT NULL, access_datetime DATETIME DEFAULT NULL, touch_datetime DATETIME DEFAULT NULL, PRIMARY KEY(quickbooks_oauthv1_id)) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = InnoDB COMMENT = \'\' '); + $this->addSql('CREATE TABLE quickbooks_oauthv2 (quickbooks_oauthv2_id INT UNSIGNED AUTO_INCREMENT NOT NULL, app_tenant VARCHAR(255) NOT NULL COLLATE utf8_general_ci, oauth_state VARCHAR(255) NOT NULL COLLATE utf8_general_ci, oauth_access_token TEXT NOT NULL COLLATE utf8_general_ci, oauth_refresh_token TEXT NOT NULL COLLATE utf8_general_ci, oauth_access_expiry DATETIME DEFAULT NULL, oauth_refresh_expiry DATETIME DEFAULT NULL, qb_realm VARCHAR(32) DEFAULT NULL COLLATE utf8_general_ci, request_datetime DATETIME NOT NULL, access_datetime DATETIME DEFAULT NULL, last_access_datetime DATETIME DEFAULT NULL, last_refresh_datetime DATETIME DEFAULT NULL, touch_datetime DATETIME DEFAULT NULL, PRIMARY KEY(quickbooks_oauthv2_id)) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = InnoDB COMMENT = \'\' '); + $this->addSql('CREATE TABLE quickbooks_recur (quickbooks_recur_id INT UNSIGNED AUTO_INCREMENT NOT NULL, qb_username VARCHAR(40) NOT NULL COLLATE utf8_general_ci, qb_action VARCHAR(32) NOT NULL COLLATE utf8_general_ci, ident VARCHAR(40) NOT NULL COLLATE utf8_general_ci, extra TEXT DEFAULT NULL COLLATE utf8_general_ci, qbxml TEXT DEFAULT NULL COLLATE utf8_general_ci, priority INT UNSIGNED DEFAULT 0, run_every INT UNSIGNED NOT NULL, recur_lasttime INT UNSIGNED NOT NULL, enqueue_datetime DATETIME NOT NULL, INDEX priority (priority), INDEX qb_username (qb_username, qb_action, ident), PRIMARY KEY(quickbooks_recur_id)) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = InnoDB COMMENT = \'\' '); + $this->addSql('CREATE TABLE quickbooks_ticket (quickbooks_ticket_id INT UNSIGNED AUTO_INCREMENT NOT NULL, qb_username VARCHAR(40) NOT NULL COLLATE utf8_general_ci, ticket CHAR(36) NOT NULL COLLATE utf8_general_ci, processed INT UNSIGNED DEFAULT 0, lasterror_num VARCHAR(32) DEFAULT NULL COLLATE utf8_general_ci, lasterror_msg VARCHAR(255) DEFAULT NULL COLLATE utf8_general_ci, ipaddr CHAR(15) NOT NULL COLLATE utf8_general_ci, write_datetime DATETIME NOT NULL, touch_datetime DATETIME NOT NULL, INDEX ticket (ticket), PRIMARY KEY(quickbooks_ticket_id)) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = InnoDB COMMENT = \'\' '); + } + + public function down(Schema $schema) : void + { + $this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.'); + + $this->addSql('DROP TABLE quickbooks_config'); + $this->addSql('DROP TABLE quickbooks_log'); + $this->addSql('DROP TABLE quickbooks_oauthv1'); + $this->addSql('DROP TABLE quickbooks_oauthv2'); + $this->addSql('DROP TABLE quickbooks_recur'); + $this->addSql('DROP TABLE quickbooks_ticket'); + } +} diff --git a/src/Migrations/Version20200515144435.php b/src/Migrations/Version20200515144435.php new file mode 100644 index 0000000..5a22cf2 --- /dev/null +++ b/src/Migrations/Version20200515144435.php @@ -0,0 +1,43 @@ +abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.'); + + $this->addSql('CREATE TABLE quickbooks_user (qb_username VARCHAR(40) NOT NULL, user_id INT NOT NULL, qb_password VARCHAR(255) NOT NULL, company_name VARCHAR(255) DEFAULT NULL, qb_company_file VARCHAR(255) DEFAULT NULL, base_currency VARCHAR(3) DEFAULT NULL, multi_currency_enabled TINYINT(1) NOT NULL, qbwc_wait_before_next_update INT DEFAULT NULL, qbwc_min_run_every_n_seconds INT DEFAULT NULL, status VARCHAR(1) NOT NULL, write_datetime DATETIME NOT NULL, touch_datetime DATETIME NOT NULL, decimal_symbol VARCHAR(1) NOT NULL, digit_grouping_symbol VARCHAR(1) NOT NULL, xml LONGTEXT DEFAULT NULL, INDEX IDX_EE77BEA9A76ED395 (user_id), PRIMARY KEY(qb_username)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'); + $this->addSql('CREATE TABLE quickbooks_queue (quickbooks_queue_id INT AUTO_INCREMENT NOT NULL, qb_username VARCHAR(40) NOT NULL, quickbooks_ticket_id INT DEFAULT NULL, qb_action VARCHAR(32) NOT NULL, ident VARCHAR(40) NOT NULL, extra LONGTEXT DEFAULT NULL, qbxml LONGTEXT DEFAULT NULL, priority INT DEFAULT NULL, qb_status VARCHAR(1) NOT NULL, msg LONGTEXT DEFAULT NULL, enqueue_datetime DATETIME NOT NULL, dequeue_datetime DATETIME DEFAULT NULL, INDEX IDX_DF947973B1AEBF2B (qb_username), INDEX quickbooks_ticket_id (quickbooks_ticket_id), INDEX qb_status (qb_status), INDEX qb_username (qb_username, qb_action, ident, qb_status), INDEX priority (priority), PRIMARY KEY(quickbooks_queue_id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'); + $this->addSql('CREATE TABLE user (id INT AUTO_INCREMENT NOT NULL, email VARCHAR(180) NOT NULL, first_name VARCHAR(255) DEFAULT NULL, last_name VARCHAR(255) DEFAULT NULL, created_at DATETIME NOT NULL, roles LONGTEXT NOT NULL COMMENT \'(DC2Type:json)\', password VARCHAR(255) NOT NULL, UNIQUE INDEX UNIQ_8D93D649E7927C74 (email), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'); + $this->addSql('CREATE TABLE reset_password_request (id INT AUTO_INCREMENT NOT NULL, user_id INT NOT NULL, selector VARCHAR(20) NOT NULL, hashed_token VARCHAR(100) NOT NULL, requested_at DATETIME NOT NULL COMMENT \'(DC2Type:datetime_immutable)\', expires_at DATETIME NOT NULL COMMENT \'(DC2Type:datetime_immutable)\', INDEX IDX_7CE748AA76ED395 (user_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'); + $this->addSql('CREATE TABLE quickbooks_account (id INT AUTO_INCREMENT NOT NULL, user_id INT NOT NULL, qb_username VARCHAR(40) NOT NULL, full_name VARCHAR(159) NOT NULL, currency VARCHAR(64) DEFAULT NULL, account_type VARCHAR(64) NOT NULL, special_account_type VARCHAR(64) DEFAULT NULL, account_number VARCHAR(7) DEFAULT NULL, INDEX IDX_DE9741A9A76ED395 (user_id), INDEX IDX_DE9741A9B1AEBF2B (qb_username), INDEX search_idx (full_name, account_type), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'); + $this->addSql('ALTER TABLE quickbooks_user ADD CONSTRAINT FK_EE77BEA9A76ED395 FOREIGN KEY (user_id) REFERENCES user (id)'); + $this->addSql('ALTER TABLE quickbooks_queue ADD CONSTRAINT FK_DF947973B1AEBF2B FOREIGN KEY (qb_username) REFERENCES quickbooks_user (qb_username)'); + $this->addSql('ALTER TABLE reset_password_request ADD CONSTRAINT FK_7CE748AA76ED395 FOREIGN KEY (user_id) REFERENCES user (id)'); + $this->addSql('ALTER TABLE quickbooks_account ADD CONSTRAINT FK_DE9741A9A76ED395 FOREIGN KEY (user_id) REFERENCES user (id)'); + $this->addSql('ALTER TABLE quickbooks_account ADD CONSTRAINT FK_DE9741A9B1AEBF2B FOREIGN KEY (qb_username) REFERENCES quickbooks_user (qb_username)'); + } + + public function down(Schema $schema) : void + { + $this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.'); + + $this->addSql('ALTER TABLE quickbooks_queue DROP FOREIGN KEY FK_DF947973B1AEBF2B'); + $this->addSql('ALTER TABLE quickbooks_account DROP FOREIGN KEY FK_DE9741A9B1AEBF2B'); + $this->addSql('ALTER TABLE quickbooks_user DROP FOREIGN KEY FK_EE77BEA9A76ED395'); + $this->addSql('ALTER TABLE reset_password_request DROP FOREIGN KEY FK_7CE748AA76ED395'); + $this->addSql('ALTER TABLE quickbooks_account DROP FOREIGN KEY FK_DE9741A9A76ED395'); + $this->addSql('DROP TABLE quickbooks_user'); + $this->addSql('DROP TABLE quickbooks_queue'); + $this->addSql('DROP TABLE user'); + $this->addSql('DROP TABLE reset_password_request'); + $this->addSql('DROP TABLE quickbooks_account'); + } +} diff --git a/src/QuickbooksFormatter.php b/src/QuickbooksFormatter.php new file mode 100644 index 0000000..2c9a32d --- /dev/null +++ b/src/QuickbooksFormatter.php @@ -0,0 +1,171 @@ + + + + + ' . $string . ' + + '; + + return $return; + } + + public function getTestPaymentReceiveAdd(): string + { + $payment = new \QuickBooks_QBXML_Object_ReceivePayment(); + $payment->setCustomerFullName('Acme Inc.'); + $payment->setARAccountFullName('Accounts Receivable - USD'); + + $payment->setTxnDate('2015-10-10'); + $payment->setRefNumber('1000'); + $payment->setTotalAmount('100.00'); + $payment->set('ExchangeRate', '7'); + $payment->setPaymentMethodFullName('Cash'); + $payment->setMemo('Inv. #9460'); + $payment->setDepositToAccountFullName('Citibank USD'); + + $payment->setIsAutoApply(true); + + return $this->formatForOutput($payment->asQBXML(QUICKBOOKS_ADD_RECEIVE_PAYMENT)); + } + + public function getTestBillPaymentCheckAdd(): string + { + $payment = new \QuickBooks_QBXML_Object_BillPaymentCheck(); + $payment->setPayeeEntityFullName('Brilliant Business Center'); + $payment->setAPAccountFullName('Accounts Payable'); + $payment->setTxnDate('2015-10-10'); + $payment->setBankAccountFullName('Citibank USD'); + $payment->setRefNumber('1000'); + $payment->setMemo('Inv. #9460'); + $payment->set('ExchangeRate', '1'); +// $payment->set('AppliedToTxnAdd TxnID', '100.00'); + $payment->set('AppliedToTxnAdd PaymentAmount', '100.00'); + + return $this->formatForOutput($payment->asQBXML(QUICKBOOKS_ADD_BILLPAYMENTCHECK)); + } + + public function getTestJournalEntryAdd(): string + { + $entry = new \QuickBooks_QBXML_Object_JournalEntry(); + $entry->setTxnDate('2018-10-10'); + $entry->setRefNumber(''); + + $entry->set('ExchangeRate', '8'); + $entry->set('CurrencyRef FullName', 'US Dollar'); + + $creditLine = new \QuickBooks_QBXML_Object_JournalEntry_JournalCreditLine(); + $creditLine->setAccountName('Uncategorized Income'); + $creditLine->setAmount('503.00'); + $creditLine->setMemo(''); + $entry->addCreditLine($creditLine); + $debitLine = new \QuickBooks_QBXML_Object_JournalEntry_JournalDebitLine(); + $debitLine->setAccountName('Citibank USD'); + $debitLine->setAmount('503.00'); + $debitLine->setMemo(''); + $entry->addDebitLine($debitLine); + + return $this->formatForOutput($entry->asQBXML(QUICKBOOKS_ADD_JOURNALENTRY)); + } + + public function getTestCustomerAdd(): string + { + //Generate a QBXML object + $Customer = new QuickBooks_QBXML_Object_Customer(); + + $Customer->setFullName('Acme Inc.'); + $Customer->setFirstName('John'); + $Customer->setLastName('Tulchin'); + $Customer->setCompanyName('Acme Inc.'); + + $Customer->setBillAddress('56 Cowles Road', '', '', '', '', + 'Willington', 'CT', '', '06279', 'United States'); + + $Customer->set('CurrencyRef FullName', 'US Dollar'); + + return $this->formatForOutput($Customer->asQBXML(QUICKBOOKS_ADD_CUSTOMER)); + } + + public function getTestInvoiceAdd(): string + { +// new QuickBooks_QBXML_ObjectLine + $Invoice = new QuickBooks_QBXML_Object_Invoice(); + $Invoice->setCustomerFullName('Acme Inc.'); + $Invoice->setRefNumber('A-123'); + $Invoice->setMemo('This invoice was created using the QuickBooks PHP API!'); + $Invoice->setARAccountName('Accounts Receivable - USD'); + + $Invoice->setTxnDate('2018-05-03'); + + $InvoiceLine1 = new QuickBooks_QBXML_Object_Invoice_InvoiceLine(); + $InvoiceLine1->setItemName('Consulting'); + $InvoiceLine1->setDesc('Item desc rate'); + $InvoiceLine1->setRate(60.00); + $InvoiceLine1->setQuantity(3); + +// 5 items of type "Item Type 2", for a total amount of $225.00 ($45.00 each) + $InvoiceLine2 = new QuickBooks_QBXML_Object_Invoice_InvoiceLine(); + $InvoiceLine2->setItemName('Consulting'); + $InvoiceLine2->setDesc('Item desc amount'); + $InvoiceLine2->setAmount(225.00); + $InvoiceLine2->setQuantity(5); + +// Make sure you add those invoice lines on to the invoice + $Invoice->addInvoiceLine($InvoiceLine1); + $Invoice->addInvoiceLine($InvoiceLine2); + + return $this->formatForOutput($Invoice->asQBXML(QUICKBOOKS_ADD_INVOICE)); + } + + public function getTestVendorAdd(): string + { + $vendor = new QuickBooks_QBXML_Object_Vendor(); + + $vendor->setName('Brilliant Business Center'); + $vendor->setCompanyName('Brilliant Business Center'); + $vendor->setVendorAddress('Unit 1104A, 11/F, Kai Tak Commercial Building', '317-319 Des Voeux Rd. Central, H.K'); + $vendor->setVendorTypeRef('Service Providers'); + $vendor->set('TermsRef FullName', 'Due on receipt'); + $vendor->set('CurrencyRef FullName', 'Hong Kong Dollar'); + + return $this->formatForOutput($vendor->asQBXML(QUICKBOOKS_ADD_VENDOR)); + } + + public function getTestBillAdd(): string + { + $bill = new QuickBooks_QBXML_Object_Bill(); + $bill->setVendorFullname('Brilliant Business Center'); + $bill->setMemo('Bill Memo'); + $bill->set('TermsRef FullName', 'Due on receipt'); + $bill->set('APAccountRef FullName', 'Accounts Payable'); + $bill->setRefNumber('vendor-invoice-id'); + $bill->setTxnDate('2018-05-10'); + + $line = new QuickBooks_QBXML_Object_Bill_ExpenseLine(); + $line->setAccountFullName('Rent Expense'); + $line->setAmount(10.00); + $line->setMemo('Item Memo'); + $bill->addExpenseLine($line); + + return $this->formatForOutput($bill->asQBXML(QUICKBOOKS_ADD_BILL)); + } +} diff --git a/src/QuickbooksServer.php b/src/QuickbooksServer.php new file mode 100644 index 0000000..92056a8 --- /dev/null +++ b/src/QuickbooksServer.php @@ -0,0 +1,216 @@ +dsn = str_replace('mysql://', 'mysqli://', $dsn); + $this->appName = $appName; + $this->projectUrl = $projectUrl; + $this->logger = $logger; + $this->eventDispatcher = $eventDispatcher; + } + + public function truncateQueue(): void + { + /** @var QuickBooks_Driver_Sql_Mysqli $driver */ + $driver = QuickBooks_Driver_Factory::create($this->dsn); + $driver->query('TRUNCATE TABLE quickbooks_queue', $errNum, $errMsg); + $driver->query('TRUNCATE TABLE quickbooks_ticket', $errNum, $errMsg); + } + + public function createCompany(string $username, string $password): void + { + QuickBooks_Utilities::createUser($this->dsn, $username, $password); + } + + public function schedule(?string $username, string $action, string $id, ?string $qbXml = null, ?array $extra = null): bool + { + $Queue = new QuickBooks_WebConnector_Queue($this->dsn); + return $Queue->enqueue($action, $id, $priority = 0, $extra, $username, $qbXml); + } + + public function config(QuickbooksCompany $company): string + { + $username = $company->getQbUsername(); + Assert::notNull($username); + $companyName = $company->getCompanyName(); + Assert::notNull($companyName); + $guid = $username;//$this->createGUID($username); + $qwc = new \QuickBooks_WebConnector_QWC( + "{$this->appName} for {$companyName}", + "User: {$companyName}", + $this->projectUrl . '/qbwc', + $this->projectUrl, + $username, + $guid, + $guid, + QUICKBOOKS_TYPE_QBFS, + $readonly = false, + $company->getQbwcMinRunEveryNSeconds() + ); + return $qwc->generate(); + } + + private function createGUID(string $string): string + { + // GUID is 128-bit hex + $hash = md5($string); + // Create formatted GUID + $guid = ''; + // GUID format is XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX for readability + $guid .= substr($hash, 0, 8) . + '-' . + substr($hash, 8, 4) . + '-' . + substr($hash, 12, 4) . + '-' . + substr($hash, 16, 4) . + '-' . + substr($hash, 20, 12); + + return $guid; + } + + + public function qbwc(?string $input): string + { + //Configure the logging level + $log_level = QUICKBOOKS_LOG_DEVELOP; + + //Pure-PHP SOAP server + $soapserver = QUICKBOOKS_SOAPSERVER_BUILTIN; + + //we can turn this off + $handler_options = [ + 'deny_concurrent_logins' => false, + 'deny_reallyfast_logins' => false, + ]; + + // The next three params $map, $errmap, and $hooks are callbacks which + // will be called when certain actions/events/requests/responses occur within + // the framework + $map = [ + '*' => [[$this, '_qbRequest'], [$this, '_qbResponse']], + ]; + $errmap = [ + '*' => [$this, '_catchallErrors'], + ]; + $hooks = array( + QuickBooks_WebConnector_Handlers::HOOK_LOGINSUCCESS => [[$this,'_loginSuccess']], // requires vendor/consolibyte/quickbooks/QuickBooks/WebConnector/Handlers.php + QUICKBOOKS_HANDLERS_HOOK_SENDREQUESTXML => [[$this,'_sendRequestXml']], + QUICKBOOKS_HANDLERS_HOOK_LOGINFAILURE => [[$this,'_loginFail']], + ); + + //To be used with singleton queue + $driver_options = []; + + //Callback options, not needed at the moment + $callback_options = []; + + //nothing needed here at the moment + $soap_options = []; + + //construct a new instance of the web connector server + $Server = new QuickBooks_WebConnector_Server($this->dsn, $map, $errmap, $hooks, $log_level, $soapserver, QUICKBOOKS_WSDL, $soap_options, $handler_options, $driver_options, $callback_options); + + //_input is from file_get_contents('php://input') + $class = new ReflectionClass(QuickBooks_WebConnector_Server::class); + $property = $class->getProperty('_input'); + $property->setAccessible(true); + $property->setValue($Server, $input); + + ob_start(); + $Server->handle(false, true); + $result = ob_get_clean(); + Assert::string($result); + return $result; + } + + public function init(): void + { + if (!QuickBooks_Utilities::initialized($this->dsn)) { + QuickBooks_Utilities::initialize($this->dsn); + } + QuickBooks_WebConnector_Queue_Singleton::initialize($this->dsn); + } + + public function _loginSuccess(?string $requestId, string $user, string $hook, ?string &$err, + array $hook_data, array $callbackConfig): void + { + } + + public function _loginFail(?string $requestId, string $user, string $action, string $ident, ?array $extra, ?array $err): void + { + $this->logger->error('QuickbooksServer::_loginFail: '.$action, func_get_args()); + } + + /** + * @param mixed|null $extra + * @param mixed|null $errNum + */ + public function _catchallErrors(?string $requestId, string $user, ?string $action, ?string $ident, $extra, + ?string &$err, ?string $xml, $errNum, string $errMsg, array $callbackConfig): bool + { + $this->logger->error('QuickbooksServer::_catchallErrors', func_get_args()); + return $continueOnError = true; + } + + /** + * @param mixed $extra + * @return string xml or QUICKBOOKS_NOOP trim + */ + public function _qbRequest(string $requestId, string $user, string $action, string $ident, $extra, + string &$err, ?int $lastActionTime, ?int $lastActionIdentTime, + string $version, string $locale, array $callbackConfig, ?string $qbXml): ?string + { + //$err no matter + return $qbXml; + } + + /** + * @param mixed $extra + */ + public function _qbResponse(string $requestId, string $user, string $action, string $ident, $extra, + ?string &$err, ?int $lastActionTime, ?int $lastActionIdentTime, + string $xml, array $qbIdentifier, array $callbackConfig, ?string $qbXml): void + { + $event = new QuickbooksServerResponseEvent($requestId, $user, $action, $ident, $extra, + $err, $lastActionTime, $lastActionIdentTime, $xml, $qbIdentifier, $callbackConfig, $qbXml); + $this->eventDispatcher->dispatch($event); + $err = $event->getErr(); + } + + + public function _sendRequestXml(?string $requestId, string $qbUsername, string $hook, string &$err, array $hookData, array $callbackConfig): bool + { + $event = new QuickbooksServerSendRequestXmlEvent($requestId, $qbUsername, $hook, $err, $hookData, $callbackConfig); + $this->eventDispatcher->dispatch($event); + $err = $event->getErr(); + return !$event->isStopPropagation(); + } +} diff --git a/src/QuickbooksServerInterface.php b/src/QuickbooksServerInterface.php new file mode 100644 index 0000000..5f659af --- /dev/null +++ b/src/QuickbooksServerInterface.php @@ -0,0 +1,16 @@ +createQueryBuilder('a') + ->delete() + ->where('a.company = :company') + ->setParameter('company', $company) + ->getQuery(); + + /** @var int $deleted */ + $deleted = $query->execute(); + return $deleted; + } + + public function findOneByName(string $username, string $accountName): ?QuickbooksAccount + { + $qb = $this->createQueryBuilder('a') + ->where('a.fullName = :fullName') + ->setParameter('fullName', $accountName) + ->join('a.company', 'c') + ->andWhere('c.qbUsername = :qbUsername') + ->setParameter('qbUsername', $username) + ->setMaxResults(1); + $result = $qb->getQuery()->getOneOrNullResult(); + + return $result instanceof QuickbooksAccount ? $result : null; + } + + public function getCurrency(string $username, string $accountName, ?string $accountType = null): ?string + { + $qb = $this->_em->createQueryBuilder() + ->select('a.currency') + ->from($this->_entityName, 'a') + ->setMaxResults(1) + ->join('a.company', 'c') + ->where('c.qbUsername = :username') + ->setParameter('username', $username) + ->andWhere('a.fullName = :fullName') + ->setParameter('fullName', $accountName); + if (null !== $accountType) { + $qb->andWhere('a.accountType = :accountType') + ->setParameter('accountType', $accountType); + } + $res = $qb->getQuery()->getArrayResult(); + if (count($res) === 0) { + return null; + } + return $res[0]['currency']; + } +} diff --git a/src/Repository/QuickbooksCompanyRepository.php b/src/Repository/QuickbooksCompanyRepository.php new file mode 100644 index 0000000..bce4d6b --- /dev/null +++ b/src/Repository/QuickbooksCompanyRepository.php @@ -0,0 +1,16 @@ +setPassword($newEncodedPassword); + $this->_em->persist($user); + $this->_em->flush(); + } +} diff --git a/src/Security/EnableUserScopeFilterOnRequestSubscriber.php b/src/Security/EnableUserScopeFilterOnRequestSubscriber.php new file mode 100644 index 0000000..d40f940 --- /dev/null +++ b/src/Security/EnableUserScopeFilterOnRequestSubscriber.php @@ -0,0 +1,44 @@ +em = $em; + $this->security = $security; + } + + public function onKernelRequest(RequestEvent $event): void + { + if (null !== $this->security && ($user = $this->security->getUser()) instanceof User) { + /** @var UserScopeFilter $filter */ + $filter = $this->em + ->getFilters() + ->enable('user_scope'); + + $filter->setParameter('userId', (string)$user->getId()); + $qbUsernames = array_filter(array_map(fn(QuickbooksCompany $company) => $company->getQbUsername(), iterator_to_array($user->getCompanies()))); + $filter->setParameter('qbUsernames', implode('|', $qbUsernames)); + } + } + + public static function getSubscribedEvents() + { + return [ + KernelEvents::REQUEST => 'onKernelRequest', + ]; + } +} diff --git a/src/Security/LoginFormAuthenticator.php b/src/Security/LoginFormAuthenticator.php new file mode 100644 index 0000000..d8b41a3 --- /dev/null +++ b/src/Security/LoginFormAuthenticator.php @@ -0,0 +1,109 @@ +entityManager = $entityManager; + $this->urlGenerator = $urlGenerator; + $this->csrfTokenManager = $csrfTokenManager; + $this->passwordEncoder = $passwordEncoder; + } + + public function supports(Request $request) + { + return self::LOGIN_ROUTE === $request->attributes->get('_route') + && $request->isMethod('POST'); + } + + public function getCredentials(Request $request) + { + $credentials = [ + 'email' => $request->request->get('email'), + 'password' => $request->request->get('password'), + 'csrf_token' => $request->request->get('_csrf_token'), + ]; + $request->getSession()->set( + Security::LAST_USERNAME, + $credentials['email'] + ); + + return $credentials; + } + + public function getUser($credentials, UserProviderInterface $userProvider) + { + $token = new CsrfToken('authenticate', $credentials['csrf_token']); + if (!$this->csrfTokenManager->isTokenValid($token)) { + throw new InvalidCsrfTokenException(); + } + + $user = $this->entityManager->getRepository(User::class)->findOneBy(['email' => $credentials['email']]); + + if ($user === null) { + // fail authentication with a custom error + throw new CustomUserMessageAuthenticationException('Email could not be found.'); + } + + return $user; + } + + public function checkCredentials($credentials, UserInterface $user) + { + return $this->passwordEncoder->isPasswordValid($user, $credentials['password']); + } + + /** + * Used to upgrade (rehash) the user's password automatically over time. + * @param mixed $credentials + */ + public function getPassword($credentials): ?string + { + Assert::isArray($credentials); + return $credentials['password']; + } + + public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey) + { + if (null !== $targetPath = $this->getTargetPath($request->getSession(), $providerKey)) { + return new RedirectResponse($targetPath); + } + + return new RedirectResponse($this->urlGenerator->generate('app_homepage')); + } + + protected function getLoginUrl() + { + return $this->urlGenerator->generate(self::LOGIN_ROUTE); + } +} diff --git a/src/Security/UserScopeFilter.php b/src/Security/UserScopeFilter.php new file mode 100644 index 0000000..3ff1d7b --- /dev/null +++ b/src/Security/UserScopeFilter.php @@ -0,0 +1,33 @@ +hasAssociation('user') && $this->hasParameter('userId')) { + $userId = $this->getParameter('userId'); + return "{$targetTableAlias}.user_id = {$userId}"; + } + if ($targetEntity->hasAssociation('company') && $this->hasParameter('qbUsernames')) { + $sql = array_map(fn(string $qbUsername) => "{$targetTableAlias}.qb_username = '{$qbUsername}'", $this->getQbUsernames()); + return empty($sql) ? 'false' : implode(' OR ', $sql); + } + + return ''; + } + + private function getQbUsernames(): array + { + $qbUsernames = $this->getParameter('qbUsernames'); + $qbUsernames = str_replace("'", '', $qbUsernames); + $qbUsernames = explode('|', $qbUsernames); + $qbUsernames = array_values((array)array_filter($qbUsernames)); + + return $qbUsernames; + } +} diff --git a/src/SheetScheduler.php b/src/SheetScheduler.php new file mode 100644 index 0000000..1a81554 --- /dev/null +++ b/src/SheetScheduler.php @@ -0,0 +1,275 @@ + CustomerInvoice::class, + self::TYPE_CUSTOMER => Customer::class, + self::TYPE_VENDOR_BILL => VendorBill::class, + self::TYPE_VENDOR => Vendor::class, + self::TYPE_TRANSACTION => Transaction::class, + ]; + + /** @var FilesystemInterface */ + private $sheetFilesystem; + /** @var FileDecoder */ + private $fileDecoder; + /** @var DenormalizerInterface */ + private $denormalizer; + /** @var ValidatorInterface */ + private $validator; + /** @var QuickbooksServerInterface */ + private $quickbooksServer; + /** @var TransformerResolver */ + private $transformerResolver; + /** @var QuickbooksFormatter */ + private $quickbooksFormatter; + /** @var EventDispatcherInterface */ + private $eventDispatcher; + private $propertyAccessor; + + private array $dateFields = []; + + public function __construct(QuickbooksServerInterface $quickbooksServer, + FilesystemInterface $sheetFilesystem, + FileDecoder $fileDecoder, + DenormalizerInterface $denormalizer, + ValidatorInterface $validator, + TransformerResolver $transformerResolver, + QuickbooksFormatter $quickbooksFormatter, + EventDispatcherInterface $eventDispatcher, + PropertyAccessorInterface $propertyAccessor) + { + $this->quickbooksServer = $quickbooksServer; + $this->sheetFilesystem = $sheetFilesystem; + $this->fileDecoder = $fileDecoder; + $this->denormalizer = $denormalizer; + $this->validator = $validator; + $this->transformerResolver = $transformerResolver; + $this->quickbooksFormatter = $quickbooksFormatter; + $this->eventDispatcher = $eventDispatcher; + $this->propertyAccessor = $propertyAccessor; + } + + public function schedule(QuickbooksCompany $user, string $type, UploadedFile $file, ?array $fieldsMapping = null, ?string $dateFormat = null): int + { + $toSchedule = $this->prepare($user, $type, $file, $fieldsMapping, $dateFormat); + foreach ($toSchedule as [$action, $id, $xml]) { + $xml = $this->quickbooksFormatter->formatForOutput($xml); + $this->quickbooksServer->schedule($user->getQbUsername(), $action, (string)$id, $xml); + } + + return count($toSchedule); + } + + public function dryRun(QuickbooksCompany $user, string $type, UploadedFile $file, ?array $fieldsMapping = null, ?string $dateFormat = null): string + { + $toSchedule = $this->prepare($user, $type, $file, $fieldsMapping, $dateFormat); + $output = ''; + foreach ($toSchedule as [$action, $id, $xml]) { + $output .= $xml; + } + return $this->quickbooksFormatter->formatForOutput($output); + } + + public function loadFile(UploadedFile $file, ?array $fieldsMapping = null, ?int $limit = null): array + { + $path = $file->getRealPath(); + Assert::string($path); + $items = $this->fileDecoder->decodeFile($path, $file->getMimeType()); + if (null !== $limit && count($items) > $limit) { + $items = array_slice($items, 0, $limit); + } + if (is_array($fieldsMapping) && count($fieldsMapping) > 0) { + $items = array_map(static function($input) use ($fieldsMapping): array { + $output = []; + foreach ($fieldsMapping as $keyOutput => $keyInput) { + if (null !== $keyInput && isset($input[$keyInput])) { + $output[$keyOutput] = $input[$keyInput]; + } + } + return $output; + }, $items); + } + + return $items; + } + + public function canonizeDate(array $entities, string $sourceFormat, string $targetFormat = 'Y-m-d'): array + { + if (count($entities) === 0) { + return []; + } + $dateFields = $this->getDateFields(get_class($entities[0])); + foreach ($entities as $entity) { + foreach ($dateFields as $dateField) { + if (null !== ($date = $this->propertyAccessor->getValue($entity, $dateField)) && + false !== $dateInstance = \DateTime::createFromFormat($sourceFormat, $date) + ) { + $this->propertyAccessor->setValue($entity, $dateField, $dateInstance->format($targetFormat)); + } + } + } + return $entities; + } + + public function denormalize(string $type, array $items): array + { + $class = $this->getClass($type); + try { + /** @var array $entities */ + $entities = $this->denormalizer->denormalize($items, $class . '[]'); + } catch (ExceptionInterface $e) { + throw new RuntimeException('Unable to denormalize array of objects', $e->getCode(), $e); + } + return $entities; + } + + public function validateAllEntities(QuickbooksCompany $user, array $entities): void + { + $exceptions = []; + foreach ($entities as $idx => $entity) { + $line = $idx + 1; + $errorList = $this->validator->validate($entity); + if ($errorList->count() > 0) { + $exceptions[] = new ValidationException($errorList, "Validation error on line number: {$line}"); + } + $event = new EntityOnScheduledEvent($user, [$entity], $line); + $this->eventDispatcher->dispatch($event); + foreach ($event->getEntities() as $eventIdx => $eventEntity) { + $errorList = $this->validator->validate($eventEntity); + if ($errorList->count() > 0) { + $exceptions[] = new ValidationException($errorList, "Validation error on line number: {$line}"); + } + } + } + if (count($exceptions) > 0) { + throw new ValidationsException('Validation failed', $exceptions); + } + } + + private function prepare(QuickbooksCompany $company, string $type, UploadedFile $file, ?array $fieldsMapping = null, ?string $dateFormat = null): array + { + $items = $this->loadFile($file, $fieldsMapping); + $entities = $this->denormalize($type, $items); + if (null !== $dateFormat) { + $entities = $this->canonizeDate($entities, $dateFormat); + } + $this->validateAllEntities($company, $entities); + + $remotePath = $file->getClientOriginalName(); + Assert::string($remotePath); + $class = $this->getClass($type); + $transformer = $this->transformerResolver->resolve($class); + $transformer->setCompany($company); + $toSchedule = []; + foreach ($entities as $idx => $entity) { + $line = $idx + 1; + $event = new EntityOnScheduledEvent($company, [$entity], $line); + $this->eventDispatcher->dispatch($event); + foreach ($event->getEntities() as $eventIdx => $eventEntity) { + $qbEntities = $transformer->transform($eventEntity); + /** + * @var string $action + * @var QuickBooks_QBXML_Object $qbEntity + */ + foreach ($qbEntities as [$action, $qbEntity]) { + $xml = $qbEntity->asQBXML($action); + $id = $this->shrinkIdent($remotePath, (string)$line, (string)$eventIdx); + $toSchedule[] = [$action, $id, $xml]; + } + } + } + return $toSchedule; + } + + public function shrinkIdent(string $filename, string $line, string $eventIdx): string + { + $maxIdentLen = 40; + $rest = ':'.$line.':'.$eventIdx; + $allowedFilenameLen = $maxIdentLen - mb_strlen($rest); + Assert::greaterThan($allowedFilenameLen, 0); + if (mb_strlen($filename) > $allowedFilenameLen) { + $hashLen = min(3, $allowedFilenameLen); + $hash = mb_substr(md5($filename), 0, $hashLen); + $filename = mb_substr($filename, 0, $allowedFilenameLen - $hashLen) . $hash; + Assert::eq(mb_strlen($filename), $allowedFilenameLen); + } + return mb_substr($filename.$rest, -40); + } + + public function copyToLocal(string $remotePath): UploadedFile + { + try { + $content = $this->sheetFilesystem->read($remotePath); + } catch (FileNotFoundException $e) { + throw new NotFoundException("File {$remotePath} not found", $e->getCode(), $e); + } + $path = tempnam(sys_get_temp_dir(), 'import'); + if (false === $path || false === file_put_contents($path, $content)) { + throw new RuntimeException('Unable to save the file'); + } + $origName = basename($remotePath); + return new UploadedFile($path, $origName); + } + + private function getClass(string $type): string + { + if (!isset(self::CLASS_MAP[$type])) { + throw new RuntimeException("Type {$type} is not supported"); + } + return self::CLASS_MAP[$type]; + } + + private function getDateFields(string $class): array + { + if (!isset($this->dateFields[$class])) { + $dateFields = []; + /** @var ClassMetadata $metadata */ + $metadata = $this->validator->getMetadataFor($class); + foreach ($metadata->properties as $property) { + foreach ($property->constraints as $constraint) { + if ($constraint instanceof Date) { + $dateFields[] = $property->name; + } + } + } + $this->dateFields[$class] = $dateFields; + } + return $this->dateFields[$class]; + } +} diff --git a/src/SheetScheduler/Customer.php b/src/SheetScheduler/Customer.php new file mode 100644 index 0000000..921f3a6 --- /dev/null +++ b/src/SheetScheduler/Customer.php @@ -0,0 +1,270 @@ +getCustomerNameWithAttention(); + if ('' !== $attn) { + $arr[] = $attn; + } + $company = $this->companyName; + if (null !== $company && '' !== trim($company)) { + $arr[] = $company; + } + $arr[] = $this->addr1; + $arr[] = $this->addr2; + while (count($arr) < 5) { $arr[] = ''; } + + $arr[] = $this->city; + $arr[] = $this->state; + $arr[] = ''; + $arr[] = $this->postalcode; + $arr[] = $this->country; + + return $arr; + } + + private function getCustomerNameWithAttention(): string + { + $arr = array_filter([ + trim($this->firstName ?? ''), + trim($this->lastName ?? ''), + ]); + if (count($arr) === 0) { + return ''; + } + return 'Attn: '.implode(' ', array_filter($arr)); + } + + /** + * @return string|null + */ + public function getCustomerFullName(): ?string + { + return $this->customerFullName; + } + + /** + * @param string|null $customerFullName + */ + public function setCustomerFullName(?string $customerFullName): void + { + $this->customerFullName = $customerFullName; + } + + /** + * @return string|null + */ + public function getFirstName(): ?string + { + return $this->firstName; + } + + /** + * @param string|null $firstName + */ + public function setFirstName(?string $firstName): void + { + $this->firstName = $firstName; + } + + /** + * @return string|null + */ + public function getLastName(): ?string + { + return $this->lastName; + } + + /** + * @param string|null $lastName + */ + public function setLastName(?string $lastName): void + { + $this->lastName = $lastName; + } + + /** + * @return string|null + */ + public function getCompanyName(): ?string + { + return $this->companyName; + } + + /** + * @param string|null $companyName + */ + public function setCompanyName(?string $companyName): void + { + $this->companyName = $companyName; + } + + /** + * @return string|null + */ + public function getTerms(): ?string + { + return $this->terms; + } + + /** + * @param string|null $terms + */ + public function setTerms(?string $terms): void + { + $this->terms = $terms; + } + + /** + * @return string|null + */ + public function getCurrency(): ?string + { + return $this->currency; + } + + /** + * @param string|null $currency + */ + public function setCurrency(?string $currency): void + { + $this->currency = $currency; + } + + /** + * @return string|null + */ + public function getAddr1(): ?string + { + return $this->addr1; + } + + /** + * @param string|null $addr1 + */ + public function setAddr1(?string $addr1): void + { + $this->addr1 = $addr1; + } + + /** + * @return string|null + */ + public function getAddr2(): ?string + { + return $this->addr2; + } + + /** + * @param string|null $addr2 + */ + public function setAddr2(?string $addr2): void + { + $this->addr2 = $addr2; + } + + /** + * @return string|null + */ + public function getCity(): ?string + { + return $this->city; + } + + /** + * @param string|null $city + */ + public function setCity(?string $city): void + { + $this->city = $city; + } + + /** + * @return string|null + */ + public function getState(): ?string + { + return $this->state; + } + + /** + * @param string|null $state + */ + public function setState(?string $state): void + { + $this->state = $state; + } + + /** + * @return string|null + */ + public function getPostalcode(): ?string + { + return $this->postalcode; + } + + /** + * @param string|null $postalcode + */ + public function setPostalcode(?string $postalcode): void + { + $this->postalcode = $postalcode; + } + + /** + * @return string|null + */ + public function getCountry(): ?string + { + return $this->country; + } + + /** + * @param string|null $country + */ + public function setCountry(?string $country): void + { + $this->country = $country; + } +} diff --git a/src/SheetScheduler/CustomerInvoice.php b/src/SheetScheduler/CustomerInvoice.php new file mode 100644 index 0000000..5a53c87 --- /dev/null +++ b/src/SheetScheduler/CustomerInvoice.php @@ -0,0 +1,203 @@ +line1Quantity, $this->line1Rate, $this->line1Amount); + } catch (RuntimeException $e) { + $context->addViolation($e->getMessage()); + } + } + + public function getRefNumber(): ?string + { + return $this->refNumber; + } + + public function setRefNumber(?string $refNumber): void + { + $this->refNumber = $refNumber; + } + + /** + * @return string|null + */ + public function getInvoiceMemo(): ?string + { + return $this->invoiceMemo; + } + + /** + * @param string|null $invoiceMemo + */ + public function setInvoiceMemo(?string $invoiceMemo): void + { + $this->invoiceMemo = $invoiceMemo; + } + + /** + * @return string|null + */ + public function getArAccount(): ?string + { + return $this->arAccount; + } + + /** + * @param string|null $arAccount + */ + public function setArAccount(?string $arAccount): void + { + $this->arAccount = $arAccount; + } + + /** + * @return string|null + */ + public function getTxnDate(): ?string + { + return $this->txnDate; + } + + /** + * @param string|null $txnDate + */ + public function setTxnDate(?string $txnDate): void + { + $this->txnDate = $txnDate; + } + + /** + * @return string|null + */ + public function getExchangeRate(): ?string + { + return $this->exchangeRate; + } + + /** + * @param string|null $exchangeRate + */ + public function setExchangeRate(?string $exchangeRate): void + { + $this->exchangeRate = $exchangeRate; + } + + /** + * @return string|null + */ + public function getLine1ItemName(): ?string + { + return $this->line1ItemName; + } + + /** + * @param string|null $line1ItemName + */ + public function setLine1ItemName(?string $line1ItemName): void + { + $this->line1ItemName = $line1ItemName; + } + + /** + * @return string|null + */ + public function getLine1Desc(): ?string + { + return $this->line1Desc; + } + + /** + * @param string|null $line1Desc + */ + public function setLine1Desc(?string $line1Desc): void + { + $this->line1Desc = $line1Desc; + } + + /** + * @return string|null + */ + public function getLine1Quantity(): ?string + { + return $this->line1Quantity; + } + + /** + * @param string|null $line1Quantity + */ + public function setLine1Quantity(?string $line1Quantity): void + { + $this->line1Quantity = $line1Quantity; + } + + /** + * @return string|null + */ + public function getLine1Amount(): ?string + { + return $this->line1Amount; + } + + /** + * @param string|null $line1Amount + */ + public function setLine1Amount(?string $line1Amount): void + { + $this->line1Amount = $line1Amount; + } + + /** + * @return string|null + */ + public function getLine1Rate(): ?string + { + return $this->line1Rate; + } + + /** + * @param string|null $line1Rate + */ + public function setLine1Rate(?string $line1Rate): void + { + $this->line1Rate = $line1Rate; + } +} diff --git a/src/SheetScheduler/EntityOnScheduledEvent.php b/src/SheetScheduler/EntityOnScheduledEvent.php new file mode 100644 index 0000000..97a0693 --- /dev/null +++ b/src/SheetScheduler/EntityOnScheduledEvent.php @@ -0,0 +1,56 @@ +user = $user; + $this->entities = $entities; + $this->line = $line; + } + + /** + * @return QuickbooksCompany + */ + public function getUser(): QuickbooksCompany + { + return $this->user; + } + + /** + * @return int + */ + public function getLine(): int + { + return $this->line; + } + + /** + * @return array + */ + public function getEntities(): array + { + return $this->entities; + } + + /** + * @param array $entities + */ + public function setEntities(array $entities): void + { + $this->entities = $entities; + } +} diff --git a/src/SheetScheduler/EntityTransformerInterface.php b/src/SheetScheduler/EntityTransformerInterface.php new file mode 100644 index 0000000..2416ce8 --- /dev/null +++ b/src/SheetScheduler/EntityTransformerInterface.php @@ -0,0 +1,18 @@ +decoder = $decoder; + } + + public function decodeFile(string $path, ?string $mimeType): array + { + $mimeType = $mimeType ?? 'text/plain'; + return $mimeType === 'text/plain' ? $this->decodeAsCsv($path) : $this->decodeAsExcel($path); + } + + private function decodeAsCsv(string $path): array + { + $contents = file_get_contents($path); + if (false === $contents) { + throw new RuntimeException('Unable to read file: ' . $path); + } + $contents = trim($contents); + return $this->decoder->decode($contents, 'csv', ['as_collection' => true]); + } + + private function decodeAsExcel(string $path): array + { + try { + /** @var BaseReader $reader */ + $reader = IOFactory::createReaderForFile($path); + $reader->setReadDataOnly(true); + $spreadsheet = $reader->load($path); + $worksheet = $spreadsheet->getActiveSheet(); + } catch (\Throwable $e) { + throw new RuntimeException('Unable to read file: ' . $path, $e->getCode(), $e); + } + $rows = $worksheet->toArray(); + if (count($rows) <= 1) { + return []; + } + $results = []; + $header = array_shift($rows); + foreach ($rows as $row) { + $results[] = array_combine($header, $row); + } + return $results; + } +} diff --git a/src/SheetScheduler/LineItemLogic.php b/src/SheetScheduler/LineItemLogic.php new file mode 100644 index 0000000..0aa867c --- /dev/null +++ b/src/SheetScheduler/LineItemLogic.php @@ -0,0 +1,33 @@ +serializer = $serializer; + } + + public function generateSampleCsv(string $type): string + { + $entities = []; + switch ($type) { + case SheetScheduler::TYPE_CUSTOMER: + $entities[] = $this->getSampleCustomer(); + break; + case SheetScheduler::TYPE_CUSTOMER_INVOICE: + $entities[] = $this->getSampleCustomerInvoice(); + break; + case SheetScheduler::TYPE_VENDOR_BILL: + $entities[] = $this->getSampleVendorBill(); + break; + case SheetScheduler::TYPE_VENDOR: + $entities[] = $this->getSampleVendor(); + break; + case SheetScheduler::TYPE_TRANSACTION: + $entities = $this->getSampleTransactions(); + break; + default: + throw new RuntimeException("{$type} is not supported"); + } + + return $this->serializer->serialize($entities, 'csv'); + } + + private function getSampleVendor(?Vendor $entity = null): Vendor + { + $entity = $entity ?? new Vendor(); + + $entity->setVendorFullname('Silo'); + $entity->setVendorCompanyName('Silo LIMITED'); + + $entity->setAddr1('68 Tap Kwok Nam Path'); + $entity->setAddr2('Lok Sheuk Tsan'); + $entity->setCity('Hong Kong'); + $entity->setState('HK'); + $entity->setPostalcode('999077'); + $entity->setCountry('Hong Kong'); + + $entity->setVendorType('Service Providers'); + $entity->setTerms('Due on receipt'); + $entity->setCurrency('US Dollar'); + return $entity; + } + + private function getSampleTransactions(): array + { + $entities = []; + + $entities[] = $this->getTransaction('2018-10-10', '1', 'ref21', 'US Dollar', + 'Undeposited Funds', '500.00', 'credit memo', + 'Chase Savings USD', '500.00', 'debit memo'); + + $entities[] = $this->getTransaction('2018-10-10', '1', 'ref22', 'US Dollar', + 'Chase Savings USD', '400.00', 'credit memo', + 'Ask My Accountant', '400.00', 'debit memo'); + + $entities[] = $this->getTransaction('2020-04-01', '', 'ref23', 'Euro', + 'Chase Savings USD', '10.09', 'credit memo', + 'Chase Savings EUR', '9.02', 'debit memo'); + + $entities[] = $this->getTransaction('2018-10-10', '', 'ref24', 'Euro', + 'Chase Savings EUR', '100.00', 'credit memo', + 'Ask My Accountant', '100.00', 'debit memo'); + + return $entities; + } + + private function getTransaction(?string $txnDate, ?string $exchangeRate, ?string $refNumber, ?string $currency, + ?string $creditAccount, ?string $creditAmount, ?string $creditMemo, + ?string $debitAccount, ?string $debitAmount, ?string $debitMemo): Transaction + { + + $entity = new Transaction(); + + $entity->setExchangeRate($exchangeRate); + $entity->setCurrency($currency); + + $entity->setTxnDate($txnDate); + $entity->setRefNumber($refNumber); + + $entity->setCreditAccount($creditAccount); + $entity->setCreditAmount($creditAmount); + $entity->setCreditMemo($creditMemo); + $entity->setDebitAccount($debitAccount); + $entity->setDebitAmount($debitAmount); + $entity->setDebitMemo($debitMemo); + + return $entity; + } + + private function getSampleVendorBill(): VendorBill + { + $entity = new VendorBill(); + $this->getSampleVendor($entity); + + $entity->setMemo('Bill Memo'); + $entity->setApAccount('Accounts Payable'); + $entity->setRefNumber('2018-05-10-0001'); + $entity->setTxnDate('2018-05-10'); + $entity->setLine1AccountFullName('Rent Expense'); + $entity->setLine1Amount('10.00'); + $entity->setLine1Memo('Item1 Memo'); + $entity->setLine2AccountFullName('Rent Expense'); + $entity->setLine2Amount('15.00'); + $entity->setLine2Memo('Item2 Memo'); + $entity->setExchangeRate('1'); + + return $entity; + } + + private function getSampleCustomer(?Customer $entity = null): Customer + { + $entity = $entity ?? new Customer(); + + $entity->setCustomerFullName('HomeBase'); + $entity->setFirstName('Long'); + $entity->setLastName('Allen'); + $entity->setCompanyName('HomeBase LLC'); + + $entity->setAddr1('4420 Shadowmar Drive'); + $entity->setAddr2('Louisiana'); + $entity->setCity('New Orleans'); + $entity->setState('LA'); + $entity->setPostalcode('70112'); + $entity->setCountry('United States'); + + $entity->setTerms('Due on receipt'); + $entity->setCurrency('US Dollar'); + + return $entity; + } + + private function getSampleCustomerInvoice(): CustomerInvoice + { + $entity = new CustomerInvoice(); + $this->getSampleCustomer($entity); + + $entity->setRefNumber('180510-0001'); + $entity->setTxnDate('2018-05-10'); + $entity->setExchangeRate('1'); + + $entity->setInvoiceMemo('Invoice memo'); + $entity->setArAccount('Accounts Receivable'); + + $entity->setLine1ItemName('Consulting'); + $entity->setLine1Desc('Item desc'); + $entity->setLine1Amount('10.00'); + $entity->setLine1Quantity('1'); + + return $entity; + } +} diff --git a/src/SheetScheduler/SplitTransactionsSubscriber.php b/src/SheetScheduler/SplitTransactionsSubscriber.php new file mode 100644 index 0000000..23fdaae --- /dev/null +++ b/src/SheetScheduler/SplitTransactionsSubscriber.php @@ -0,0 +1,132 @@ +em = $em; + } + + public function onScheduled(EntityOnScheduledEvent $event): void + { + $username = $event->getUser()->getQbUsername(); + Assert::notNull($username); + + $results = []; + foreach ($event->getEntities() as $entity) { + if (!$entity instanceof Transaction) { + $results[] = $entity; + continue; + } + + /** @var QuickbooksAccountRepositoryInterface $repo */ + $repo = $this->em->getRepository(QuickbooksAccount::class); + + $creditAccount = $entity->getCreditAccount(); + $debitAccount = $entity->getDebitAccount(); + Assert::notNull($creditAccount); + Assert::notNull($debitAccount); + + $fallbackCurrency = $entity->getCurrency(); + $creditCurrency = $repo->getCurrency($username, $creditAccount, QuickbooksAccount::TYPE_BANK) ?? $fallbackCurrency; + $debitCurrency = $repo->getCurrency($username, $debitAccount, QuickbooksAccount::TYPE_BANK) ?? $fallbackCurrency; + + if ($creditCurrency === $debitCurrency) { + $results[] = $entity; + } else { + $results[] = $this->createTransaction( + $entity->getTxnDate(), $entity->getExchangeRate(), $entity->getRefNumber(), + $creditCurrency, $creditAccount, $entity->getCreditAmount(), $entity->getCreditMemo(), + QuickbooksAccount::UNDEPOSITED_FUNDS, $entity->getCreditAmount(), $entity->getDebitMemo() + ); + $results[] = $this->createTransaction( + $entity->getTxnDate(), self::ID, $entity->getRefNumber(), + $debitCurrency, QuickbooksAccount::UNDEPOSITED_FUNDS, $entity->getDebitAmount(), $entity->getCreditMemo(), + $debitAccount, $entity->getDebitAmount(), $entity->getDebitMemo() + ); + } + } + $event->setEntities($results); + } + + public function afterExchangeRateUpdated(EntityOnScheduledEvent $event): void + { + $username = $event->getUser()->getQbUsername(); + Assert::notNull($username); + + /** @var Transaction|null $prev */ + $prev = null; + foreach ($event->getEntities() as $entity) { + if (!$entity instanceof Transaction) { + continue; + } + if ($prev !== null && $entity->getExchangeRate() === self::ID + && (null !== $creditAmount = $prev->getCreditAmount()) + && (null !== $exchangeRate = $prev->getExchangeRate()) + && (null !== $debitAmount = $entity->getDebitAmount()) + && $entity->getCreditAccount() === QuickbooksAccount::UNDEPOSITED_FUNDS + && $prev->getDebitAccount() === QuickbooksAccount::UNDEPOSITED_FUNDS + && $creditAmount === $prev->getDebitAmount() + && $entity->getCreditAmount() === $debitAmount + ) { + $creditExchangeRate = $this->getReversedExchangeRate((float)$debitAmount, (float)$exchangeRate, (float)$creditAmount); + $entity->setExchangeRate($creditExchangeRate); + } else { + $prev = $entity; + } + } + } + + private function getReversedExchangeRate(float $debitAmount, float $creditExchangeRate, float $creditAmount): ?string + { + $baseCurrencyAmount = $creditExchangeRate * $creditAmount; + if ($debitAmount > 0) { + return (string)($baseCurrencyAmount / $debitAmount); + } + return null; + } + + private function createTransaction(?string $txnDate, ?string $exchangeRate, ?string $refNumber, ?string $currency, + ?string $creditAccount, ?string $creditAmount, ?string $creditMemo, + ?string $debitAccount, ?string $debitAmount, ?string $debitMemo): Transaction + { + $entity = new Transaction(); + + $entity->setExchangeRate($exchangeRate); + $entity->setCurrency($currency); + + $entity->setTxnDate($txnDate); + $entity->setRefNumber($refNumber); + + $entity->setCreditAccount($creditAccount); + $entity->setCreditAmount($creditAmount); + $entity->setCreditMemo($creditMemo); + $entity->setDebitAccount($debitAccount); + $entity->setDebitAmount($debitAmount); + $entity->setDebitMemo($debitMemo); + + return $entity; + } + + public static function getSubscribedEvents(): array + { + return [ + EntityOnScheduledEvent::class => [ + ['onScheduled', EntityOnScheduledEvent::PRIORITY_MANUPILATE], + ['afterExchangeRateUpdated', EntityOnScheduledEvent::PRIORITY_POST_UPDATE], + ], + ]; + } +} diff --git a/src/SheetScheduler/Transaction.php b/src/SheetScheduler/Transaction.php new file mode 100644 index 0000000..ecf12db --- /dev/null +++ b/src/SheetScheduler/Transaction.php @@ -0,0 +1,198 @@ +txnDate; + } + + /** + * @param string|null $txnDate + */ + public function setTxnDate(?string $txnDate): void + { + $this->txnDate = $txnDate; + } + + /** + * @return string|null + */ + public function getRefNumber(): ?string + { + return $this->refNumber; + } + + /** + * @param string|null $refNumber + */ + public function setRefNumber(?string $refNumber): void + { + $this->refNumber = $refNumber; + } + + /** + * @return string|null + */ + public function getCurrency(): ?string + { + return $this->currency; + } + + /** + * @param string|null $currency + */ + public function setCurrency(?string $currency): void + { + $this->currency = $currency; + } + + /** + * @return string|null + */ + public function getExchangeRate(): ?string + { + return $this->exchangeRate; + } + + /** + * @param string|null $exchangeRate + */ + public function setExchangeRate(?string $exchangeRate): void + { + $this->exchangeRate = $exchangeRate; + } + + /** + * @return string|null + */ + public function getCreditAccount(): ?string + { + return $this->creditAccount; + } + + /** + * @param string|null $creditAccount + */ + public function setCreditAccount(?string $creditAccount): void + { + $this->creditAccount = $creditAccount; + } + + /** + * @return string|null + */ + public function getCreditMemo(): ?string + { + return $this->creditMemo; + } + + /** + * @param string|null $creditMemo + */ + public function setCreditMemo(?string $creditMemo): void + { + $this->creditMemo = $creditMemo; + } + + /** + * @return string|null + */ + public function getCreditAmount(): ?string + { + return $this->creditAmount; + } + + /** + * @param string|null $creditAmount + */ + public function setCreditAmount(?string $creditAmount): void + { + $this->creditAmount = $creditAmount; + } + + /** + * @return string|null + */ + public function getDebitAccount(): ?string + { + return $this->debitAccount; + } + + /** + * @param string|null $debitAccount + */ + public function setDebitAccount(?string $debitAccount): void + { + $this->debitAccount = $debitAccount; + } + + /** + * @return string|null + */ + public function getDebitMemo(): ?string + { + return $this->debitMemo; + } + + /** + * @param string|null $debitMemo + */ + public function setDebitMemo(?string $debitMemo): void + { + $this->debitMemo = $debitMemo; + } + + /** + * @return string|null + */ + public function getDebitAmount(): ?string + { + return $this->debitAmount; + } + + /** + * @param string|null $debitAmount + */ + public function setDebitAmount(?string $debitAmount): void + { + $this->debitAmount = $debitAmount; + } +} diff --git a/src/SheetScheduler/Transformer/CustomerInvoiceTransformer.php b/src/SheetScheduler/Transformer/CustomerInvoiceTransformer.php new file mode 100644 index 0000000..c145394 --- /dev/null +++ b/src/SheetScheduler/Transformer/CustomerInvoiceTransformer.php @@ -0,0 +1,69 @@ +customerTransformer = $customerTransformer; + } + + public function supports(string $class): bool + { + return $class === CustomerInvoice::class; + } + + /** + * @param CustomerInvoice|object $entity + */ + public function transform($entity): array + { + Assert::isInstanceOf($entity, CustomerInvoice::class); + $this->customerTransformer->setCompany($this->company); + + $Invoice = new QuickBooks_QBXML_Object_Invoice(); + + $Invoice->setCustomerFullName($entity->getCustomerFullName()); + $Invoice->setRefNumber($entity->getRefNumber()); + $Invoice->setMemo($entity->getInvoiceMemo()); + $Invoice->setARAccountName($entity->getArAccount()); + $Invoice->setTermsName($entity->getTerms()); + + $Invoice->setTxnDate($entity->getTxnDate()); + if ($this->isMultiCurrencyEnabled()) { + $Invoice->set('ExchangeRate', $entity->getExchangeRate()); + } + [$a1, $a2, $a3, $a4, $a5, $ct, $st, $pr, $zip, $cn] = $entity->composeBillAddress(); + $Invoice->setBillAddress($a1, $a2, $a3, $a4, $a5, $ct, $st, $pr, $zip, $cn); + + $InvoiceLine1 = new QuickBooks_QBXML_Object_Invoice_InvoiceLine(); + $InvoiceLine1->setItemName($entity->getLine1ItemName()); + $InvoiceLine1->setDesc($entity->getLine1Desc()); + $InvoiceLine1->setRate($this->getAmount($entity->getLine1Rate())); + $InvoiceLine1->setAmount($this->getAmount($entity->getLine1Amount())); + $quantity = LineItemLogic::getQuantity( + $entity->getLine1Quantity(), + $entity->getLine1Rate(), + $entity->getLine1Amount() + ); + $InvoiceLine1->setQuantity($quantity); + $Invoice->addInvoiceLine($InvoiceLine1); + + $results = $this->customerTransformer->transform($entity); + $results[] = [QUICKBOOKS_ADD_INVOICE, $Invoice]; + return $results; + } +} diff --git a/src/SheetScheduler/Transformer/CustomerTransformer.php b/src/SheetScheduler/Transformer/CustomerTransformer.php new file mode 100644 index 0000000..a03c977 --- /dev/null +++ b/src/SheetScheduler/Transformer/CustomerTransformer.php @@ -0,0 +1,46 @@ +setFullName($entity->getCustomerFullName()); + $Customer->setFirstName($entity->getFirstName()); + $Customer->setLastName($entity->getLastName()); + $Customer->setCompanyName($entity->getCompanyName()); + $Customer->setTermsFullName($entity->getTerms()); + + [$a1, $a2, $a3, $a4, $a5, $ct, $st, $pr, $zip, $cn] = $entity->composeBillAddress(); + $Customer->setBillAddress($a1, $a2, $a3, $a4, $a5, $ct, $st, $pr, $zip, $cn); + + if ($this->isMultiCurrencyEnabled()) { + $currency = $entity->getCurrency(); + if ($currency !== null && $currency !== '') { + $Customer->set('CurrencyRef FullName', $entity->getCurrency()); + } + } + return [ + [QUICKBOOKS_ADD_CUSTOMER, $Customer] + ]; + } +} diff --git a/src/SheetScheduler/Transformer/TransactionTransformer.php b/src/SheetScheduler/Transformer/TransactionTransformer.php new file mode 100644 index 0000000..fbf5f7c --- /dev/null +++ b/src/SheetScheduler/Transformer/TransactionTransformer.php @@ -0,0 +1,64 @@ +createEntry( + $entity->getTxnDate(), $entity->getRefNumber(), $entity->getExchangeRate(), $entity->getCurrency(), + $entity->getCreditAccount(), $entity->getCreditAmount(), $entity->getCreditMemo(), + $entity->getDebitAccount(), $entity->getDebitMemo(), $entity->getDebitAmount() + ); + + return [ + [QUICKBOOKS_ADD_JOURNALENTRY, $journalEntry], + ]; + } + + private function createEntry(?string $date, ?string $refNumber, ?string $exchangeRate, ?string $currency, ?string $creditAccount, + ?string $creditAmount, ?string $creditMemo, ?string $debitAccount, ?string $debitMemo, ?string $debitAmount): QuickBooks_QBXML_Object_JournalEntry + { + + $entry = new QuickBooks_QBXML_Object_JournalEntry(); + $entry->setTxnDate($date); + $entry->setRefNumber($refNumber); + + if ($this->isMultiCurrencyEnabled()) { + $entry->set('ExchangeRate', $exchangeRate); + $entry->set('CurrencyRef FullName', $currency); + } + // amount from crediting side to the debited one + $creditLine = new \QuickBooks_QBXML_Object_JournalEntry_JournalCreditLine(); + $creditLine->setAccountName($creditAccount); + $creditLine->setAmount($this->getAmount($creditAmount)); + $creditLine->setMemo($creditMemo); + $entry->addCreditLine($creditLine); + $debitLine = new \QuickBooks_QBXML_Object_JournalEntry_JournalDebitLine(); + $debitLine->setAccountName($debitAccount); + $debitLine->setAmount($this->getAmount($debitAmount)); + $debitLine->setMemo($debitMemo); + $entry->addDebitLine($debitLine); + + return $entry; + } + +} diff --git a/src/SheetScheduler/Transformer/TransformerContextTrait.php b/src/SheetScheduler/Transformer/TransformerContextTrait.php new file mode 100644 index 0000000..07d8396 --- /dev/null +++ b/src/SheetScheduler/Transformer/TransformerContextTrait.php @@ -0,0 +1,43 @@ +company = $company; + } + + public function isMultiCurrencyEnabled(): bool + { + return null !== $this->company? $this->company->isMultiCurrencyEnabled() : false; + } + + /** + * Cast to decimal symbol still doesn't work + * because of sprintf('%01.2f', (float) $amount) + * See setAmountType/getAmountType in \QuickBooks_QBXML_Object + */ + public function getAmount(?string $amount): ?string + { + if (null === $amount) { + return null; + } + $amount = preg_replace('/[^0-9.-]/', '', $amount); + + if (is_string($amount) && + null !== $this->company && + null !== ($symbol = $this->company->getDecimalSymbol()) && + $symbol !== '.' + ) { + $amount = str_replace('.', $symbol, $amount); + } + return $amount; + } +} diff --git a/src/SheetScheduler/Transformer/VendorBillTransformer.php b/src/SheetScheduler/Transformer/VendorBillTransformer.php new file mode 100644 index 0000000..4d5c80f --- /dev/null +++ b/src/SheetScheduler/Transformer/VendorBillTransformer.php @@ -0,0 +1,75 @@ +vendorTransformer = $vendorTransformer; + } + + public function supports(string $class): bool + { + return $class === VendorBill::class; + } + + /** + * @param VendorBill|object $entity + */ + public function transform($entity): array + { + Assert::isInstanceOf($entity, VendorBill::class); + $this->vendorTransformer->setCompany($this->company); + + $bill = new QuickBooks_QBXML_Object_Bill(); + $bill->setVendorFullname($entity->getVendorFullname()); + $bill->setMemo($entity->getMemo()); + + $terms = $entity->getTerms(); + if ($terms !== null) { + $bill->set('TermsRef FullName', $terms); + } + if (null !== $apAccount = $entity->getApAccount()) { + $bill->set('APAccountRef FullName', $apAccount); + } + + $bill->setRefNumber($entity->getRefNumber()); + $bill->setTxnDate($entity->getTxnDate()); + + if ($this->isMultiCurrencyEnabled()) { + if (null !== $exchangeRate = $entity->getExchangeRate()) { + $bill->set('ExchangeRate', $exchangeRate); + } + } + $line1 = new QuickBooks_QBXML_Object_Bill_ExpenseLine(); + $line1->setAccountFullName($entity->getLine1AccountFullName()); + $line1->setAmount($this->getAmount($entity->getLine1Amount())); + $line1->setMemo($entity->getLine1Memo()); + $bill->addExpenseLine($line1); + + if (!LineItemLogic::isValueEmpty($entity->getLine2AccountFullName())) { + $line2 = new QuickBooks_QBXML_Object_Bill_ExpenseLine(); + $line2->setAccountFullName($entity->getLine2AccountFullName()); + $line2->setAmount($this->getAmount($entity->getLine2Amount())); + $line2->setMemo($entity->getLine2Memo()); + $bill->addExpenseLine($line2); + } + + $results = $this->vendorTransformer->transform($entity); + $results[] = [QUICKBOOKS_ADD_BILL, $bill]; + return $results; + } +} diff --git a/src/SheetScheduler/Transformer/VendorTransformer.php b/src/SheetScheduler/Transformer/VendorTransformer.php new file mode 100644 index 0000000..fa25fa8 --- /dev/null +++ b/src/SheetScheduler/Transformer/VendorTransformer.php @@ -0,0 +1,48 @@ +setName($entity->getVendorFullname()); + $vendor->setCompanyName($entity->getVendorCompanyName()); + + $vendor->setVendorAddress($entity->getAddr1(), $entity->getAddr2(), '', '', '', + $entity->getCity(), $entity->getState(), $entity->getPostalcode(), $entity->getCountry()); + + $vendor->setVendorTypeRef($entity->getVendorType()); + $terms = $entity->getTerms(); + if ($terms !== null) { + $vendor->set('TermsRef FullName', $terms); + } + if ($this->isMultiCurrencyEnabled()) { + $currency = $entity->getCurrency(); + if ($currency !== null) { + $vendor->set('CurrencyRef FullName', $currency); + } + } + return [ + [QUICKBOOKS_ADD_VENDOR, $vendor] + ]; + } +} diff --git a/src/SheetScheduler/TransformerResolver.php b/src/SheetScheduler/TransformerResolver.php new file mode 100644 index 0000000..7d9a7fb --- /dev/null +++ b/src/SheetScheduler/TransformerResolver.php @@ -0,0 +1,26 @@ +entityTransformers[] = $entityTransformer; + } + + public function resolve(string $class): EntityTransformerInterface + { + foreach ($this->entityTransformers as $entityTransformer) { + if ($entityTransformer->supports($class)) { + return $entityTransformer; + } + } + throw new NotFoundException("Unable to resolve data transformer for class {$class}"); + } +} diff --git a/src/SheetScheduler/Vendor.php b/src/SheetScheduler/Vendor.php new file mode 100644 index 0000000..b537d15 --- /dev/null +++ b/src/SheetScheduler/Vendor.php @@ -0,0 +1,215 @@ +vendorFullname; + } + + /** + * @param string|null $vendorFullname + */ + public function setVendorFullname(?string $vendorFullname): void + { + $this->vendorFullname = $vendorFullname; + } + + /** + * @return string|null + */ + public function getVendorCompanyName(): ?string + { + return $this->vendorCompanyName; + } + + /** + * @param string|null $vendorCompanyName + */ + public function setVendorCompanyName(?string $vendorCompanyName): void + { + $this->vendorCompanyName = $vendorCompanyName; + } + + /** + * @return string|null + */ + public function getAddr1(): ?string + { + return $this->addr1; + } + + /** + * @param string|null $addr1 + */ + public function setAddr1(?string $addr1): void + { + $this->addr1 = $addr1; + } + + /** + * @return string|null + */ + public function getAddr2(): ?string + { + return $this->addr2; + } + + /** + * @param string|null $addr2 + */ + public function setAddr2(?string $addr2): void + { + $this->addr2 = $addr2; + } + + /** + * @return string|null + */ + public function getVendorType(): ?string + { + return $this->vendorType; + } + + /** + * @param string|null $vendorType + */ + public function setVendorType(?string $vendorType): void + { + $this->vendorType = $vendorType; + } + + /** + * @return string|null + */ + public function getTerms(): ?string + { + return $this->terms; + } + + /** + * @param string|null $terms + */ + public function setTerms(?string $terms): void + { + $this->terms = $terms; + } + + /** + * @return string|null + */ + public function getCurrency(): ?string + { + return $this->currency; + } + + /** + * @param string|null $currency + */ + public function setCurrency(?string $currency): void + { + $this->currency = $currency; + } + + /** + * @return string|null + */ + public function getCity(): ?string + { + return $this->city; + } + + /** + * @param string|null $city + */ + public function setCity(?string $city): void + { + $this->city = $city; + } + + /** + * @return string|null + */ + public function getState(): ?string + { + return $this->state; + } + + /** + * @param string|null $state + */ + public function setState(?string $state): void + { + $this->state = $state; + } + + /** + * @return string|null + */ + public function getPostalcode(): ?string + { + return $this->postalcode; + } + + /** + * @param string|null $postalcode + */ + public function setPostalcode(?string $postalcode): void + { + $this->postalcode = $postalcode; + } + + /** + * @return string|null + */ + public function getCountry(): ?string + { + return $this->country; + } + + /** + * @param string|null $country + */ + public function setCountry(?string $country): void + { + $this->country = $country; + } +} diff --git a/src/SheetScheduler/VendorBill.php b/src/SheetScheduler/VendorBill.php new file mode 100644 index 0000000..bf1da59 --- /dev/null +++ b/src/SheetScheduler/VendorBill.php @@ -0,0 +1,217 @@ +memo; + } + + /** + * @param string|null $memo + */ + public function setMemo(?string $memo): void + { + $this->memo = $memo; + } + + /** + * @return string|null + */ + public function getApAccount(): ?string + { + return $this->apAccount; + } + + /** + * @param string|null $apAccount + */ + public function setApAccount(?string $apAccount): void + { + $this->apAccount = $apAccount; + } + + /** + * @return string|null + */ + public function getRefNumber(): ?string + { + return $this->refNumber; + } + + /** + * @param string|null $refNumber + */ + public function setRefNumber(?string $refNumber): void + { + $this->refNumber = $refNumber; + } + + /** + * @return string|null + */ + public function getTxnDate(): ?string + { + return $this->txnDate; + } + + /** + * @param string|null $txnDate + */ + public function setTxnDate(?string $txnDate): void + { + $this->txnDate = $txnDate; + } + + /** + * @return string|null + */ + public function getLine1AccountFullName(): ?string + { + return $this->line1AccountFullName; + } + + /** + * @param string|null $line1AccountFullName + */ + public function setLine1AccountFullName(?string $line1AccountFullName): void + { + $this->line1AccountFullName = $line1AccountFullName; + } + + /** + * @return string|null + */ + public function getLine1Amount(): ?string + { + return $this->line1Amount; + } + + /** + * @param string|null $line1Amount + */ + public function setLine1Amount(?string $line1Amount): void + { + $this->line1Amount = $line1Amount; + } + + /** + * @return string|null + */ + public function getLine1Memo(): ?string + { + return $this->line1Memo; + } + + /** + * @param string|null $line1Memo + */ + public function setLine1Memo(?string $line1Memo): void + { + $this->line1Memo = $line1Memo; + } + + /** + * @return string|null + */ + public function getLine2AccountFullName(): ?string + { + return $this->line2AccountFullName; + } + + /** + * @param string|null $line2AccountFullName + */ + public function setLine2AccountFullName(?string $line2AccountFullName): void + { + $this->line2AccountFullName = $line2AccountFullName; + } + + /** + * @return string|null + */ + public function getLine2Amount(): ?string + { + return $this->line2Amount; + } + + /** + * @param string|null $line2Amount + */ + public function setLine2Amount(?string $line2Amount): void + { + $this->line2Amount = $line2Amount; + } + + /** + * @return string|null + */ + public function getLine2Memo(): ?string + { + return $this->line2Memo; + } + + /** + * @param string|null $line2Memo + */ + public function setLine2Memo(?string $line2Memo): void + { + $this->line2Memo = $line2Memo; + } + + /** + * @return string|null + */ + public function getExchangeRate(): ?string + { + return $this->exchangeRate; + } + + /** + * @param string|null $exchangeRate + */ + public function setExchangeRate(?string $exchangeRate): void + { + $this->exchangeRate = $exchangeRate; + } +} diff --git a/src/TransactionsConverter.php b/src/TransactionsConverter.php new file mode 100644 index 0000000..651d6eb --- /dev/null +++ b/src/TransactionsConverter.php @@ -0,0 +1,172 @@ + 'Date', + self::FIELD_CURRENCY => 'Commodity/Currency', + self::FIELD_MEMO => 'Description', + self::FIELD_ACCOUNT => 'Full Account Name', + self::FIELD_AMOUNT => 'Amount With Sym', + ]; + + const DEFAULT_EXPENSE_ACCOUNT = 'Ask My Accountant'; + + private $csvEncoder; + private $normalizer; + private $accountRepository; + private $accountsMapping; + private $currencyMap; + + public function __construct(CsvEncoder $encoder, NormalizerInterface $normalizer, + QuickbooksAccountRepositoryInterface $accountRepository, + string $accountsMappingPath) + { + $this->csvEncoder = $encoder; + $this->normalizer = $normalizer; + $this->accountRepository = $accountRepository; + $accountsMappingJson = file_get_contents($accountsMappingPath); + Assert::string($accountsMappingJson); + $this->accountsMapping = json_decode($accountsMappingJson, true, 512, JSON_THROW_ON_ERROR); + $this->currencyMap = new CurrencyMap(); + } + + public function convertWrapper(UploadedFile $file, string $qbUsername): string + { + Assert::eq('text/plain', $file->getMimeType()); + $inputFilePath = $file->getRealPath(); + Assert::string($inputFilePath); + $inputFile = file_get_contents($inputFilePath); + Assert::string($inputFile); + $inputData = $this->csvEncoder->decode($inputFile, 'csv'); + $entities = $this->convert($inputData, $qbUsername); + $result = $this->csvEncoder->encode($entities, 'csv'); + Assert::string($result); + return $result; + } + + /** + * @param array> $inputData + * @param array $fieldsMapping + * + * @return array + */ + public function convert(array $inputData, string $qbUsername, ?array $fieldsMapping = null): array + { + $fieldsMapping = $fieldsMapping ?? self::GNUCASH_FIELDS_MAPPING; + Assert::true(count($inputData) % 2 === 0, 'The amount of rows must be even'); + + $odds = $evens = []; + foreach ($inputData as $k => $v) { + if ($k % 2 === 0) { + $evens[] = $v; + } else { + $odds[] = $v; + } + } + + $entities = []; + foreach ($evens as $i => $evenItem) { + $oddItem = $odds[$i]; + + $entity = new Transaction(); + $date = $evenItem[$fieldsMapping[self::FIELD_DATE]] ?? null; + Assert::string($date, "Line {$i}:"); + $date = \DateTime::createFromFormat('d/m/y', $date); + Assert::isInstanceOf($date, \DateTime::class, "Line {$i}:"); + $entity->setTxnDate($date->format('Y-m-d')); + +// Assert::notNull($currency = $evenItem[$mapping[self::FIELD_CURRENCY]] ?? null, "Line {$i}:"); +// Assert::eq(1, preg_match('/CURRENCY::(.+)/', $currency, $matches), "Line {$i}: Currency in wrong format"); +// [, $currency] = $this->currencyMap->findCurrency($matches[1]); +// $entity->setCurrency($currency); + + + $description = $evenItem[$fieldsMapping[self::FIELD_MEMO]] ?? null; + Assert::notNull($description, "Line {$i}:"); + $entity->setCreditMemo($description); + $entity->setDebitMemo($description); + + Assert::string($evenAccountMapped = $evenItem[$fieldsMapping[self::FIELD_ACCOUNT]] ?? null, "Line {$i}:"); + $evenAccount = $this->accountsMapping[$qbUsername][$evenAccountMapped] ?? null; + Assert::string($evenAccount, "Line {$i}: Account not mapped: {$evenAccountMapped}"); + $qbEvenAccount = $this->accountRepository->findOneByName($qbUsername, $evenAccount); + Assert::notNull($qbEvenAccount, "Line {$i}: Unable to find account: {$evenAccount}"); + Assert::eq(QuickbooksAccount::TYPE_BANK, $qbEvenAccount->getAccountType(), "Line {$i}: Type of {$evenAccount} must be bank"); + + [$evenAmount, $evenCurrency, $evenNeg] = $this->getAmount($evenItem, $fieldsMapping[self::FIELD_AMOUNT]); + [$oddAmount, $oddCurrency, $oddNeg] = $this->getAmount($oddItem, $fieldsMapping[self::FIELD_AMOUNT]); + + if ($evenNeg === $oddNeg) { + if ($oddAmount === '0.00' ) { + $oddNeg = !$evenNeg; + } else if ($evenAmount === '0.00') { + $evenNeg = !$oddNeg; + } + } + Assert::notEq($evenNeg, $oddNeg, "Line {$i}: Sign of both amount is the same"); + + Assert::string($oddAccountMapped = $oddItem[$fieldsMapping[self::FIELD_ACCOUNT]] ?? null, "Line {$i}:"); + $oddAccount = $this->accountsMapping[$qbUsername][$oddAccountMapped] + ?? ($evenNeg ? self::DEFAULT_EXPENSE_ACCOUNT : QuickbooksAccount::UNCATEGORIZED_INCOME); +// Assert::string($oddAccount, "Account not mapped: {$oddAccountMapped}"); + $qbOddAccount = $this->accountRepository->findOneByName($qbUsername, $oddAccount); + Assert::notNull($qbOddAccount, "Line {$i}: Unable to find account: {$oddAccount}"); + + $useEvenAmount = $qbOddAccount->getAccountType() !== QuickbooksAccount::TYPE_BANK; + $oddAmount = $useEvenAmount ? $evenAmount : $oddAmount; + + Assert::eq($evenCurrency, $qbEvenAccount->getCurrency(), "Line {$i}: Currencies of {$evenAccount} do not match"); + $entity->setCurrency($evenCurrency); + + + [$creditAmount, $debitAmount] = $evenNeg ? [$evenAmount, $oddAmount] : [$oddAmount, $evenAmount]; + [$creditAccount, $debitAccount] = $evenNeg ? [$evenAccount, $oddAccount] : [$oddAccount, $evenAccount]; + + $entity->setCreditAccount($creditAccount); + $entity->setCreditAmount($creditAmount); + + $entity->setDebitAccount($debitAccount); + $entity->setDebitAmount($debitAmount); + + + $entities[] = $entity; + } + + $result = $this->normalizer->normalize($entities); + Assert::isArray($result); + + return $result; + } + + /** + * @param array $item + * @return array{0: string, 1: string, 2: bool} + */ + private function getAmount(array $item, string $key): array + { + $amount = $item[$key] ?? null; + Assert::string($amount); + Assert::eq(1, preg_match('/(-*?)([^0-9.,-]+)([0-9.,]+)/', $amount, $matches), "Amount in wrong format: {$amount}"); + [, $currency] = $this->currencyMap->findCurrency($matches[2]); + + return [$matches[3], $currency, $matches[1] === '-']; + } +} diff --git a/src/Twig/TwigExtension.php b/src/Twig/TwigExtension.php new file mode 100644 index 0000000..9e2f4f7 --- /dev/null +++ b/src/Twig/TwigExtension.php @@ -0,0 +1,75 @@ +em = $em; + $this->twig = $twig; + $this->normalizer = $normalizer; + } + + public function getFunctions() + { + return [ + new TwigFunction('alerts', [$this, 'getAlerts']), + new TwigFunction('render_preview', [$this, 'renderPreview']), + ]; + } + + public function getAlerts(): string + { + $accountRepo = $this->em->getRepository(QuickbooksAccount::class); + $companyRepo = $this->em->getRepository(QuickbooksCompany::class); + $companies = $companyRepo->findBy([]); + $accountlessCompanies = array_values(array_filter($companies, static function(QuickbooksCompany $company) use ($accountRepo): bool { + return $accountRepo->findOneBy(['company' => $company]) === null; + })); + $noCompanies = count($companies) === 0; + + return $this->twig->render('helper/alerts.html.twig', [ + 'has_alerts' => $noCompanies || count($accountlessCompanies) > 0, + 'no_companies' => $noCompanies, + 'accountless_companies' => $accountlessCompanies, + ]); + } + + public function renderPreview(array $entities): string + { + $entities = $this->normalizer->normalize($entities); + Assert::isArray($entities); + $entities = $this->removeEmptyElements($entities); + return $this->twig->render('helper/render_preview.html.twig', [ + 'entities' => $entities, + ]); + } + + private function removeEmptyElements(array $haystack): array + { + foreach ($haystack as $key => $value) { + if (is_array($value)) { + $haystack[$key] = $this->removeEmptyElements($haystack[$key]); + } + if (in_array($haystack[$key], [null, ''], true)) { + unset($haystack[$key]); + } + } + + return $haystack; + } +} diff --git a/src/User/UserService.php b/src/User/UserService.php new file mode 100644 index 0000000..223a10a --- /dev/null +++ b/src/User/UserService.php @@ -0,0 +1,34 @@ +em = $em; + $this->passwordEncoder = $passwordEncoder; + } + + public function createUser(string $email, string $plainPassword, array $roles = []): User + { + $user = $this->em->getRepository(User::class)->findOneBy(['email' => $email]); + if (null === $user) { + $user = new User(); + $user->setEmail($email); + $this->em->persist($user); + } + $user->setRoles($roles); + $user->setPassword($this->passwordEncoder->encodePassword($user, $plainPassword)); + $this->em->flush(); + + return $user; + } +} diff --git a/symfony.lock b/symfony.lock new file mode 100644 index 0000000..6776ca2 --- /dev/null +++ b/symfony.lock @@ -0,0 +1,654 @@ +{ + "brick/math": { + "version": "0.8.15" + }, + "clue/stream-filter": { + "version": "v1.4.1" + }, + "consolibyte/quickbooks": { + "version": "dev-qbxmlops130" + }, + "craue/formflow-bundle": { + "version": "3.3.2" + }, + "doctrine/annotations": { + "version": "1.0", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "master", + "version": "1.0", + "ref": "a2759dd6123694c8d901d0ec80006e044c2e6457" + }, + "files": [ + "config/routes/annotations.yaml" + ] + }, + "doctrine/cache": { + "version": "1.10.0" + }, + "doctrine/collections": { + "version": "1.6.4" + }, + "doctrine/common": { + "version": "2.13.0" + }, + "doctrine/data-fixtures": { + "version": "1.4.2" + }, + "doctrine/dbal": { + "version": "2.10.2" + }, + "doctrine/deprecations": { + "version": "v0.5.3" + }, + "doctrine/doctrine-bundle": { + "version": "1.12", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "master", + "version": "1.12", + "ref": "b11d5292f574a9cd092d506c899d05c79cf4d613" + }, + "files": [ + "config/packages/doctrine.yaml", + "config/packages/prod/doctrine.yaml", + "src/Entity/.gitignore", + "src/Repository/.gitignore" + ] + }, + "doctrine/doctrine-cache-bundle": { + "version": "1.4.0" + }, + "doctrine/doctrine-fixtures-bundle": { + "version": "3.0", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "master", + "version": "3.0", + "ref": "fc52d86631a6dfd9fdf3381d0b7e3df2069e51b3" + }, + "files": [ + "src/DataFixtures/AppFixtures.php" + ] + }, + "doctrine/doctrine-migrations-bundle": { + "version": "1.2", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "master", + "version": "1.2", + "ref": "c1431086fec31f17fbcfe6d6d7e92059458facc1" + }, + "files": [ + "config/packages/doctrine_migrations.yaml", + "src/Migrations/.gitignore" + ] + }, + "doctrine/event-manager": { + "version": "1.1.0" + }, + "doctrine/inflector": { + "version": "1.4.1" + }, + "doctrine/instantiator": { + "version": "1.3.0" + }, + "doctrine/lexer": { + "version": "1.2.0" + }, + "doctrine/migrations": { + "version": "2.2.1" + }, + "doctrine/orm": { + "version": "v2.7.2" + }, + "doctrine/persistence": { + "version": "1.3.7" + }, + "doctrine/reflection": { + "version": "1.2.1" + }, + "easycorp/easyadmin-bundle": { + "version": "2.0", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "master", + "version": "2.0", + "ref": "0a51040a43c6f2172a3a0135cd15daf2043905d7" + }, + "files": [ + "config/packages/easy_admin.yaml", + "config/routes/easy_admin.yaml" + ] + }, + "egulias/email-validator": { + "version": "2.1.17" + }, + "ezyang/htmlpurifier": { + "version": "v4.13.0" + }, + "florianv/exchanger": { + "version": "2.4.0" + }, + "guzzlehttp/guzzle": { + "version": "6.5.3" + }, + "guzzlehttp/promises": { + "version": "v1.3.1" + }, + "guzzlehttp/psr7": { + "version": "1.6.1" + }, + "jdorn/sql-formatter": { + "version": "v1.2.17" + }, + "laminas/laminas-code": { + "version": "3.4.1" + }, + "laminas/laminas-eventmanager": { + "version": "3.2.1" + }, + "laminas/laminas-zendframework-bridge": { + "version": "1.0.3" + }, + "league/flysystem": { + "version": "1.0.68" + }, + "league/flysystem-webdav": { + "version": "1.0.9" + }, + "league/mime-type-detection": { + "version": "1.7.0" + }, + "maennchen/zipstream-php": { + "version": "2.1.0" + }, + "markbaker/complex": { + "version": "1.4.8" + }, + "markbaker/matrix": { + "version": "1.2.0" + }, + "monolog/monolog": { + "version": "1.25.3" + }, + "myclabs/php-enum": { + "version": "1.8.0" + }, + "nikic/php-parser": { + "version": "v4.4.0" + }, + "nyholm/psr7": { + "version": "1.0", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "master", + "version": "1.0", + "ref": "7c0a9352a57376f04f5444e74565102c3a23d0c7" + }, + "files": [ + "config/packages/nyholm_psr7.yaml" + ] + }, + "ocramius/package-versions": { + "version": "1.8.0" + }, + "ocramius/proxy-manager": { + "version": "2.8.0" + }, + "oneup/flysystem-bundle": { + "version": "3.0", + "recipe": { + "repo": "github.com/symfony/recipes-contrib", + "branch": "master", + "version": "3.0", + "ref": "0eb87bba411c227da027fe5f7c1dc7954b02f242" + }, + "files": [ + "config/packages/oneup_flysystem.yaml" + ] + }, + "pagerfanta/pagerfanta": { + "version": "v2.1.3" + }, + "php": { + "version": "7.4" + }, + "php-http/discovery": { + "version": "1.7.4" + }, + "php-http/guzzle6-adapter": { + "version": "v2.0.1" + }, + "php-http/httplug": { + "version": "2.1.0" + }, + "php-http/message": { + "version": "1.8.0" + }, + "php-http/message-factory": { + "version": "v1.0.2" + }, + "php-http/promise": { + "version": "v1.0.0" + }, + "phpdocumentor/reflection-common": { + "version": "2.1.0" + }, + "phpdocumentor/reflection-docblock": { + "version": "5.1.0" + }, + "phpdocumentor/type-resolver": { + "version": "1.1.0" + }, + "phpoffice/phpspreadsheet": { + "version": "1.12.0" + }, + "phpstan/phpstan": { + "version": "0.12.25" + }, + "phpstan/phpstan-deprecation-rules": { + "version": "0.12.2" + }, + "phpstan/phpstan-doctrine": { + "version": "0.12.13" + }, + "phpstan/phpstan-phpunit": { + "version": "0.12.8" + }, + "phpstan/phpstan-strict-rules": { + "version": "0.12.2" + }, + "phpstan/phpstan-symfony": { + "version": "0.12.6" + }, + "phpstan/phpstan-webmozart-assert": { + "version": "0.12.4" + }, + "psr/cache": { + "version": "1.0.1" + }, + "psr/container": { + "version": "1.0.0" + }, + "psr/http-client": { + "version": "1.0.0" + }, + "psr/http-factory": { + "version": "1.0.1" + }, + "psr/http-message": { + "version": "1.0.1" + }, + "psr/log": { + "version": "1.1.3" + }, + "psr/simple-cache": { + "version": "1.0.1" + }, + "ralouphie/getallheaders": { + "version": "3.0.3" + }, + "ramsey/collection": { + "version": "1.0.1" + }, + "ramsey/uuid": { + "version": "4.0.1" + }, + "sabre/dav": { + "version": "4.1.0" + }, + "sabre/event": { + "version": "5.1.0" + }, + "sabre/http": { + "version": "5.1.0" + }, + "sabre/uri": { + "version": "2.2.0" + }, + "sabre/vobject": { + "version": "4.3.0" + }, + "sabre/xml": { + "version": "2.2.1" + }, + "symfony/amazon-mailer": { + "version": "4.4", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "master", + "version": "4.4", + "ref": "9648db3ecae5c8a6b1a5f74715d3907124348815" + } + }, + "symfony/asset": { + "version": "v4.4.8" + }, + "symfony/browser-kit": { + "version": "v4.4.8" + }, + "symfony/cache": { + "version": "v4.4.8" + }, + "symfony/cache-contracts": { + "version": "v2.0.1" + }, + "symfony/config": { + "version": "v4.4.8" + }, + "symfony/console": { + "version": "4.4", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "master", + "version": "4.4", + "ref": "ea8c0eda34fda57e7d5cd8cbd889e2a387e3472c" + }, + "files": [ + "bin/console", + "config/bootstrap.php" + ] + }, + "symfony/css-selector": { + "version": "v4.4.8" + }, + "symfony/debug": { + "version": "v4.4.8" + }, + "symfony/dependency-injection": { + "version": "v4.4.8" + }, + "symfony/deprecation-contracts": { + "version": "v2.4.0" + }, + "symfony/doctrine-bridge": { + "version": "v4.4.8" + }, + "symfony/dom-crawler": { + "version": "v4.4.8" + }, + "symfony/dotenv": { + "version": "v4.4.8" + }, + "symfony/error-handler": { + "version": "v4.4.8" + }, + "symfony/event-dispatcher": { + "version": "v4.4.8" + }, + "symfony/event-dispatcher-contracts": { + "version": "v1.1.7" + }, + "symfony/expression-language": { + "version": "v4.4.8" + }, + "symfony/filesystem": { + "version": "v4.4.8" + }, + "symfony/finder": { + "version": "v4.4.8" + }, + "symfony/flex": { + "version": "1.0", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "master", + "version": "1.0", + "ref": "c0eeb50665f0f77226616b6038a9b06c03752d8e" + }, + "files": [ + ".env" + ] + }, + "symfony/form": { + "version": "v4.4.8" + }, + "symfony/framework-bundle": { + "version": "4.4", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "master", + "version": "4.4", + "ref": "36d3075b2b8e0c4de0e82356a86e4c4a4eb6681b" + }, + "files": [ + "config/bootstrap.php", + "config/packages/cache.yaml", + "config/packages/framework.yaml", + "config/packages/test/framework.yaml", + "config/routes/dev/framework.yaml", + "config/services.yaml", + "public/index.php", + "src/Controller/.gitignore", + "src/Kernel.php" + ] + }, + "symfony/http-client": { + "version": "v4.4.8" + }, + "symfony/http-client-contracts": { + "version": "v2.1.1" + }, + "symfony/http-foundation": { + "version": "v4.4.8" + }, + "symfony/http-kernel": { + "version": "v4.4.8" + }, + "symfony/inflector": { + "version": "v4.4.8" + }, + "symfony/intl": { + "version": "v4.4.8" + }, + "symfony/mailer": { + "version": "4.3", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "master", + "version": "4.3", + "ref": "15658c2a0176cda2e7dba66276a2030b52bd81b2" + }, + "files": [ + "config/packages/mailer.yaml" + ] + }, + "symfony/maker-bundle": { + "version": "1.0", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "master", + "version": "1.0", + "ref": "fadbfe33303a76e25cb63401050439aa9b1a9c7f" + } + }, + "symfony/mime": { + "version": "v4.4.8" + }, + "symfony/monolog-bridge": { + "version": "v4.4.8" + }, + "symfony/monolog-bundle": { + "version": "3.3", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "master", + "version": "3.3", + "ref": "a89f4cd8a232563707418eea6c2da36acd36a917" + }, + "files": [ + "config/packages/dev/monolog.yaml", + "config/packages/prod/monolog.yaml", + "config/packages/test/monolog.yaml" + ] + }, + "symfony/options-resolver": { + "version": "v4.4.8" + }, + "symfony/phpunit-bridge": { + "version": "4.3", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "master", + "version": "4.3", + "ref": "6d0e35f749d5f4bfe1f011762875275cd3f9874f" + }, + "files": [ + ".env.test", + "bin/phpunit", + "phpunit.xml.dist", + "tests/bootstrap.php" + ] + }, + "symfony/polyfill-intl-icu": { + "version": "v1.17.0" + }, + "symfony/polyfill-intl-idn": { + "version": "v1.17.0" + }, + "symfony/polyfill-intl-normalizer": { + "version": "v1.22.1" + }, + "symfony/polyfill-mbstring": { + "version": "v1.17.0" + }, + "symfony/polyfill-php72": { + "version": "v1.17.0" + }, + "symfony/polyfill-php73": { + "version": "v1.17.0" + }, + "symfony/polyfill-php80": { + "version": "v1.22.1" + }, + "symfony/property-access": { + "version": "v4.4.8" + }, + "symfony/property-info": { + "version": "v4.4.8" + }, + "symfony/routing": { + "version": "4.2", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "master", + "version": "4.2", + "ref": "683dcb08707ba8d41b7e34adb0344bfd68d248a7" + }, + "files": [ + "config/packages/prod/routing.yaml", + "config/packages/routing.yaml", + "config/routes.yaml" + ] + }, + "symfony/security": { + "version": "v4.4.8" + }, + "symfony/security-bundle": { + "version": "4.4", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "master", + "version": "4.4", + "ref": "7b4408dc203049666fe23fabed23cbadc6d8440f" + }, + "files": [ + "config/packages/security.yaml" + ] + }, + "symfony/serializer": { + "version": "v4.4.8" + }, + "symfony/serializer-pack": { + "version": "v1.0.3" + }, + "symfony/service-contracts": { + "version": "v2.0.1" + }, + "symfony/stopwatch": { + "version": "v4.4.8" + }, + "symfony/test-pack": { + "version": "v1.0.6" + }, + "symfony/translation": { + "version": "3.3", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "master", + "version": "3.3", + "ref": "2ad9d2545bce8ca1a863e50e92141f0b9d87ffcd" + }, + "files": [ + "config/packages/translation.yaml", + "translations/.gitignore" + ] + }, + "symfony/translation-contracts": { + "version": "v2.0.1" + }, + "symfony/twig-bridge": { + "version": "v4.4.8" + }, + "symfony/twig-bundle": { + "version": "4.4", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "master", + "version": "4.4", + "ref": "15a41bbd66a1323d09824a189b485c126bbefa51" + }, + "files": [ + "config/packages/test/twig.yaml", + "config/packages/twig.yaml", + "templates/base.html.twig" + ] + }, + "symfony/validator": { + "version": "4.3", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "master", + "version": "4.3", + "ref": "d902da3e4952f18d3bf05aab29512eb61cabd869" + }, + "files": [ + "config/packages/test/validator.yaml", + "config/packages/validator.yaml" + ] + }, + "symfony/var-dumper": { + "version": "v4.4.8" + }, + "symfony/var-exporter": { + "version": "v4.4.8" + }, + "symfony/yaml": { + "version": "v4.4.8" + }, + "symfonycasts/reset-password-bundle": { + "version": "1.0", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "master", + "version": "1.0", + "ref": "97c1627c0384534997ae1047b93be517ca16de43" + }, + "files": [ + "config/packages/reset_password.yaml" + ] + }, + "twig/twig": { + "version": "v3.0.3" + }, + "webmozart/assert": { + "version": "1.8.0" + }, + "zendframework/zend-code": { + "version": "3.3.1" + }, + "zendframework/zend-eventmanager": { + "version": "3.2.1" + } +} diff --git a/templates/admin.html.twig b/templates/admin.html.twig new file mode 100644 index 0000000..9f86dd4 --- /dev/null +++ b/templates/admin.html.twig @@ -0,0 +1,25 @@ +{% extends '@!EasyAdmin/default/layout.html.twig' %} + +{% block flash_messages %} + {{ alerts()|raw }} +{% endblock %} + +{% block body_custom_javascript %} + {{ parent() }} + + + + +{% endblock %} diff --git a/templates/base.html.twig b/templates/base.html.twig new file mode 100644 index 0000000..1e7f8c2 --- /dev/null +++ b/templates/base.html.twig @@ -0,0 +1,73 @@ +{#https://bootstrapious.com/tutorial/sidebar/index.html#} + + + + + + + + + + + {% block title %}Welcome!{% endblock %} + + + + + + + + + + {% block stylesheets %}{% endblock %} + + + + + +{% block body %} +{% block wrapper %} +
+ + + + +
+ {% for type, messages in app.session.flashBag.all %} + {% for message in messages %} + {%if type == 'error'%} {% set type = 'danger' %} {%endif%} +
+ {{ message|raw }} +
+ {% endfor %} + {% endfor %} + + {% block content %}{% endblock %} + +
+
+{% endblock %} +{% endblock %} + + + + + + + + + +{% block javascripts %}{% endblock %} + + + + diff --git a/templates/documentation/index.html.twig b/templates/documentation/index.html.twig new file mode 100644 index 0000000..cc9f61b --- /dev/null +++ b/templates/documentation/index.html.twig @@ -0,0 +1,60 @@ +{% extends 'admin.html.twig' %} + +{% block content_title %}Documentation{% endblock %} + +{% block content %} +
+
+
+
+

Connect EasyQuickImport to QuickBooks Desktop

+
+ +
+
+
+

How to Backup and Restore in QuickBooks Desktop

+
+ +
+
+
+

Import invoices from Excel into QuickBooks Desktop

+
+ +
+
+
+

Import bills and vendors from Excel into QuickBooks Desktop

+
+ +
+
+
+

Import transactions from Excel into QuickBooks Desktop

+
+ +
+
+
+

Import multicurrency transactions from Excel into QuickBooks + Desktop

+
+ +
+
+
+
+ {% endblock %} diff --git a/templates/form/bootstrap_4.html.twig b/templates/form/bootstrap_4.html.twig new file mode 100644 index 0000000..b5b248b --- /dev/null +++ b/templates/form/bootstrap_4.html.twig @@ -0,0 +1,64 @@ +{% use 'bootstrap_4_layout.html.twig' %} + +{# copied from vendor/easycorp/easyadmin-bundle/src/Resources/views/form/bootstrap_4.html.twig #} +{% block form_row -%} + {% set _field_type = easyadmin.field.fieldType|default('default') %} +
+ {{- form_label(form) -}} +
+ {% set has_prepend_html = easyadmin.field.prepend_html|default(null) is not null %} + {% set has_append_html = easyadmin.field.append_html|default(null) is not null %} + {% set has_input_groups = has_prepend_html or has_append_html %} + + {% if has_input_groups %}
{% endif %} + {% if has_prepend_html %} +
+ {{ easyadmin.field.prepend_html|raw }} +
+ {% endif %} + + {{- form_widget(form) -}} + + {% if has_append_html %} +
+ {{ easyadmin.field.append_html|raw }} +
+ {% endif %} + {% if has_input_groups %}
{% endif %} + + {% if _field_type in ['datetime', 'datetime_immutable', 'date', 'date_immutable', 'dateinterval', 'time', 'time_immutable', 'birthday'] and easyadmin.field.nullable|default(false) %} +
+ +
+ {% endif %} + + {% if easyadmin.field.help|default(form.vars.help) != '' %} + {{ easyadmin.field.help|default(form.vars.help)|trans(domain = form.vars.translation_domain)|raw }} + {% endif %} + + {{- form_errors(form) -}} +
+
+{%- endblock form_row %} + +{# copied from vendor/easycorp/easyadmin-bundle/src/Resources/views/form/bootstrap_4.html.twig #} +{% block file_widget -%} + {% if vich|default(false) %} + {%- set type = type|default('file') -%} + {{- block('form_widget_simple') -}} + {% else %} + {{- parent() -}} + + + {% endif %} +{%- endblock %} diff --git a/templates/helper/alerts.html.twig b/templates/helper/alerts.html.twig new file mode 100644 index 0000000..9cf6928 --- /dev/null +++ b/templates/helper/alerts.html.twig @@ -0,0 +1,29 @@ +{% if has_alerts %} +
+ {% if no_companies %} +
+ You don't have companies! + Go to companies + and create one. Then you will be able to download the QWC file! +
+ {% endif %} + + {% for company in accountless_companies %} +
+ {{ company.companyName|default(company.qbUsername) }} has no accounts. + Synchronize chart of accounts here + and you will be able to import transactions! +
+ {% endfor %} + + + {% for type, messages in app.session.flashBag.all %} + {% for message in messages %} + {%if type == 'error'%} {% set type = 'danger' %} {%endif%} +
+ {{ message|raw }} +
+ {% endfor %} + {% endfor %} +
+{% endif %} diff --git a/templates/helper/render_preview.html.twig b/templates/helper/render_preview.html.twig new file mode 100644 index 0000000..dc7c341 --- /dev/null +++ b/templates/helper/render_preview.html.twig @@ -0,0 +1,3 @@ +
+
{{ entities|yaml_encode(3) }}
+
diff --git a/templates/import/create.html.twig b/templates/import/create.html.twig new file mode 100644 index 0000000..c316ec3 --- /dev/null +++ b/templates/import/create.html.twig @@ -0,0 +1,102 @@ +{% extends 'admin.html.twig' %} + +{% block content_title %}Create import{% endblock %} + +{% block head_custom_stylesheets %} + {{ parent() }} + + +{% endblock %} + +{% block content %} +
+
+
+ {% include '@CraueFormFlow/FormFlow/stepList.html.twig' %} +
+
+
+ {{ form_start(form) }} + {{ form_errors(form) }} + + {% for error in errors %} + + {% endfor %} + + {% if flow.getCurrentStepLabel() == 'import_wizard_step.confirmation' %} +
+ Preview
+
+ {{ render_preview(entities)|raw }} + {% endif %} + + {{ form_rest(form) }} + +
+ {% include '@CraueFormFlow/FormFlow/buttons.html.twig' with { + craue_formflow_button_label_next: 'Next', + craue_formflow_button_label_back: 'Back', + craue_formflow_button_label_finish: 'Schedule import', + craue_formflow_button_class_last: 'btn btn-primary', + craue_formflow_button_class_back: 'btn', + craue_formflow_button_render_reset: 0, + } %} +
+ {{ form_end(form) }} +
+
+{% endblock %} + + +{% block body_custom_javascript %} + {{ parent() }} + +{% endblock %} diff --git a/templates/import/scheduled.html.twig b/templates/import/scheduled.html.twig new file mode 100644 index 0000000..dbd2055 --- /dev/null +++ b/templates/import/scheduled.html.twig @@ -0,0 +1,9 @@ +{% extends 'admin.html.twig' %} + +{% block content_title %}Import scheduled{% endblock %} + +{% block content %} +
+ Now go to quickbooks and run WebConnector +
+{% endblock %} diff --git a/templates/registration/register.html.twig b/templates/registration/register.html.twig new file mode 100644 index 0000000..ff8c111 --- /dev/null +++ b/templates/registration/register.html.twig @@ -0,0 +1,33 @@ +{% extends 'base.html.twig' %} + +{% block title %}Register{% endblock %} + +{% block body %} +
+
+
+
+ {% for flashError in app.flashes('verify_email_error') %} + + {% endfor %} + +

Register

+ + {{ form_start(registrationForm) }} + {{ form_row(registrationForm.email) }} + {{ form_row(registrationForm.plainPassword, { + label: 'Password' + }) }} + + + +
+
+ Login +
+ {{ form_end(registrationForm) }} +
+
+
+
+{% endblock %} diff --git a/templates/reset_password/check_email.html.twig b/templates/reset_password/check_email.html.twig new file mode 100644 index 0000000..35f1153 --- /dev/null +++ b/templates/reset_password/check_email.html.twig @@ -0,0 +1,8 @@ +{% extends 'base.html.twig' %} + +{% block title %}Password Reset Email Sent{% endblock %} + +{% block body %} +

An email has been sent that contains a link that you can click to reset your password. This link will expire in {{ tokenLifetime|date('g') }} hour(s).

+

If you don't receive an email please check your spam folder or try again.

+{% endblock %} diff --git a/templates/reset_password/email.html.twig b/templates/reset_password/email.html.twig new file mode 100644 index 0000000..d02ca97 --- /dev/null +++ b/templates/reset_password/email.html.twig @@ -0,0 +1,11 @@ +

Hi!

+ +

+ To reset your password, please visit + here + This link will expire in {{ tokenLifetime|date('g') }} hour(s).. +

+ +

+ Cheers! +

\ No newline at end of file diff --git a/templates/reset_password/request.html.twig b/templates/reset_password/request.html.twig new file mode 100644 index 0000000..b5dff14 --- /dev/null +++ b/templates/reset_password/request.html.twig @@ -0,0 +1,30 @@ +{% extends 'base.html.twig' %} + +{% block title %}Reset your password{% endblock %} + +{% block body %} +
+
+
+
+ {% for flashError in app.flashes('reset_password_error') %} + + {% endfor %} +

Reset your password

+ + {{ form_start(requestForm) }} + {{ form_row(requestForm.email) }} +
+ + Enter your email address and we we will send you a + link to reset your password. + +
+ + + {{ form_end(requestForm) }} +
+
+
+
+{% endblock %} diff --git a/templates/reset_password/reset.html.twig b/templates/reset_password/reset.html.twig new file mode 100644 index 0000000..8ffa72b --- /dev/null +++ b/templates/reset_password/reset.html.twig @@ -0,0 +1,20 @@ +{% extends 'base.html.twig' %} + +{% block title %}Reset your password{% endblock %} + +{% block body %} +
+
+
+
+

Reset your password

+ + {{ form_start(resetForm) }} + {{ form_row(resetForm.plainPassword) }} + + {{ form_end(resetForm) }} +
+
+
+
+{% endblock %} diff --git a/templates/scheduling/accounts.html.twig b/templates/scheduling/accounts.html.twig new file mode 100644 index 0000000..e66978a --- /dev/null +++ b/templates/scheduling/accounts.html.twig @@ -0,0 +1,28 @@ +{% extends 'admin.html.twig' %} + +{% form_theme schedule_accounts_update_form with easyadmin_config('design.form_theme') %} + +{% block content_title %}Scheduling{% endblock %} + +{% block content %} +
+
+
+
+
+
+

Schedule accounts update

+
+ +
+
+
+
+ {{ form(schedule_accounts_update_form) }} +
+
+
+ +
+ +{% endblock %} diff --git a/templates/scheduling/schedule.html.twig b/templates/scheduling/schedule.html.twig new file mode 100644 index 0000000..e0ad5e1 --- /dev/null +++ b/templates/scheduling/schedule.html.twig @@ -0,0 +1,76 @@ +{% extends 'admin.html.twig' %} + +{% form_theme schedule_form with easyadmin_config('design.form_theme') %} +{% form_theme sample_form with easyadmin_config('design.form_theme') %} +{% form_theme truncate_form with easyadmin_config('design.form_theme') %} +{% form_theme converter_form with easyadmin_config('design.form_theme') %} + +{% block content_title %}Scheduling{% endblock %} + +{% block content %} +
+ +
+
+
+
+
+

Schedule a sheet

+
+ +
+
+
+
+ {{ form(schedule_form) }} +
+
+ +
+
+
+
+

Download a sample sheet

+
+ +
+
+
+
+ {{ form(sample_form) }} +
+
+ +
+
+
+
+

Convert gnucash to csv

+
+ +
+
+
+
+ {{ form(converter_form) }} +
+
+ +
+
+
+
+

Truncate queue

+
+ +
+
+
+
+ {{ form(truncate_form) }} +
+
+
+
+ +{% endblock %} diff --git a/templates/security/login.html.twig b/templates/security/login.html.twig new file mode 100644 index 0000000..f8614bd --- /dev/null +++ b/templates/security/login.html.twig @@ -0,0 +1,58 @@ +{% extends 'base.html.twig' %} + +{% block title %}Log in!{% endblock %} + +{% block body %} + +
+
+
+
+
+ {% if error %} +
{{ error.messageKey|trans(error.messageData, 'security') }}
+ {% endif %} + + {% if app.user %} +
+ You are logged in as {{ app.user.username }}, Logout +
+ {% endif %} + +

Please sign in

+ + + + + + + + {# + Uncomment this section and add a remember_me option below your firewall to activate remember me functionality. + See https://symfony.com/doc/current/security/remember_me.html + +
+ +
+ #} +
+ +
+ +
+
+
+
+
+ +{% endblock %} diff --git a/tests/.gitignore b/tests/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/tests/Functional/FakeCurrencyExchanger.php b/tests/Functional/FakeCurrencyExchanger.php new file mode 100644 index 0000000..7b5f6a9 --- /dev/null +++ b/tests/Functional/FakeCurrencyExchanger.php @@ -0,0 +1,13 @@ +client = self::createClient(); + + /** @var UserService $userService */ + $userService = static::$container->get(UserService::class); + $this->user = $userService->createUser('admin@localhost', 'pass', ['ROLE_ADMIN']); + + ob_flush(); + } + public function tearDown(): void + { + ob_flush(); + } + + public function testExchange(): void + { + /** @var QuickbooksServer $server */ + $server = static::$container->get(QuickbooksServer::class); + $server->truncateQueue(); + $this->givenCompany($this->user, self::USERNAME, self::PASSWORD); + + $customerAddXml = $this->getFixture('add_customer.xml'); + $server->schedule(self::USERNAME,QUICKBOOKS_ADD_CUSTOMER, '100', $customerAddXml); + + + #authentication + $request = $this->getFixture('authenticate_request.xml'); + $this->patchServer('POST', '/qbwc'); + $this->client->request('POST', '/qbwc', [], [], [], $request); + + $response = $this->client->getResponse(); + self::assertSame(200, $response->getStatusCode()); + self::assertSame(1, preg_match('|(.+)|', $response->getContent(), $matches)); + $ticketId = $matches[1]; + + $expectedResponse = $this->getFixture('authenticate_response.xml', ['{ticketId}' => $ticketId]); + self::assertSame($expectedResponse, $response->getContent()); + + #task request + $request = $this->getFixture('task_request.xml', ['{ticketId}' => $ticketId]); + $this->patchServer('POST', '/qbwc'); + $this->client->request('POST', '/qbwc', [], [], [], $request); + + $response = $this->client->getResponse(); + self::assertSame(200, $response->getStatusCode()); + + $expectedResponse = $this->getFixture('task_add_customer_response.xml'); + self::assertSame($expectedResponse, $response->getContent()); + + #task request added + $request = $this->getFixture('task_add_customer_added_request.xml', ['{ticketId}' => $ticketId]); + $this->patchServer('POST', '/qbwc'); + $this->client->request('POST', '/qbwc', [], [], [], $request); + + $response = $this->client->getResponse(); + self::assertSame(200, $response->getStatusCode()); + + $expectedResponse = $this->getFixture('task_add_customer_added_received_response.xml'); + self::assertSame($expectedResponse, $response->getContent()); + + #bye + $request = $this->getFixture('bye_request.xml', ['{ticketId}' => $ticketId]); + $this->patchServer('POST', '/qbwc'); + $this->client->request('POST', '/qbwc', [], [], [], $request); + + $response = $this->client->getResponse(); + self::assertSame(200, $response->getStatusCode()); + + $expectedResponse = $this->getFixture('bye_response.xml'); + self::assertSame($expectedResponse, $response->getContent()); + } + + public function testSchedule(): void + { + self::bootKernel(); + + /** @var QuickbooksServer $server */ + $server = static::$container->get(QuickbooksServer::class); + $server->truncateQueue(); + + /** @var EntityManagerInterface $em */ + $em = static::$container->get(EntityManagerInterface::class); + $repo = $em->getRepository(QuickbooksQueue::class); + self::assertEmpty($repo->findAll()); + + #WHEN + $server->schedule(self::USERNAME,QUICKBOOKS_ADD_CUSTOMER, '100', '', ['a' => 'b']); + + #THEN + $items = $repo->findAll(); + self::assertCount(1, $items); + [$item] = $items; + + self::assertSame(QUICKBOOKS_ADD_CUSTOMER, $item->getQbAction()); + self::assertSame('100', $item->getIdent()); + self::assertSame('', $item->getQbxml()); + self::assertSame(['a' => 'b'], $item->getExtraData()); + } + + public function testDownloadQBWCConfig(): void + { + $this->givenCompany($this->user, self::USERNAME, self::PASSWORD); + $this->logIn($this->user); + + $this->client->request('GET', '/download-qbwc-config?id='.self::USERNAME); + $response = $this->client->getResponse(); + self::assertSame(200, $response->getStatusCode()); + self::assertStringStartsWith('', $response->getContent()); + self::assertSame('text/xml; charset=UTF-8', $response->headers->get('content-type')); + self::assertStringStartsWith('attachment; filename=', $response->headers->get('content-disposition')); + } + + private function givenCompany(User $user, string $username, ?string $password): QuickbooksCompany + { + /** @var EntityManagerInterface $em */ + $em = static::$container->get(EntityManagerInterface::class); + $c = $em->getRepository(QuickbooksCompany::class)->findOneBy(['qbUsername' => $username]); + if (null === $c) { + $c = new QuickbooksCompany($username); + $em->persist($c); + } + $c->setCompanyName($username); + $c->setQbPassword($password); + $c->setUser($user); + $em->flush(); + + return $c; + } + + private function logIn(User $user) + { + $session = self::$container->get('session'); + + $firewallName = 'main'; + // if you don't define multiple connected firewalls, the context defaults to the firewall name + // See https://symfony.com/doc/current/reference/configuration/security.html#firewall-context + $firewallContext = 'main'; + + // you may need to use a different token class depending on your application. + // for example, when using Guard authentication you must instantiate PostAuthenticationGuardToken + $token = new UsernamePasswordToken($user, null, $firewallName, $user->getRoles()); + $session->set('_security_'.$firewallContext, serialize($token)); + $session->save(); + + $cookie = new Cookie($session->getName(), $session->getId()); + $this->client->getCookieJar()->set($cookie); + } + + public function testInfo(): void + { + $this->patchServer('GET', '/qbwc'); + $this->client->request('GET', '/qbwc'); + $response = $this->client->getResponse(); + self::assertSame(200, $response->getStatusCode()); + self::assertSame('Easy Quick Import', $response->getContent()); + self::assertSame('text/plain; charset=UTF-8', $response->headers->get('content-type')); + } + + public function xtestWSDL(): void //exits so not testable + { + $this->patchServer('GET', '/qbwc?wsdl'); + $this->client->request('GET', '/qbwc?wsdl'); + $response = $this->client->getResponse(); + self::assertSame(200, $response->getStatusCode()); + self::assertStringStartsWith('', $response->getContent()); + self::assertSame('text/xml; charset=UTF-8', $response->headers->get('content-type')); + } + + public function testNoDataExchangeRequired(): void + { + #server version exchange + $request = $this->getFixture('server_version_request.xml'); + $this->patchServer('POST', '/qbwc'); + $this->client->request('POST', '/qbwc', [], [], [], $request); + + $response = $this->client->getResponse(); + self::assertSame(200, $response->getStatusCode()); + self::assertSame($this->getFixture('server_version_response.xml'), $response->getContent()); + + #client version exchange + $request = $this->getFixture('client_version_request.xml'); + $this->patchServer('POST', '/qbwc'); + $this->client->request('POST', '/qbwc', [], [], [], $request); + + $response = $this->client->getResponse(); + self::assertSame(200, $response->getStatusCode()); + self::assertSame($this->getFixture('client_version_response.xml'), $response->getContent()); + + #authentication + $request = $this->getFixture('authenticate_request.xml'); + $this->patchServer('POST', '/qbwc'); + $this->client->request('POST', '/qbwc', [], [], [], $request); + + $response = $this->client->getResponse(); + self::assertSame(200, $response->getStatusCode()); + self::assertNotFalse(strpos($response->getContent(), ''), $response->getContent()); + } + + private function getFixture(string $name, ?array $replacements = null): string + { + $content = file_get_contents(__DIR__ . '/fixtures/' . $name); + + if ($replacements !== null) { + foreach ($replacements as $search => $replacement) { + $content = str_replace($search, $replacement, $content); + } + } + return $content; + } + + private function patchServer(string $method, string $uri): void + { + $_SERVER['REQUEST_METHOD'] = $method; + $_SERVER['REQUEST_URI'] = $uri; + $_SERVER['REMOTE_ADDR'] = '127.0.0.1'; + $query = parse_url($uri, PHP_URL_QUERY); + if (null !== $query) { + parse_str($query, $_GET); + } + } +} diff --git a/tests/Functional/SchedulerTest.php b/tests/Functional/SchedulerTest.php new file mode 100644 index 0000000..67315b9 --- /dev/null +++ b/tests/Functional/SchedulerTest.php @@ -0,0 +1,150 @@ +get(EntityManagerInterface::class); + if (null === $company = $em->find(QuickbooksCompany::class, self::TEST_USER)) { + $company = new QuickbooksCompany(self::TEST_USER); + $em->persist($company); + } + $company->setMultiCurrencyEnabled(true); + $em->flush(); + $this->company = $company; + + $accountRepo = static::$container->get(QuickbooksAccountRepository::class); + $accountRepo->deleteAll($this->company); + + /** @var UserService $userService */ + $userService = static::$container->get(UserService::class); + $this->user = $userService->createUser('admin@localhost', 'pass', ['ROLE_ADMIN']); + + $this->scheduler = static::$container->get(SheetScheduler::class); + } + + public function provider(): array + { + return [ + ['vendor.csv', SheetScheduler::TYPE_VENDOR, 'vendor_converted.xml'], + ['bill.csv', SheetScheduler::TYPE_VENDOR_BILL, 'bill_converted.xml'], + ['customer.csv', SheetScheduler::TYPE_CUSTOMER, 'customer_converted.xml'], + ['invoice.csv', SheetScheduler::TYPE_CUSTOMER_INVOICE, 'invoice_converted.xml'], + ['transaction.csv', SheetScheduler::TYPE_TRANSACTION, 'transaction_converted.xml'], + ]; + } + + /** + * @dataProvider provider + */ + public function testExchange(string $inputFile, string $type, string $expectedOuputFile): void + { + /** @var SheetScheduler $scheduler */ + $scheduler = static::$container->get(SheetScheduler::class); + $expected = file_get_contents(__DIR__.'/fixtures/'.$expectedOuputFile); + $actual = $scheduler->dryRun($this->company, $type, $scheduler->copyToLocal($inputFile)); + self::assertSame($expected, $actual); + } + + public function testAccountRepo(): void + { + /** @var EntityManagerInterface $em */ + $em = static::$container->get(EntityManagerInterface::class); + $accountRepo = static::$container->get(QuickbooksAccountRepository::class); + + $account = new QuickbooksAccount(); + $account->setCompany($this->company); + $account->setFullName('Bank USD'); + $account->setAccountType(QuickbooksAccount::TYPE_BANK); + $account->setCurrency('US Dollar'); + $account->setUser($this->user); + $em->persist($account); + $em->flush(); + + $currency = $accountRepo->getCurrency(self::TEST_USER, 'Bank USD', QuickbooksAccount::TYPE_BANK); + self::assertSame('US Dollar', $currency); + + $currency = $accountRepo->getCurrency(self::TEST_USER, 'Bank USD'); + self::assertSame('US Dollar', $currency); + + $currency = $accountRepo->getCurrency(self::TEST_USER, 'Bank', QuickbooksAccount::TYPE_BANK); + self::assertNull($currency); + + $currency = $accountRepo->getCurrency('', 'Bank USD', QuickbooksAccount::TYPE_BANK); + self::assertNull($currency); + + $currency = $accountRepo->getCurrency(self::TEST_USER, 'Bank USD', QuickbooksAccount::TYPE_EXPENSE); + self::assertNull($currency); + } + + public function testIdent() + { + /** @var SheetScheduler $scheduler */ + $scheduler = static::$container->get(SheetScheduler::class); + + self::assertSame('long-transactions-2856:1:JournalEntryAdd', + $scheduler->shrinkIdent('long-transactions-20200401-20200419.csv', '1', 'JournalEntryAdd')); + + self::assertSame('long-invoices-20200bdc:2:JournalEntryAdd', + $scheduler->shrinkIdent('long-invoices-20200401-20200419.csv', '2', 'JournalEntryAdd')); + + self::assertSame('long-transactions-856:11:JournalEntryAdd', + $scheduler->shrinkIdent('long-transactions-20200401-20200419.csv', '11', 'JournalEntryAdd')); + + self::assertSame('lon856:11:JournalEntryAddJournalEntryAdd', + $scheduler->shrinkIdent('long-transactions-20200401-20200419.csv', '11', 'JournalEntryAddJournalEntryAdd')); + } + + public function testIdentException() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Expected a value greater than 0. Got: -9'); + + /** @var SheetScheduler $scheduler */ + $scheduler = static::$container->get(SheetScheduler::class); + + self::assertSame('lon856:11:JournalEntryAddJournalEntryAddJournalEntryAdd', + $scheduler->shrinkIdent('long-transactions-20200401-20200419.csv', '11', 'JournalEntryAddJournalEntryAddJournalEntryAdd')); + } + + public function testDateFormat() + { + $inv = new CustomerInvoice(); + $inv->setTxnDate('Apr 15, 2020'); + $this->scheduler->canonizeDate([$inv], 'M j, Y'); + self::assertSame('2020-04-15', $inv->getTxnDate()); + } + + public function testDateFormatIncorrect() + { + $inv = new CustomerInvoice(); + $inv->setTxnDate('AAA 15, 2020'); + $this->scheduler->canonizeDate([$inv], 'M j, Y'); + self::assertSame('AAA 15, 2020', $inv->getTxnDate()); + } +} diff --git a/tests/Functional/fixtures/add_customer.xml b/tests/Functional/fixtures/add_customer.xml new file mode 100644 index 0000000..0da67bb --- /dev/null +++ b/tests/Functional/fixtures/add_customer.xml @@ -0,0 +1,25 @@ + + + + + + + Acme Inc. + Acme Inc. + John + Tulchin + + 56 Cowles Road + Willington + CT + 06279 + United States + + + US Dollar + + + + + + \ No newline at end of file diff --git a/tests/Functional/fixtures/authenticate_request.xml b/tests/Functional/fixtures/authenticate_request.xml new file mode 100644 index 0000000..54a2c08 --- /dev/null +++ b/tests/Functional/fixtures/authenticate_request.xml @@ -0,0 +1,10 @@ + + + + + quickbooks + FE7XjHVzjfdH + + + \ No newline at end of file diff --git a/tests/Functional/fixtures/authenticate_response.xml b/tests/Functional/fixtures/authenticate_response.xml new file mode 100644 index 0000000..281f79d --- /dev/null +++ b/tests/Functional/fixtures/authenticate_response.xml @@ -0,0 +1,8 @@ + + + {ticketId} + + + + \ No newline at end of file diff --git a/tests/Functional/fixtures/bill.csv b/tests/Functional/fixtures/bill.csv new file mode 100644 index 0000000..87e1d8c --- /dev/null +++ b/tests/Functional/fixtures/bill.csv @@ -0,0 +1,2 @@ +memo,apAccount,refNumber,txnDate,line1AccountFullName,line1Amount,line1Memo,exchangeRate,vendorFullname,vendorCompanyName,addr1,addr2,vendorType,terms,currency,city,state,postalcode,country +"Bill Memo","Accounts Payable",2018-05-10-0001,2018-05-10,"Rent Expense",10.00,"Item Memo",1,Silo,"Silo LIMITED","68 Tap Kwok Nam Path","Lok Sheuk Tsan","Service Providers","Due on receipt","Hong Kong Dollar","Hong Kong",HK,999077,"Hong Kong" diff --git a/tests/Functional/fixtures/bill_converted.xml b/tests/Functional/fixtures/bill_converted.xml new file mode 100644 index 0000000..a142049 --- /dev/null +++ b/tests/Functional/fixtures/bill_converted.xml @@ -0,0 +1,54 @@ + + + + + + + Silo + Silo LIMITED + + 68 Tap Kwok Nam Path + Lok Sheuk Tsan + Hong Kong + HK + 999077 + Hong Kong + + + Service Providers + + + Due on receipt + + + Hong Kong Dollar + + + + + + + Silo + + + Accounts Payable + + 2018-05-10 + 2018-05-10-0001 + + Due on receipt + + Bill Memo + 1 + + + Rent Expense + + 10.00 + Item Memo + + + + + + \ No newline at end of file diff --git a/tests/Functional/fixtures/bye_request.xml b/tests/Functional/fixtures/bye_request.xml new file mode 100644 index 0000000..b8fda72 --- /dev/null +++ b/tests/Functional/fixtures/bye_request.xml @@ -0,0 +1 @@ +{ticketId} \ No newline at end of file diff --git a/tests/Functional/fixtures/bye_response.xml b/tests/Functional/fixtures/bye_response.xml new file mode 100644 index 0000000..fd09786 --- /dev/null +++ b/tests/Functional/fixtures/bye_response.xml @@ -0,0 +1,6 @@ + + + Complete! + + \ No newline at end of file diff --git a/tests/Functional/fixtures/client_version_request.xml b/tests/Functional/fixtures/client_version_request.xml new file mode 100644 index 0000000..f32d696 --- /dev/null +++ b/tests/Functional/fixtures/client_version_request.xml @@ -0,0 +1 @@ +2.3.0.25 \ No newline at end of file diff --git a/tests/Functional/fixtures/client_version_response.xml b/tests/Functional/fixtures/client_version_response.xml new file mode 100644 index 0000000..602a8db --- /dev/null +++ b/tests/Functional/fixtures/client_version_response.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/tests/Functional/fixtures/company_query_rs.xml b/tests/Functional/fixtures/company_query_rs.xml new file mode 100644 index 0000000..71e8f61 --- /dev/null +++ b/tests/Functional/fixtures/company_query_rs.xml @@ -0,0 +1,202 @@ + + + + + + QuickBooks Desktop Pro 2019 + 29 + 0 + US + 1.0 + 1.1 + 2.0 + 2.1 + 3.0 + 4.0 + 4.1 + 5.0 + 6.0 + 7.0 + 8.0 + 9.0 + 10.0 + 11.0 + 12.0 + 13.0 + false + SingleUser + + + + + false + Acme Inc + Acme Inc + January + January + ProfessionalConsulting + Form1040 + + + QuickBooks Online Banking + banking.qb + Never + + + QuickBooks Online Billing + billing.qb + Never + + + QuickBooks Online Billing Level 1 Service + qbob1.qbn + Never + + + QuickBooks Online Billing Level 2 Service + qbob2.qbn + Never + + + QuickBooks Online Billing Payment Service + qbobpay.qbn + Never + + + QuickBooks Bill Payment + billpay.qb + Never + + + QuickBooks Online Billing Paper Mailing Service + qbobpaper.qbn + Never + + + QuickBooks Payroll Service + payroll.qb + Never + + + QuickBooks Basic Payroll Service + payrollbsc.qb + Never + + + QuickBooks Basic Disk Payroll Service + payrollbscdisk.qb + Never + + + QuickBooks Deluxe Payroll Service + payrolldlx.qb + Never + + + QuickBooks Premier Payroll Service + payrollprm.qb + Never + + + Basic Plus Federal + basic_plus_fed.qb + Never + + + Basic Plus Federal and State + basic_plus_fed_state.qb + Never + + + Basic Plus Direct Deposit + basic_plus_dd.qb + Never + + + Merchant Account Service + mas.qbn + Never + + + + false + + + {0e102412-c367-403d-806f-28b2bea18297} + AppLock + STR255TYPE + LOCKED:WIN-OPFD9B31KJ1:637251322674318831 + + + {0e102412-c367-403d-806f-28b2bea18297} + FileID + STR255TYPE + {0e102412-c367-403d-806f-28b2bea18297} + + + + + + + false + true + false + true + true + + + 0,0 + 0,00 + 0 + false + DueDate + false + + + true + false + false + + + false + + + false + false + + + false + 10 + false + + + AgeFromDueDate + Accrual + + + false + true + + true + true + + + + Monday + + + true + Admin + false + + + false + None + false + false + false + + + + + diff --git a/tests/Functional/fixtures/customer.csv b/tests/Functional/fixtures/customer.csv new file mode 100644 index 0000000..7d2433a --- /dev/null +++ b/tests/Functional/fixtures/customer.csv @@ -0,0 +1,2 @@ +customerFullName,firstName,lastName,companyName,terms,currency,addr1,addr2,city,state,postalcode,country +HomeBase,Long,Allen,"HomeBase LLC","Due on receipt","US Dollar","4420 Shadowmar Drive",Louisiana,"New Orleans",LA,70112,"United States" diff --git a/tests/Functional/fixtures/customer_converted.xml b/tests/Functional/fixtures/customer_converted.xml new file mode 100644 index 0000000..922680d --- /dev/null +++ b/tests/Functional/fixtures/customer_converted.xml @@ -0,0 +1,31 @@ + + + + + + + HomeBase + HomeBase LLC + Long + Allen + + Attn: Long Allen + HomeBase LLC + 4420 Shadowmar Drive + Louisiana + New Orleans + LA + 70112 + United States + + + Due on receipt + + + US Dollar + + + + + + \ No newline at end of file diff --git a/tests/Functional/fixtures/invoice.csv b/tests/Functional/fixtures/invoice.csv new file mode 100644 index 0000000..a2c58f0 --- /dev/null +++ b/tests/Functional/fixtures/invoice.csv @@ -0,0 +1,2 @@ +refNumber,invoiceMemo,arAccount,txnDate,exchangeRate,line1ItemName,line1Desc,line1Quantity,line1Amount,line1Rate,customerFullName,firstName,lastName,companyName,terms,currency,addr1,addr2,city,state,postalcode,country +180510-0001,"Invoice memo","Accounts Receivable - USD",2018-05-10,7.83126,Consulting,"Item desc",1,10.00,,HomeBase,Long,Allen,"HomeBase LLC","Due on receipt","US Dollar","4420 Shadowmar Drive",Louisiana,"New Orleans",LA,70112,"United States" diff --git a/tests/Functional/fixtures/invoice_converted.xml b/tests/Functional/fixtures/invoice_converted.xml new file mode 100644 index 0000000..6277d69 --- /dev/null +++ b/tests/Functional/fixtures/invoice_converted.xml @@ -0,0 +1,67 @@ + + + + + + + HomeBase + HomeBase LLC + Long + Allen + + Attn: Long Allen + HomeBase LLC + 4420 Shadowmar Drive + Louisiana + New Orleans + LA + 70112 + United States + + + Due on receipt + + + US Dollar + + + + + + + HomeBase + + + Accounts Receivable - USD + + 2018-05-10 + 180510-0001 + + Attn: Long Allen + HomeBase LLC + 4420 Shadowmar Drive + Louisiana + New Orleans + LA + 70112 + United States + + + Due on receipt + + Invoice memo + 7.83126 + + + Consulting + + Item desc + 1 + + 10.00 + + + + + + \ No newline at end of file diff --git a/tests/Functional/fixtures/server_version_request.xml b/tests/Functional/fixtures/server_version_request.xml new file mode 100644 index 0000000..2c9bce3 --- /dev/null +++ b/tests/Functional/fixtures/server_version_request.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tests/Functional/fixtures/server_version_response.xml b/tests/Functional/fixtures/server_version_response.xml new file mode 100644 index 0000000..2c5a360 --- /dev/null +++ b/tests/Functional/fixtures/server_version_response.xml @@ -0,0 +1,6 @@ + + + PHP QuickBooks SOAP Server v3.0 at /qbwc + + \ No newline at end of file diff --git a/tests/Functional/fixtures/task_add_customer_added_received_response.xml b/tests/Functional/fixtures/task_add_customer_added_received_response.xml new file mode 100644 index 0000000..d425a28 --- /dev/null +++ b/tests/Functional/fixtures/task_add_customer_added_received_response.xml @@ -0,0 +1,6 @@ + + + 100 + + \ No newline at end of file diff --git a/tests/Functional/fixtures/task_add_customer_added_request.xml b/tests/Functional/fixtures/task_add_customer_added_request.xml new file mode 100644 index 0000000..eea02c2 --- /dev/null +++ b/tests/Functional/fixtures/task_add_customer_added_request.xml @@ -0,0 +1,41 @@ +{ticketId}<?xml version="1.0" ?> +<QBXML> +<QBXMLMsgsRs> +<CustomerAddRs requestID="1" statusCode="0" statusSeverity="Info" statusMessage="Status OK"> +<CustomerRet> +<ListID>80000009-1560962812</ListID> +<TimeCreated>2019-06-19T09:46:52-08:00</TimeCreated> +<TimeModified>2019-06-19T09:46:52-08:00</TimeModified> +<EditSequence>1560962812</EditSequence> +<Name>Acme Inc.</Name> +<FullName>Acme Inc.</FullName> +<IsActive>true</IsActive> +<Sublevel>0</Sublevel> +<CompanyName>Acme Inc.</CompanyName> +<FirstName>John</FirstName> +<LastName>Tulchin</LastName> +<BillAddress> +<Addr1>56 Cowles Road</Addr1> +<City>Willington</City> +<State>CT</State> +<PostalCode>06279</PostalCode> +<Country>USA</Country> +</BillAddress> +<BillAddressBlock> +<Addr1>56 Cowles Road</Addr1> +<Addr2>Willington, CT 06279</Addr2> +<Addr3>United States</Addr3> +</BillAddressBlock> +<Balance>0.00</Balance> +<TotalBalance>0.00</TotalBalance> +<JobStatus>None</JobStatus> +<PreferredDeliveryMethod>None</PreferredDeliveryMethod> +<CurrencyRef> +<ListID>80000096-1560777869</ListID> +<FullName>US Dollar</FullName> +</CurrencyRef> +</CustomerRet> +</CustomerAddRs> +</QBXMLMsgsRs> +</QBXML> + \ No newline at end of file diff --git a/tests/Functional/fixtures/task_add_customer_response.xml b/tests/Functional/fixtures/task_add_customer_response.xml new file mode 100644 index 0000000..05348e8 --- /dev/null +++ b/tests/Functional/fixtures/task_add_customer_response.xml @@ -0,0 +1,30 @@ + + + <?xml version="1.0" encoding="utf-8"?> + <?qbxml version="13.0"?> + <QBXML> + <QBXMLMsgsRq onError="continueOnError"> + <CustomerAddRq requestID="1"> + <CustomerAdd> + <Name>Acme Inc.</Name> + <CompanyName>Acme Inc.</CompanyName> + <FirstName>John</FirstName> + <LastName>Tulchin</LastName> + <BillAddress> + <Addr1>56 Cowles Road</Addr1> + <City>Willington</City> + <State>CT</State> + <PostalCode>06279</PostalCode> + <Country>United States</Country> + </BillAddress> + <CurrencyRef> + <FullName>US Dollar</FullName> + </CurrencyRef> + </CustomerAdd> +</CustomerAddRq> + + </QBXMLMsgsRq> + </QBXML> + + \ No newline at end of file diff --git a/tests/Functional/fixtures/task_request.xml b/tests/Functional/fixtures/task_request.xml new file mode 100644 index 0000000..b9c9fd4 --- /dev/null +++ b/tests/Functional/fixtures/task_request.xml @@ -0,0 +1,226 @@ +{ticketId}<?xml version="1.0" ?> +<QBXML> +<QBXMLMsgsRs> +<HostQueryRs requestID="0" statusCode="0" statusSeverity="Info" statusMessage="Status OK"> +<HostRet> +<ProductName>QuickBooks Desktop Pro 2019</ProductName> +<MajorVersion>29</MajorVersion> +<MinorVersion>0</MinorVersion> +<Country>US</Country> +<SupportedQBXMLVersion>1.0</SupportedQBXMLVersion> +<SupportedQBXMLVersion>1.1</SupportedQBXMLVersion> +<SupportedQBXMLVersion>2.0</SupportedQBXMLVersion> +<SupportedQBXMLVersion>2.1</SupportedQBXMLVersion> +<SupportedQBXMLVersion>3.0</SupportedQBXMLVersion> +<SupportedQBXMLVersion>4.0</SupportedQBXMLVersion> +<SupportedQBXMLVersion>4.1</SupportedQBXMLVersion> +<SupportedQBXMLVersion>5.0</SupportedQBXMLVersion> +<SupportedQBXMLVersion>6.0</SupportedQBXMLVersion> +<SupportedQBXMLVersion>7.0</SupportedQBXMLVersion> +<SupportedQBXMLVersion>8.0</SupportedQBXMLVersion> +<SupportedQBXMLVersion>9.0</SupportedQBXMLVersion> +<SupportedQBXMLVersion>10.0</SupportedQBXMLVersion> +<SupportedQBXMLVersion>11.0</SupportedQBXMLVersion> +<SupportedQBXMLVersion>12.0</SupportedQBXMLVersion> +<SupportedQBXMLVersion>13.0</SupportedQBXMLVersion> +<IsAutomaticLogin>false</IsAutomaticLogin> +<QBFileMode>SingleUser</QBFileMode> +</HostRet> +</HostQueryRs> +<CompanyQueryRs requestID="1" statusCode="0" statusSeverity="Info" statusMessage="Status OK"> +<CompanyRet> +<IsSampleCompany>false</IsSampleCompany> +<CompanyName>ACME LIMITED</CompanyName> +<LegalCompanyName>ACME LIMITED</LegalCompanyName> +<Address> +<Addr1>1104A, Kai Tak Comm Bld, </Addr1> +<Addr2>317-319 Des Voeux Rd. Central</Addr2> +<City>Hong Kong</City> +<PostalCode>999077</PostalCode> +<Country>Other</Country> +</Address> +<AddressBlock> +<Addr1>1104A, Kai Tak Comm Bld,</Addr1> +<Addr2>317-319 Des Voeux Rd. Central</Addr2> +<Addr3>Hong Kong, 999077</Addr3> +</AddressBlock> +<LegalAddress> +<Addr1>1104A, Kai Tak Comm Bld,</Addr1> +<Addr2>317-319 Des Voeux Rd. Central</Addr2> +<City>Hong Kong</City> +<PostalCode>999077</PostalCode> +<Country>US</Country> +</LegalAddress> +<Email>info@acme.com</Email> +<FirstMonthFiscalYear>January</FirstMonthFiscalYear> +<FirstMonthIncomeTaxYear>January</FirstMonthIncomeTaxYear> +<CompanyType>InformationTechnologyComputersSoftware</CompanyType> +<TaxForm>Form1065</TaxForm> +<SubscribedServices> +<Service> +<Name>QuickBooks Online Banking</Name> +<Domain>banking.qb</Domain> +<ServiceStatus>Never</ServiceStatus> +</Service> +<Service> +<Name>QuickBooks Online Billing</Name> +<Domain>billing.qb</Domain> +<ServiceStatus>Never</ServiceStatus> +</Service> +<Service> +<Name>QuickBooks Online Billing Level 1 Service</Name> +<Domain>qbob1.qbn</Domain> +<ServiceStatus>Never</ServiceStatus> +</Service> +<Service> +<Name>QuickBooks Online Billing Level 2 Service</Name> +<Domain>qbob2.qbn</Domain> +<ServiceStatus>Never</ServiceStatus> +</Service> +<Service> +<Name>QuickBooks Online Billing Payment Service</Name> +<Domain>qbobpay.qbn</Domain> +<ServiceStatus>Never</ServiceStatus> +</Service> +<Service> +<Name>QuickBooks Bill Payment</Name> +<Domain>billpay.qb</Domain> +<ServiceStatus>Never</ServiceStatus> +</Service> +<Service> +<Name>QuickBooks Online Billing Paper Mailing Service</Name> +<Domain>qbobpaper.qbn</Domain> +<ServiceStatus>Never</ServiceStatus> +</Service> +<Service> +<Name>QuickBooks Payroll Service</Name> +<Domain>payroll.qb</Domain> +<ServiceStatus>Never</ServiceStatus> +</Service> +<Service> +<Name>QuickBooks Basic Payroll Service</Name> +<Domain>payrollbsc.qb</Domain> +<ServiceStatus>Never</ServiceStatus> +</Service> +<Service> +<Name>QuickBooks Basic Disk Payroll Service</Name> +<Domain>payrollbscdisk.qb</Domain> +<ServiceStatus>Never</ServiceStatus> +</Service> +<Service> +<Name>QuickBooks Deluxe Payroll Service</Name> +<Domain>payrolldlx.qb</Domain> +<ServiceStatus>Never</ServiceStatus> +</Service> +<Service> +<Name>QuickBooks Premier Payroll Service</Name> +<Domain>payrollprm.qb</Domain> +<ServiceStatus>Never</ServiceStatus> +</Service> +<Service> +<Name>Basic Plus Federal</Name> +<Domain>basic_plus_fed.qb</Domain> +<ServiceStatus>Never</ServiceStatus> +</Service> +<Service> +<Name>Basic Plus Federal and State</Name> +<Domain>basic_plus_fed_state.qb</Domain> +<ServiceStatus>Never</ServiceStatus> +</Service> +<Service> +<Name>Basic Plus Direct Deposit</Name> +<Domain>basic_plus_dd.qb</Domain> +<ServiceStatus>Never</ServiceStatus> +</Service> +<Service> +<Name>Merchant Account Service</Name> +<Domain>mas.qbn</Domain> +<ServiceStatus>Never</ServiceStatus> +</Service> +</SubscribedServices> +<AccountantCopy> +<AccountantCopyExists>false</AccountantCopyExists> +</AccountantCopy> +<DataExtRet> +<OwnerID>{0e77aa8a-0e9c-a864-41a7-4aa4f63858ab}</OwnerID> +<DataExtName>AppLock</DataExtName> +<DataExtType>STR255TYPE</DataExtType> +<DataExtValue>LOCKED:WIN-KDMFU3DFTBK:636965595837848585</DataExtValue> +</DataExtRet> +<DataExtRet> +<OwnerID>{0e77aa8a-0e9c-a864-41a7-4aa4f63858ab}</OwnerID> +<DataExtName>FileID</DataExtName> +<DataExtType>STR255TYPE</DataExtType> +<DataExtValue>{7e113158-25d1-2004-4d47-a62929e377f2}</DataExtValue> +</DataExtRet> +</CompanyRet> +</CompanyQueryRs> +<PreferencesQueryRs requestID="2" statusCode="0" statusSeverity="Info" statusMessage="Status OK"> +<PreferencesRet> +<AccountingPreferences> +<IsUsingAccountNumbers>false</IsUsingAccountNumbers> +<IsRequiringAccounts>true</IsRequiringAccounts> +<IsUsingClassTracking>false</IsUsingClassTracking> +<IsUsingAuditTrail>true</IsUsingAuditTrail> +<IsAssigningJournalEntryNumbers>true</IsAssigningJournalEntryNumbers> +</AccountingPreferences> +<FinanceChargePreferences> +<AnnualInterestRate>0.00</AnnualInterestRate> +<MinFinanceCharge>0.00</MinFinanceCharge> +<GracePeriod>0</GracePeriod> +<IsAssessingForOverdueCharges>false</IsAssessingForOverdueCharges> +<CalculateChargesFrom>DueDate</CalculateChargesFrom> +<IsMarkedToBePrinted>false</IsMarkedToBePrinted> +</FinanceChargePreferences> +<JobsAndEstimatesPreferences> +<IsUsingEstimates>true</IsUsingEstimates> +<IsUsingProgressInvoicing>false</IsUsingProgressInvoicing> +<IsPrintingItemsWithZeroAmounts>false</IsPrintingItemsWithZeroAmounts> +</JobsAndEstimatesPreferences> +<MultiCurrencyPreferences> +<IsMultiCurrencyOn>true</IsMultiCurrencyOn> +<HomeCurrencyRef> +<ListID>8000003F-1560777868</ListID> +<FullName>Hong Kong Dollar</FullName> +</HomeCurrencyRef> +</MultiCurrencyPreferences> +<MultiLocationInventoryPreferences> +<IsMultiLocationInventoryAvailable>false</IsMultiLocationInventoryAvailable> +<IsMultiLocationInventoryEnabled>false</IsMultiLocationInventoryEnabled> +</MultiLocationInventoryPreferences> +<PurchasesAndVendorsPreferences> +<IsUsingInventory>false</IsUsingInventory> +<DaysBillsAreDue>10</DaysBillsAreDue> +<IsAutomaticallyUsingDiscounts>false</IsAutomaticallyUsingDiscounts> +</PurchasesAndVendorsPreferences> +<ReportsPreferences> +<AgingReportBasis>AgeFromDueDate</AgingReportBasis> +<SummaryReportBasis>Accrual</SummaryReportBasis> +</ReportsPreferences> +<SalesAndCustomersPreferences> +<IsTrackingReimbursedExpensesAsIncome>false</IsTrackingReimbursedExpensesAsIncome> +<IsAutoApplyingPayments>true</IsAutoApplyingPayments> +<PriceLevels> +<IsUsingPriceLevels>true</IsUsingPriceLevels> +<IsRoundingSalesPriceUp>true</IsRoundingSalesPriceUp> +</PriceLevels> +</SalesAndCustomersPreferences> +<TimeTrackingPreferences> +<FirstDayOfWeek>Monday</FirstDayOfWeek> +</TimeTrackingPreferences> +<CurrentAppAccessRights> +<IsAutomaticLoginAllowed>false</IsAutomaticLoginAllowed> +<IsPersonalDataAccessAllowed>false</IsPersonalDataAccessAllowed> +</CurrentAppAccessRights> +<ItemsAndInventoryPreferences> +<EnhancedInventoryReceivingEnabled>false</EnhancedInventoryReceivingEnabled> +<IsTrackingSerialOrLotNumber>None</IsTrackingSerialOrLotNumber> +<FIFOEnabled>false</FIFOEnabled> +<IsRSBEnabled>false</IsRSBEnabled> +<IsBarcodeEnabled>false</IsBarcodeEnabled> +</ItemsAndInventoryPreferences> +</PreferencesRet> +</PreferencesQueryRs> +</QBXMLMsgsRs> +</QBXML> +C:\Users\Public\Documents\Intuit\QuickBooks\Company Files\ACME LIMITED.qbwUS130 \ No newline at end of file diff --git a/tests/Functional/fixtures/transaction.csv b/tests/Functional/fixtures/transaction.csv new file mode 100644 index 0000000..4b69479 --- /dev/null +++ b/tests/Functional/fixtures/transaction.csv @@ -0,0 +1,4 @@ +txnDate,refNumber,currency,exchangeRate,creditAccount,creditMemo,creditAmount,debitAccount,debitMemo,debitAmount +2018-10-10,ref1,"US Dollar",7.83704,"Bank USD","credit memo",400.00,"Uncategorized Expenses","debit memo",400.00 +2018-10-10,ref21,"US Dollar",7.83704,"Bank USD","credit memo",500.00,"Undeposited Funds","debit memo",500.00 +2018-10-10,ref22,Euro,9.0126,"Undeposited Funds","credit memo",433.33,"Bank EUR","debit memo",433.33 diff --git a/tests/Functional/fixtures/transaction_converted.xml b/tests/Functional/fixtures/transaction_converted.xml new file mode 100644 index 0000000..2a73424 --- /dev/null +++ b/tests/Functional/fixtures/transaction_converted.xml @@ -0,0 +1,79 @@ + + + + + + + 2018-10-10 + ref1 + + US Dollar + + 7.83704 + + + Uncategorized Expenses + + 400.00 + debit memo + + + + Bank USD + + 400.00 + credit memo + + + + + + 2018-10-10 + ref21 + + US Dollar + + 7.83704 + + + Undeposited Funds + + 500.00 + debit memo + + + + Bank USD + + 500.00 + credit memo + + + + + + 2018-10-10 + ref22 + + Euro + + 9.0126 + + + Bank EUR + + 433.33 + debit memo + + + + Undeposited Funds + + 433.33 + credit memo + + + + + + \ No newline at end of file diff --git a/tests/Functional/fixtures/vendor.csv b/tests/Functional/fixtures/vendor.csv new file mode 100644 index 0000000..8f5299b --- /dev/null +++ b/tests/Functional/fixtures/vendor.csv @@ -0,0 +1,2 @@ +vendorFullname,vendorCompanyName,addr1,addr2,vendorType,terms,currency,city,state,postalcode,country +Silo,"Silo LIMITED","68 Tap Kwok Nam Path","Lok Sheuk Tsan","Service Providers","Due on receipt","Hong Kong Dollar","Hong Kong",HK,999077,"Hong Kong" diff --git a/tests/Functional/fixtures/vendor_converted.xml b/tests/Functional/fixtures/vendor_converted.xml new file mode 100644 index 0000000..55614db --- /dev/null +++ b/tests/Functional/fixtures/vendor_converted.xml @@ -0,0 +1,30 @@ + + + + + + + Silo + Silo LIMITED + + 68 Tap Kwok Nam Path + Lok Sheuk Tsan + Hong Kong + HK + 999077 + Hong Kong + + + Service Providers + + + Due on receipt + + + Hong Kong Dollar + + + + + + \ No newline at end of file diff --git a/tests/Unit/Accounts/AccountsUpdaterTest.php b/tests/Unit/Accounts/AccountsUpdaterTest.php new file mode 100644 index 0000000..d31eec7 --- /dev/null +++ b/tests/Unit/Accounts/AccountsUpdaterTest.php @@ -0,0 +1,75 @@ +server = $this->getMockBuilder(QuickbooksServerInterface::class)->getMock(); + $this->accountRepo = $this->getMockBuilder(QuickbooksAccountRepositoryInterface::class)->getMock(); + $this->companyRepo = $this->getMockBuilder(QuickbooksCompanyRepositoryInterface::class)->getMock(); + $this->em = $this->getMockBuilder(EntityManagerInterface::class)->getMock(); + $this->em->method('getRepository')->willReturnMap([ + [QuickbooksAccount::class, $this->accountRepo], + [QuickbooksCompany::class, $this->companyRepo], + ]); + + + $this->updater = new AccountsUpdater( + new QuickbooksFormatter(), + $this->server, + $this->em + ); + } + + + public function testSchedule(): void + { + $this->server->expects(self::once())->method('schedule') + ->with('user_test', QUICKBOOKS_QUERY_ACCOUNT, AccountsUpdater::ACCOUNTS_UPDATE_REQUEST_ID) + ->willReturn(true); + self::assertTrue($this->updater->scheduleUpdate('user_test')); + } + + public function testUpdate(): void + { + $this->accountRepo->expects(self::once())->method('deleteAll'); + + $user = new User(); + $user->setId(999); + $company = new QuickbooksCompany('test_user'); + $company->setUser($user); + $this->companyRepo->method('findOneBy')->willReturn($company); + + $this->em->expects(self::atLeastOnce())->method('persist') + ->with(self::isInstanceOf(QuickbooksAccount::class)); + $this->em->expects(self::once())->method('flush'); + + $xml = file_get_contents(__DIR__.'/chart_of_accounts.xml'); + $this->updater->update('test_user', $xml); + } +} diff --git a/tests/Unit/Accounts/chart_of_accounts.xml b/tests/Unit/Accounts/chart_of_accounts.xml new file mode 100644 index 0000000..3116e10 --- /dev/null +++ b/tests/Unit/Accounts/chart_of_accounts.xml @@ -0,0 +1,93 @@ + + + + + + 80000028-1560778874 + 2019-06-17T06:41:14-08:00 + 2019-07-22T11:03:01-08:00 + 1560778874 + Bank EUR + Bank EUR + true + 0 + Bank + 0.00 + 0.00 + + 1864 + B/S-Assets: Cash + + NotApplicable + + 80000033-1560777868 + Euro + + + + 80000027-1560778661 + 2019-06-17T06:37:41-08:00 + 2019-07-22T11:03:15-08:00 + 1560778661 + Bank USD + Bank USD + true + 0 + Bank + 0.00 + 0.00 + + 1864 + B/S-Assets: Cash + + NotApplicable + + 80000096-1560777869 + US Dollar + + + + 8000002D-1560783117 + 2019-06-17T07:51:57-08:00 + 2019-06-17T07:51:57-08:00 + 1560783117 + Accounts Receivable + Accounts Receivable + true + 0 + AccountsReceivable + AccountsReceivable + 11000 + Unpaid or unapplied customer invoices and credits + 0.00 + 0.00 + Operating + + 8000003F-1560777868 + Hong Kong Dollar + + + + 8000002E-1560783173 + 2019-06-17T07:52:53-08:00 + 2019-07-21T07:06:24-08:00 + 1560783173 + Accounts Receivable - USD + Accounts Receivable - USD + true + 0 + AccountsReceivable + AccountsReceivable + 11001 + Unpaid or unapplied customer invoices and credits + 0.00 + 0.00 + Operating + + 80000096-1560777869 + US Dollar + + + + + diff --git a/tests/Unit/Currency/UpdateRateOnEntityScheduledSubscriberTest.php b/tests/Unit/Currency/UpdateRateOnEntityScheduledSubscriberTest.php new file mode 100644 index 0000000..a2145b6 --- /dev/null +++ b/tests/Unit/Currency/UpdateRateOnEntityScheduledSubscriberTest.php @@ -0,0 +1,71 @@ +user = new QuickbooksCompany(self::TEST_USER); + $this->user->setBaseCurrency('HKD'); + $this->exchanger = $this->getMockBuilder(CurrencyExchangerInterface::class)->getMock(); + $this->subscriber = new UpdateRateOnEntityScheduledSubscriber($this->exchanger); + } + + public function testSetExchangeRateIfNotSet(): void + { + $t1 = new Transaction(); + $t1->setTxnDate('2018-10-10'); + $t1->setCurrency('US Dollar'); + + $this->exchanger->expects(self::once())->method('getExchangeRate') + ->with('HKD', 'USD', '2018-10-10') + ->willReturn(8); + $event = new EntityOnScheduledEvent($this->user, [$t1], 0); + $this->subscriber->onScheduled($event); + + self::assertSame('8', $t1->getExchangeRate()); + } + + public function testSkipsIfExchangeRateIsSet(): void + { + $t1 = new Transaction(); + $t1->setTxnDate('2018-10-10'); + $t1->setCurrency('US Dollar'); + $t1->setExchangeRate('8'); + + $this->exchanger->expects(self::never())->method('getExchangeRate'); + + $event = new EntityOnScheduledEvent($this->user, [$t1], 0); + $this->subscriber->onScheduled($event); + + self::assertSame('8', $t1->getExchangeRate()); + } + + public function testIrrelevantObjectsSkipped(): void + { + $obj = new \stdClass(); + $obj->a = 'b'; + + $this->exchanger->expects(self::never())->method('getExchangeRate'); + $event = new EntityOnScheduledEvent($this->user, [$obj], 0); + $this->subscriber->onScheduled($event); + } +} diff --git a/tests/Unit/EventSubscriber/UpdateCompanySubscriberTest.php b/tests/Unit/EventSubscriber/UpdateCompanySubscriberTest.php new file mode 100644 index 0000000..cea47d2 --- /dev/null +++ b/tests/Unit/EventSubscriber/UpdateCompanySubscriberTest.php @@ -0,0 +1,108 @@ +companyRepo = $this->getMockBuilder(QuickbooksCompanyRepositoryInterface::class)->getMock(); + $this->em = $this->getMockBuilder(EntityManagerInterface::class)->getMock(); + $this->subscriber = new UpdateCompanySubscriber($this->companyRepo, $this->em); + $this->xml = file_get_contents(__DIR__.'/../../Functional/fixtures/company_query_rs.xml'); + Assert::string($this->xml); + } + + public function testOnSendRequestXmlEvent() + { + $this->em->expects(self::once())->method('flush'); + $this->companyRepo->expects(self::once())->method('findOneBy') + ->willReturn($c = new QuickbooksCompany()); + + $c->setMultiCurrencyEnabled(true); + $c->setDecimalSymbol('.'); + $c->setDigitGroupingSymbol(','); + $c->setQbCompanyFile(null); + + $this->subscriber->onSendRequestXmlEvent($this->getEvent($this->xml)); + Assert::false($c->isMultiCurrencyEnabled()); + Assert::same(',', $c->getDecimalSymbol()); + Assert::same('.', $c->getDigitGroupingSymbol()); + Assert::same('C:\\Users\\Public\\Documents\\Intuit\\QuickBooks\\Company Files\\Acme Inc.qbw', $c->getQbCompanyFile()); + } + + public function testDoesNothingIfNoCompany() + { + $this->em->expects(self::never())->method('flush'); + $this->companyRepo->expects(self::once())->method('findOneBy')->willReturn(null); + + $this->subscriber->onSendRequestXmlEvent($this->getEvent($this->xml)); + } + + public function testDoesNothingOnInvalidXML() + { + $this->em->expects(self::never())->method('flush'); + $this->companyRepo->expects(self::once())->method('findOneBy') + ->willReturn($c = new QuickbooksCompany()); + + $xml = 'invalid'; + $this->subscriber->onSendRequestXmlEvent($this->getEvent($xml)); + } + + public function testDoesNothingOnIncorrectXML() + { + $this->em->expects(self::never())->method('flush'); + $this->companyRepo->expects(self::once())->method('findOneBy') + ->willReturn($c = new QuickbooksCompany()); + + $xml = ''; + $this->subscriber->onSendRequestXmlEvent($this->getEvent($xml)); + } + + public function testGetDecimalSymbol() + { + self::assertSame('.', $this->subscriber->getDecimalSymbol('100,000.00')); + self::assertSame(',', $this->subscriber->getDecimalSymbol('100.000,00')); + self::assertSame(',', $this->subscriber->getDecimalSymbol('100,00')); + self::assertSame('.', $this->subscriber->getDecimalSymbol('100.00')); + self::assertSame('.', $this->subscriber->getDecimalSymbol('0')); + self::assertSame('.', $this->subscriber->getDecimalSymbol('')); + self::assertSame('.', $this->subscriber->getDecimalSymbol(null)); + } + + private function getEvent(?string $xml): QuickbooksServerSendRequestXmlEvent + { + \QuickBooks_WebConnector_Handlers::HOOK_AUTHENTICATE; //hack to load constants + return new QuickbooksServerSendRequestXmlEvent(null, self::USERNAME, QUICKBOOKS_HANDLERS_HOOK_SENDREQUESTXML, '', [ + 'username' => self::USERNAME, + 'ticket' => '20bdc17a-83aa-2de4-ad26-0614439c0391', + 'strHCPResponse' => $xml, + 'strCompanyFileName' => 'C:\\Users\\Public\\Documents\\Intuit\\QuickBooks\\Company Files\\Acme Inc.qbw', + 'qbXMLCountry' => 'US', + 'qbXMLMajorVers' => '13', + 'qbXMLMinorVers' => '0', + 'requestID' => null, + 'user' => self::USERNAME, + ], []); + } +} diff --git a/tests/Unit/SheetScheduler/CustomerTest.php b/tests/Unit/SheetScheduler/CustomerTest.php new file mode 100644 index 0000000..a3e8de8 --- /dev/null +++ b/tests/Unit/SheetScheduler/CustomerTest.php @@ -0,0 +1,125 @@ +customer = new Customer(); + $this->customer->setAddr1(self::ADDR_1); + $this->customer->setAddr2(self::ADDR_2); + $this->customer->setCity(self::CITY); + $this->customer->setState(self::STATE); + $this->customer->setPostalcode(self::ZIP); + $this->customer->setCountry(self::COUNTRY); + $this->customer->setCompanyName(''); + $this->customer->setFirstName(''); + $this->customer->setLastName(''); + } + + public function testWithoutCompanyAndName(): void + { + self::assertSame([ + self::ADDR_1, + self::ADDR_2, + '', + '', + '', + self::CITY, + self::STATE, + '', + self::ZIP, + self::COUNTRY, + ], $this->customer->composeBillAddress()); + } + + public function testWithCompanyAndName(): void + { + $this->customer->setCompanyName('Company'); + $this->customer->setFirstName('First'); + $this->customer->setLastName('Last'); + + self::assertSame([ + 'Attn: First Last', + 'Company', + self::ADDR_1, + self::ADDR_2, + '', + self::CITY, + self::STATE, + '', + self::ZIP, + self::COUNTRY, + ], $this->customer->composeBillAddress()); + } + + public function testWithCompanyWithoutName(): void + { + $this->customer->setCompanyName('Company'); + + self::assertSame([ + 'Company', + self::ADDR_1, + self::ADDR_2, + '', + '', + self::CITY, + self::STATE, + '', + self::ZIP, + self::COUNTRY, + ], $this->customer->composeBillAddress()); + } + + public function testWithCompanyAndFirstName(): void + { + $this->customer->setCompanyName('Company'); + $this->customer->setFirstName('First'); + + self::assertSame([ + 'Attn: First', + 'Company', + self::ADDR_1, + self::ADDR_2, + '', + self::CITY, + self::STATE, + '', + self::ZIP, + self::COUNTRY, + ], $this->customer->composeBillAddress()); + } + + public function testWithCompanyAndLastName(): void + { + $this->customer->setCompanyName('Company'); + $this->customer->setLastName('Last'); + + self::assertSame([ + 'Attn: Last', + 'Company', + self::ADDR_1, + self::ADDR_2, + '', + self::CITY, + self::STATE, + '', + self::ZIP, + self::COUNTRY, + ], $this->customer->composeBillAddress()); + } +} diff --git a/tests/Unit/SheetScheduler/LineItemLogicTest.php b/tests/Unit/SheetScheduler/LineItemLogicTest.php new file mode 100644 index 0000000..104c58e --- /dev/null +++ b/tests/Unit/SheetScheduler/LineItemLogicTest.php @@ -0,0 +1,24 @@ +expectExceptionMessage("Amount is required"); + $this->expectException(RuntimeException::class); + LineItemLogic::getQuantity('', '', ''); + } +} diff --git a/tests/Unit/SheetScheduler/SplitTransactionsSubscriberTest.php b/tests/Unit/SheetScheduler/SplitTransactionsSubscriberTest.php new file mode 100644 index 0000000..b864356 --- /dev/null +++ b/tests/Unit/SheetScheduler/SplitTransactionsSubscriberTest.php @@ -0,0 +1,163 @@ +user = new QuickbooksCompany(self::TEST_USER); + $this->accountRepo = $this->getMockBuilder(QuickbooksAccountRepositoryInterface::class)->getMock(); + $this->em = $this->getMockBuilder(EntityManagerInterface::class)->getMock(); + $this->em->method('getRepository')->willReturnMap([ + [QuickbooksAccount::class, $this->accountRepo], + ]); + + $this->subscriber = new SplitTransactionsSubscriber($this->em); + } + + public function testIrrelevantObjects(): void + { + $obj = new \stdClass(); + $obj->a = 'b'; + + $event = new EntityOnScheduledEvent($this->user, [$obj], 0); + $this->subscriber->onScheduled($event); + + self::assertSame([$obj], $event->getEntities()); + } + + public function testTransferSameCurrency(): void + { + $transaction = new Transaction(); + $transaction->setTxnDate('2018-10-10'); + $transaction->setRefNumber('ref1'); + $transaction->setCurrency('US Dollar'); + $transaction->setExchangeRate('7.83704'); + $transaction->setCreditAccount('Bank1 USD'); + $transaction->setCreditMemo('credit memo'); + $transaction->setCreditAmount('400.00'); + $transaction->setDebitAccount('Bank2 USD'); + $transaction->setDebitMemo('debit memo'); + $transaction->setDebitAmount('400.00'); + + $event = new EntityOnScheduledEvent($this->user, [$transaction], 0); + $this->subscriber->onScheduled($event); + + self::assertSame([$transaction], $event->getEntities()); + } + + public function testTransferDifferentCurrency(): void + { + $transaction = new Transaction(); + $transaction->setTxnDate('2018-10-10'); + $transaction->setRefNumber('ref1'); + $transaction->setCurrency('US Dollar'); + $transaction->setCreditAccount('Bank USD'); + $transaction->setCreditMemo('credit memo'); + $transaction->setCreditAmount('500.00'); + $transaction->setDebitAccount('Bank EUR'); + $transaction->setDebitMemo('debit memo'); + $transaction->setDebitAmount('433.33'); + + $this->accountRepo->method('getCurrency')->willReturnMap([ + [self::TEST_USER, 'Bank USD', QuickbooksAccount::TYPE_BANK, 'US Dollar'], + [self::TEST_USER, 'Bank EUR', QuickbooksAccount::TYPE_BANK, 'Euro'], + ]); + + + $event = new EntityOnScheduledEvent($this->user, [$transaction], 0); + $this->subscriber->onScheduled($event); + + self::assertCount(2, $event->getEntities()); + [$e1, $e2] = $event->getEntities(); + $normalizer = new ObjectNormalizer(); + $actual = $normalizer->normalize($e1); + $expected = [ + 'txnDate' => '2018-10-10', + 'refNumber' => 'ref1', + 'currency' => 'US Dollar', + 'exchangeRate' => null, + 'creditAccount' => 'Bank USD', + 'creditMemo' => 'credit memo', + 'creditAmount' => '500.00', + 'debitAccount' => QuickbooksAccount::UNDEPOSITED_FUNDS, + 'debitMemo' => 'debit memo', + 'debitAmount' => '500.00', + ]; + self::assertEquals($expected, $actual); + + $actual = $normalizer->normalize($e2); + $expected = [ + 'txnDate' => '2018-10-10', + 'refNumber' => 'ref1', + 'currency' => 'Euro', + 'exchangeRate' => SplitTransactionsSubscriber::ID, + 'creditAccount' => QuickbooksAccount::UNDEPOSITED_FUNDS, + 'creditMemo' => 'credit memo', + 'creditAmount' => '433.33', + 'debitAccount' => 'Bank EUR', + 'debitMemo' => 'debit memo', + 'debitAmount' => '433.33', + ]; + self::assertEquals($expected, $actual); + } + + public function testUpdatesReversedRate(): void + { + $t1 = new Transaction(); + $t1->setTxnDate('2018-10-10'); + $t1->setCurrency('US Dollar'); + $t1->setCreditAccount('Bank USD'); + $t1->setCreditAmount('500.00'); + $t1->setDebitAccount(QuickbooksAccount::UNDEPOSITED_FUNDS); + $t1->setDebitAmount('500.00'); + $t1->setExchangeRate('8'); + + $t2 = new Transaction(); + $t2->setTxnDate('2018-10-10'); + $t2->setCurrency('Euro'); + $t2->setCreditAccount(QuickbooksAccount::UNDEPOSITED_FUNDS); + $t2->setCreditAmount('433.33'); + $t2->setDebitAccount('Bank EUR'); + $t2->setDebitAmount('433.33'); + $t2->setExchangeRate(SplitTransactionsSubscriber::ID); + + $this->accountRepo->method('getCurrency')->willReturnMap([ + [self::TEST_USER, 'Bank USD', QuickbooksAccount::TYPE_BANK, 'US Dollar'], + [self::TEST_USER, 'Bank EUR', QuickbooksAccount::TYPE_BANK, 'Euro'], + ]); + + $event = new EntityOnScheduledEvent($this->user, [$t1, $t2], 0); + $this->subscriber->afterExchangeRateUpdated($event); + + self::assertCount(2, $event->getEntities()); + /** @var Transaction[] $entities */ + $entities = $event->getEntities(); + [$e1, $e2] = $entities; + self::assertSame('9.2308402372326', $e2->getExchangeRate()); + } +} diff --git a/tests/Unit/SheetScheduler/Transformer/TransactionTransformerTest.php b/tests/Unit/SheetScheduler/Transformer/TransactionTransformerTest.php new file mode 100644 index 0000000..3a835ae --- /dev/null +++ b/tests/Unit/SheetScheduler/Transformer/TransactionTransformerTest.php @@ -0,0 +1,123 @@ +setMultiCurrencyEnabled(true); + $this->company = $company; + + $this->transformer = new TransactionTransformer(); + $this->transformer->setCompany($this->company); + } + + public function testExpense(): void + { + $transaction = new Transaction(); + $transaction->setTxnDate('2018-10-10'); + $transaction->setRefNumber('ref1'); + $transaction->setCurrency('US Dollar'); + $transaction->setExchangeRate('7.83704'); + $transaction->setCreditAccount('Bank USD'); + $transaction->setCreditMemo('credit memo'); + $transaction->setCreditAmount('1,400.00'); + $transaction->setDebitAccount(QuickbooksAccount::UNCATEGORIZED_EXPENSES); + $transaction->setDebitMemo('debit memo'); + $transaction->setDebitAmount('1,400.00'); + + $results = $this->transformer->transform($transaction); + self::assertCount(1, $results); + /** @var \QuickBooks_QBXML_Object_JournalEntry $object */ + [$action, $object] = $results[0]; + self::assertSame(QUICKBOOKS_ADD_JOURNALENTRY, $action); + $actual = $this->toArray($object->asList(null)); + $expected = [ + 'TxnDate' => '2018-10-10', + 'RefNumber' => 'ref1', + 'ExchangeRate' => '7.83704', + 'CurrencyRef FullName' => 'US Dollar', + 'JournalCreditLine' => [[ + 'AccountRef FullName' => 'Bank USD', + 'Amount' => '1400.00', + 'Memo' => 'credit memo', + ]], + 'JournalDebitLine' => [[ + 'AccountRef FullName' => QuickbooksAccount::UNCATEGORIZED_EXPENSES, + 'Amount' => '1400.00', + 'Memo' => 'debit memo', + ]], + ]; + self::assertEquals($expected, $actual); + } + + public function testTransferSameCurrency(): void + { + $transaction = new Transaction(); + $transaction->setTxnDate('2018-10-10'); + $transaction->setRefNumber('ref1'); + $transaction->setCurrency('US Dollar'); + $transaction->setExchangeRate('7.83704'); + $transaction->setCreditAccount('Bank1 USD'); + $transaction->setCreditMemo('credit memo'); + $transaction->setCreditAmount('400.00'); + $transaction->setDebitAccount('Bank2 USD'); + $transaction->setDebitMemo('debit memo'); + $transaction->setDebitAmount('400.00'); + + $results = $this->transformer->transform($transaction); + self::assertCount(1, $results); + /** @var \QuickBooks_QBXML_Object_JournalEntry $object */ + [$action, $object] = $results[0]; + self::assertSame(QUICKBOOKS_ADD_JOURNALENTRY, $action); + $actual = $this->toArray($object->asList(null)); + $expected = [ + 'TxnDate' => '2018-10-10', + 'RefNumber' => 'ref1', + 'ExchangeRate' => '7.83704', + 'CurrencyRef FullName' => 'US Dollar', + 'JournalCreditLine' => [[ + 'AccountRef FullName' => 'Bank1 USD', + 'Amount' => '400.00', + 'Memo' => 'credit memo', + ]], + 'JournalDebitLine' => [[ + 'AccountRef FullName' => 'Bank2 USD', + 'Amount' => '400.00', + 'Memo' => 'debit memo', + ]], + ]; + self::assertEquals($expected, $actual); + } + + + private function toArray(array $list): array + { + $array = []; + + foreach ($list as $key => $value) { + if ($value instanceof QuickBooks_QBXML_Object) { + $array[$key] = $this->toArray($value->asList(null)); + } else if (is_array($value)) { + $array[$key] = $this->toArray($value); + } else { + $array[$key] = $value; + } + } + return $array; + } +} diff --git a/tests/Unit/SheetScheduler/Transformer/TransformerContextTraitTest.php b/tests/Unit/SheetScheduler/Transformer/TransformerContextTraitTest.php new file mode 100644 index 0000000..3ecfb93 --- /dev/null +++ b/tests/Unit/SheetScheduler/Transformer/TransformerContextTraitTest.php @@ -0,0 +1,53 @@ +company = new QuickbooksCompany('test'); + } + + public function testDefaultMultiCurrencyDisabled() + { + self::assertFalse($this->isMultiCurrencyEnabled()); + } + + public function testMultiCurrencyEnabled() + { + $this->company->setMultiCurrencyEnabled(true); + self::assertTrue($this->isMultiCurrencyEnabled()); + } + + public function testNoCompanyMultiCurrencyDisabled() + { + $this->company = null; + self::assertFalse($this->isMultiCurrencyEnabled()); + } + + public function testDefaultUsedDecimalSymbolDot() + { + self::assertSame('100.00', $this->getAmount('100.00')); + self::assertSame('0.00',$this->getAmount('0.00')); + self::assertSame('0', $this->getAmount('0')); + } + + public function testUsedDecimalSymbolComma() + { + $this->company->setDecimalSymbol(','); + + self::assertSame('100,00', $this->getAmount('100.00')); + self::assertSame('0,00',$this->getAmount('0.00')); + self::assertSame('0', $this->getAmount('0')); + } +} diff --git a/tests/Unit/TransactionsConverterTest.php b/tests/Unit/TransactionsConverterTest.php new file mode 100644 index 0000000..e56dc67 --- /dev/null +++ b/tests/Unit/TransactionsConverterTest.php @@ -0,0 +1,394 @@ +csvEncoder = new CsvEncoder(); + $this->accountRepository = $this->getMockBuilder(QuickbooksAccountRepositoryInterface::class)->getMock(); + $serializer = new Serializer([new ObjectNormalizer()], [$this->csvEncoder]); + $this->converter = new TransactionsConverter($this->csvEncoder, $serializer, $this->accountRepository, __DIR__ . '/fixtures/accounts_mapping.json'); + + $this->accountRepository->method('findOneByName')->willReturnCallback(function ($qbUsername, $name): ?QuickbooksAccount { + $acc = new QuickbooksAccount(); + $acc->setCompany(new QuickbooksCompany($qbUsername)); + $acc->setFullName($name); + switch ($name) { + case 'HDFC HKD Savings': + $acc->setCurrency('Hong Kong Dollar'); + $acc->setAccountType(QuickbooksAccount::TYPE_BANK); + break; + case 'Bank Service Charges': + $acc->setCurrency('Hong Kong Dollar'); + $acc->setAccountType(QuickbooksAccount::TYPE_EXPENSE); + break; + case 'Citibank EUR': + $acc->setCurrency('Euro'); + $acc->setAccountType(QuickbooksAccount::TYPE_BANK); + break; + case 'Citibank USD': + $acc->setCurrency('US Dollar'); + $acc->setAccountType(QuickbooksAccount::TYPE_BANK); + break; + case 'Interest Income': + $acc->setCurrency('Hong Kong Dollar'); + $acc->setAccountType(QuickbooksAccount::TYPE_OTHER_INCOME); + break; + case 'Uncategorized Income': + $acc->setCurrency('Hong Kong Dollar'); + $acc->setAccountType(QuickbooksAccount::TYPE_INCOME); + break; + default: + return null; + } + return $acc; + }); + } + + public function testConvertExpenseHkd(): void + { + $input = [ + [ + 'Date' => '07/04/18', + 'Transaction ID' => '5ba07172d1b673a5523cff1e26a9bb2f', + 'Number' => '', + 'Description' => 'Monthly fee', + 'Notes' => '', + 'Commodity/Currency' => 'CURRENCY::HKD', + 'Void Reason' => '', + 'Action' => '', + 'Memo' => '', + 'Full Account Name' => 'Assets:Current Assets:HDFC Savings', + 'Account Name' => 'HDFC Savings', + 'Amount With Sym' => '-HK$1,200.00', + 'Amount Num' => + [ + '' => '-1,200.00', + ], + 'Reconcile' => 'n', + 'Reconcile Date' => '', + 'Rate/Price' => '1.00', + ], + [ + 'Date' => '', + 'Transaction ID' => '', + 'Number' => '', + 'Description' => '', + 'Notes' => '', + 'Commodity/Currency' => '', + 'Void Reason' => '', + 'Action' => '', + 'Memo' => '', + 'Full Account Name' => 'Expense:Bank Charges HKD', + 'Account Name' => 'Bank Charges HKD', + 'Amount With Sym' => 'HK$1,200.00', + 'Amount Num' => + [ + '' => '1,200.00', + ], + 'Reconcile' => 'n', + 'Reconcile Date' => '', + 'Rate/Price' => '1.00', + ] + ]; + + $output = $this->converter->convert($input, self::COMPANY1); + + self::assertSame([ + [ + 'txnDate' => '2018-04-07', + 'refNumber' => NULL, + 'currency' => 'Hong Kong Dollar', + 'exchangeRate' => NULL, + 'creditAccount' => 'HDFC HKD Savings', + 'creditMemo' => 'Monthly fee', + 'creditAmount' => '1,200.00', + 'debitAccount' => 'Bank Service Charges', + 'debitMemo' => 'Monthly fee', + 'debitAmount' => '1,200.00', + ] + ], $output); + } + + public function testConvertBankToBankTransferUsdToEuro(): void + { + $input = [ + [ + 'Date' => '17/01/19', + 'Transaction ID' => '3a37539cccd9465622266a2444dab907', + 'Number' => '', + 'Description' => 'Line Management / Negative Balance Balancing - 10300002-13045786-00014885 EUR ACME LIMITED', + 'Notes' => '', + 'Commodity/Currency' => 'CURRENCY::USD', + 'Void Reason' => '', + 'Action' => '', + 'Memo' => '', + 'Full Account Name' => 'Assets:Current Assets:Citibank EUR', + 'Account Name' => 'Citibank EUR', + 'Amount With Sym' => '€9.80', + 'Amount Num' => + [ + '' => '9.80', + ], + 'Reconcile' => 'n', + 'Reconcile Date' => '', + 'Rate/Price' => '1 + 32/245', + ], + [ + 'Date' => '', + 'Transaction ID' => '', + 'Number' => '', + 'Description' => '', + 'Notes' => '', + 'Commodity/Currency' => '', + 'Void Reason' => '', + 'Action' => '', + 'Memo' => '', + 'Full Account Name' => 'Assets:Current Assets:Citibank USD', + 'Account Name' => 'Citibank USD', + 'Amount With Sym' => '-$11.08', + 'Amount Num' => + [ + '' => '-11.08', + ], + 'Reconcile' => 'n', + 'Reconcile Date' => '', + 'Rate/Price' => '1.00', + ] + ]; + + $output = $this->converter->convert($input, self::COMPANY2); + + self::assertSame([ + [ + 'txnDate' => '2019-01-17', + 'refNumber' => NULL, + 'currency' => 'Euro', + 'exchangeRate' => NULL, + 'creditAccount' => 'Citibank USD', + 'creditMemo' => 'Line Management / Negative Balance Balancing - 10300002-13045786-00014885 EUR ACME LIMITED', + 'creditAmount' => '11.08', + 'debitAccount' => 'Citibank EUR', + 'debitMemo' => 'Line Management / Negative Balance Balancing - 10300002-13045786-00014885 EUR ACME LIMITED', + 'debitAmount' => '9.80', + ] + ], $output); + } + + public function testConvertUsdExpenseFromEuroAccount(): void + { + $input = [ + array ( + 'Date' => '28/12/18', + 'Transaction ID' => '9d4f8a03907dccbc2af3fea2bcc238a6', + 'Number' => '', + 'Description' => 'Account management fee', + 'Notes' => '', + 'Commodity/Currency' => 'CURRENCY::EUR', + 'Void Reason' => '', + 'Action' => '', + 'Memo' => '', + 'Full Account Name' => 'Assets:Current Assets:Citibank EUR', + 'Account Name' => 'Citibank EUR', + 'Amount With Sym' => '-€9.80', + 'Amount Num' => + array ( + '' => '-9.80', + ), + 'Reconcile' => 'n', + 'Reconcile Date' => '', + 'Rate/Price' => '1.00', + ), + array ( + 'Date' => '', + 'Transaction ID' => '', + 'Number' => '', + 'Description' => '', + 'Notes' => '', + 'Commodity/Currency' => '', + 'Void Reason' => '', + 'Action' => '', + 'Memo' => '', + 'Full Account Name' => 'Expense:Bank Charges USD', + 'Account Name' => 'Bank Charges USD', + 'Amount With Sym' => '$11.65', + 'Amount Num' => + array ( + '' => '11.65', + ), + 'Reconcile' => 'n', + 'Reconcile Date' => '', + 'Rate/Price' => '196/233', + ) + ]; + + $output = $this->converter->convert($input, self::COMPANY2); + + self::assertSame([ + [ + 'txnDate' => '2018-12-28', + 'refNumber' => NULL, + 'currency' => 'Euro', + 'exchangeRate' => NULL, + 'creditAccount' => 'Citibank EUR', + 'creditMemo' => 'Account management fee', + 'creditAmount' => '9.80', + 'debitAccount' => 'Bank Service Charges', + 'debitMemo' => 'Account management fee', + 'debitAmount' => '9.80', + ] + ], $output); + } + + public function testConvertIncomeHKD(): void + { + $input = [ + [ + 'Date' => '28/09/18', + 'Transaction ID' => '25d6e938c44fdea473fa25d0f879e4fb', + 'Number' => '', + 'Description' => 'CREDIT INTEREST', + 'Notes' => '', + 'Commodity/Currency' => 'CURRENCY::HKD', + 'Void Reason' => '', + 'Action' => '', + 'Memo' => '', + 'Full Account Name' => 'Assets:Current Assets:HDFC Savings', + 'Account Name' => 'HDFC Savings', + 'Amount With Sym' => 'HK$0.01', + 'Amount Num' => + [ + '' => '0.01', + ], + 'Reconcile' => 'n', + 'Reconcile Date' => '', + 'Rate/Price' => '1.00', + ], + [ + 'Date' => '', + 'Transaction ID' => '', + 'Number' => '', + 'Description' => '', + 'Notes' => '', + 'Commodity/Currency' => '', + 'Void Reason' => '', + 'Action' => '', + 'Memo' => '', + 'Full Account Name' => 'Income:Interest income', + 'Account Name' => 'Interest income', + 'Amount With Sym' => '$0.00', + 'Amount Num' => + [ + '' => '0.00', + ], + 'Reconcile' => 'n', + 'Reconcile Date' => '', + 'Rate/Price' => '0.00', + ] + ]; + + $output = $this->converter->convert($input, self::COMPANY1); + + self::assertSame([ + [ + 'txnDate' => '2018-09-28', + 'refNumber' => NULL, + 'currency' => 'Hong Kong Dollar', + 'exchangeRate' => NULL, + 'creditAccount' => 'Interest Income', + 'creditMemo' => 'CREDIT INTEREST', + 'creditAmount' => '0.01', + 'debitAccount' => 'HDFC HKD Savings', + 'debitMemo' => 'CREDIT INTEREST', + 'debitAmount' => '0.01', + ] + ], $output); + } + + public function testConvertIncomeUsdToHkd(): void + { + $input = [ + [ + 'Date' => '01/01/20', + 'Transaction ID' => '17fdb9fe2ec0d4a84533e96094875971', + 'Number' => '', + 'Description' => 'Withdrawal Conversion from: $173.19 USD Conversion to: $1,307.53 HKD Exchange rate: 7.5496846', + 'Notes' => '', + 'Commodity/Currency' => 'CURRENCY::USD', + 'Void Reason' => '', + 'Action' => '', + 'Memo' => '', + 'Full Account Name' => 'Assets:Current Assets:HDFC Savings', + 'Account Name' => 'HDFC Savings', + 'Amount With Sym' => 'HK$1,307.53', + 'Amount Num' => + [ + '' => '1,307.53', + ], + 'Reconcile' => 'n', + 'Reconcile Date' => '', + 'Rate/Price' => '17319/130753', + ], + [ + 'Date' => '', + 'Transaction ID' => '', + 'Number' => '', + 'Description' => '', + 'Notes' => '', + 'Commodity/Currency' => '', + 'Void Reason' => '', + 'Action' => '', + 'Memo' => '', + 'Full Account Name' => 'Assets:Current Assets:Paypal USD', + 'Account Name' => 'Paypal USD', + 'Amount With Sym' => '-$173.19', + 'Amount Num' => + [ + '' => '-173.19', + ], + 'Reconcile' => 'n', + 'Reconcile Date' => '', + 'Rate/Price' => '1', + ] + ]; + + $output = $this->converter->convert($input, self::COMPANY1); + + self::assertSame([ + [ + 'txnDate' => '2020-01-01', + 'refNumber' => NULL, + 'currency' => 'Hong Kong Dollar', + 'exchangeRate' => NULL, + 'creditAccount' => 'Uncategorized Income', + 'creditMemo' => 'Withdrawal Conversion from: $173.19 USD Conversion to: $1,307.53 HKD Exchange rate: 7.5496846', + 'creditAmount' => '1,307.53', + 'debitAccount' => 'HDFC HKD Savings', + 'debitMemo' => 'Withdrawal Conversion from: $173.19 USD Conversion to: $1,307.53 HKD Exchange rate: 7.5496846', + 'debitAmount' => '1,307.53', + ] + ], $output); + } +} diff --git a/tests/Unit/fixtures/accounts_mapping.json b/tests/Unit/fixtures/accounts_mapping.json new file mode 100644 index 0000000..0fd91bc --- /dev/null +++ b/tests/Unit/fixtures/accounts_mapping.json @@ -0,0 +1,12 @@ +{ + "acme1": { + "Assets:Current Assets:HDFC Savings": "HDFC HKD Savings", + "Income:Interest income": "Interest Income", + "Expense:Bank Charges HKD": "Bank Service Charges" + }, + "acme2": { + "Assets:Current Assets:Citibank EUR": "Citibank EUR", + "Assets:Current Assets:Citibank USD": "Citibank USD", + "Expense:Bank Charges USD": "Bank Service Charges" + } +} diff --git a/tests/bootstrap.php b/tests/bootstrap.php new file mode 100644 index 0000000..469dcce --- /dev/null +++ b/tests/bootstrap.php @@ -0,0 +1,11 @@ +bootEnv(dirname(__DIR__).'/.env'); +} diff --git a/translations/.gitignore b/translations/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/translations/messages.en.yaml b/translations/messages.en.yaml new file mode 100644 index 0000000..641b3cc --- /dev/null +++ b/translations/messages.en.yaml @@ -0,0 +1,4 @@ +# wizard buttons +import_wizard_step.upload: Upload +import_wizard_step.mapping: Mapping +import_wizard_step.confirmation: Confirmation