diff --git a/.ddev/.env.template b/.ddev/.env.template
index 7996b4c86..3ae0ab030 100644
--- a/.ddev/.env.template
+++ b/.ddev/.env.template
@@ -7,6 +7,7 @@ MYSQL_PORT=tcp://db:3306
MYSQL_DATABASE=db
MYSQL_USER=db
MYSQL_PASSWORD=db
+CORS=http://localhost:8080,http://localhost:8090,https://localhost:8091,http://localhost:3000,http://127.0.0.1:3000,https://zms.ddev.site,http://zms.ddev.site
# xdebug
PHP_IDE_CONFIG="serverName=zms.ddev.site"
\ No newline at end of file
diff --git a/.ddev/config.yaml b/.ddev/config.yaml
index fff88a9bd..918511c3f 100644
--- a/.ddev/config.yaml
+++ b/.ddev/config.yaml
@@ -143,13 +143,13 @@ timezone: Europe/Berlin
# fail_on_hook_fail: False
# Decide whether 'ddev start' should be interrupted by a failing hook
-# host_https_port: "59002"
+host_https_port: "8091"
# The host port binding for https can be explicitly specified. It is
# dynamic unless otherwise specified.
# This is not used by most people, most people use the *router* instead
# of the localhost port.
-# host_webserver_port: "59001"
+host_webserver_port: "8090"
# The host port binding for the ddev-webserver can be explicitly specified. It is
# dynamic unless otherwise specified.
# This is not used by most people, most people use the *router* instead
diff --git a/.github/workflows/build-images.yaml b/.github/workflows/build-images.yaml
index 3b913d38e..1fed03e70 100644
--- a/.github/workflows/build-images.yaml
+++ b/.github/workflows/build-images.yaml
@@ -45,6 +45,8 @@ jobs:
php_version: "8.0"
- module: zmscalldisplay
php_version: "8.0"
+ - module: zmscitizenapi
+ php_version: "8.0"
- module: zmsmessaging
php_version: "8.0"
- module: zmsstatistic
diff --git a/.github/workflows/unit-tests.yaml b/.github/workflows/unit-tests.yaml
index 79cef0eeb..2b74dca8f 100644
--- a/.github/workflows/unit-tests.yaml
+++ b/.github/workflows/unit-tests.yaml
@@ -27,6 +27,8 @@ jobs:
php_version: "8.0"
- module: zmscalldisplay
php_version: "8.0"
+ - module: zmscitizenapi
+ php_version: "8.0"
- module: zmsdldb
php_version: "8.0"
- module: zmsentities
diff --git a/.htaccess b/.htaccess
index 8826a3601..5d49aac60 100644
--- a/.htaccess
+++ b/.htaccess
@@ -39,6 +39,14 @@ RewriteRule ^terminvereinbarung/calldisplay(.*) /var/www/html/zmscalldisplay/pub
RewriteRule ^terminvereinbarung/calldisplay/+_(.*) /var/www/html/zmscalldisplay/public/_$1 [QSA]
+# zmscitizenapi
+SetEnvIf Request_URI ^/zmscitizenapi ZMS_MODULE_BASEPATH=/terminvereinbarung/api/citizen
+RewriteCond %{REQUEST_URI} !^/terminvereinbarung/api/citizen/+(_|doc)
+RewriteCond %{REQUEST_FILENAME} !-f
+RewriteRule ^terminvereinbarung/api/citizen(/.*)?$ /var/www/html/zmscitizenapi/public/index.php$1 [L]
+RewriteRule ^terminvereinbarung/api/citizen/+(doc|_)(.*) /var/www/html/zmscitizenapi/public/$1$2 [QSA]
+
+
# zmsstatistic
SetEnvIf Request_URI ^/zmsstatistic ZMS_MODULE_BASEPATH=/terminvereinbarung/statistic
RewriteCond %{REQUEST_URI} !^/terminvereinbarung/[^/]+/+_
diff --git a/cli b/cli
index b9c91682d..4f0f349c3 100755
--- a/cli
+++ b/cli
@@ -22,6 +22,7 @@ modules = [
"zmsadmin",
"zmsapi",
"zmscalldisplay",
+ "zmscitizenapi",
"zmsclient",
"zmsdb",
"zmsdldb",
diff --git a/mellon/phpunit.xml.dist b/mellon/phpunit.xml.dist
index 73763f0ef..24f093133 100644
--- a/mellon/phpunit.xml.dist
+++ b/mellon/phpunit.xml.dist
@@ -10,7 +10,7 @@
convertWarningsToExceptions="true"
processIsolation="false"
forceCoversAnnotation="false"
- stopOnFailure="true"
+ stopOnFailure="false"
bootstrap="tests/bootstrap.php"
>
diff --git a/zmsadmin/package-lock.json b/zmsadmin/package-lock.json
index 4f081e326..fafdb39ed 100644
--- a/zmsadmin/package-lock.json
+++ b/zmsadmin/package-lock.json
@@ -2892,108 +2892,6 @@
"node": ">= 8"
}
},
- "node_modules/css-select": {
- "version": "5.1.0",
- "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz",
- "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==",
- "dev": true,
- "license": "BSD-2-Clause",
- "optional": true,
- "peer": true,
- "dependencies": {
- "boolbase": "^1.0.0",
- "css-what": "^6.1.0",
- "domhandler": "^5.0.2",
- "domutils": "^3.0.1",
- "nth-check": "^2.0.1"
- },
- "funding": {
- "url": "https://github.com/sponsors/fb55"
- }
- },
- "node_modules/css-select/node_modules/dom-serializer": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
- "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "peer": true,
- "dependencies": {
- "domelementtype": "^2.3.0",
- "domhandler": "^5.0.2",
- "entities": "^4.2.0"
- },
- "funding": {
- "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
- }
- },
- "node_modules/css-select/node_modules/domhandler": {
- "version": "5.0.3",
- "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
- "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
- "dev": true,
- "license": "BSD-2-Clause",
- "optional": true,
- "peer": true,
- "dependencies": {
- "domelementtype": "^2.3.0"
- },
- "engines": {
- "node": ">= 4"
- },
- "funding": {
- "url": "https://github.com/fb55/domhandler?sponsor=1"
- }
- },
- "node_modules/css-select/node_modules/domutils": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz",
- "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==",
- "dev": true,
- "license": "BSD-2-Clause",
- "optional": true,
- "peer": true,
- "dependencies": {
- "dom-serializer": "^2.0.0",
- "domelementtype": "^2.3.0",
- "domhandler": "^5.0.3"
- },
- "funding": {
- "url": "https://github.com/fb55/domutils?sponsor=1"
- }
- },
- "node_modules/css-select/node_modules/entities": {
- "version": "4.5.0",
- "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
- "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
- "dev": true,
- "license": "BSD-2-Clause",
- "optional": true,
- "peer": true,
- "engines": {
- "node": ">=0.12"
- },
- "funding": {
- "url": "https://github.com/fb55/entities?sponsor=1"
- }
- },
- "node_modules/css-tree": {
- "version": "2.3.1",
- "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz",
- "integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "peer": true,
- "dependencies": {
- "mdn-data": "2.0.30",
- "source-map-js": "^1.0.1"
- },
- "engines": {
- "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0"
- }
- },
"node_modules/css-what": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz",
@@ -3007,48 +2905,6 @@
"url": "https://github.com/sponsors/fb55"
}
},
- "node_modules/csso": {
- "version": "5.0.5",
- "resolved": "https://registry.npmjs.org/csso/-/csso-5.0.5.tgz",
- "integrity": "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "peer": true,
- "dependencies": {
- "css-tree": "~2.2.0"
- },
- "engines": {
- "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0",
- "npm": ">=7.0.0"
- }
- },
- "node_modules/csso/node_modules/css-tree": {
- "version": "2.2.1",
- "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.2.1.tgz",
- "integrity": "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "peer": true,
- "dependencies": {
- "mdn-data": "2.0.28",
- "source-map-js": "^1.0.1"
- },
- "engines": {
- "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0",
- "npm": ">=7.0.0"
- }
- },
- "node_modules/csso/node_modules/mdn-data": {
- "version": "2.0.28",
- "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.28.tgz",
- "integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==",
- "dev": true,
- "license": "CC0-1.0",
- "optional": true,
- "peer": true
- },
"node_modules/data-view-buffer": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz",
@@ -4846,7 +4702,8 @@
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
- "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+ "dev": true
},
"node_modules/js-yaml": {
"version": "3.14.1",
@@ -5267,6 +5124,7 @@
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
+ "dev": true,
"dependencies": {
"js-tokens": "^3.0.0 || ^4.0.0"
},
@@ -5282,15 +5140,6 @@
"node": ">= 0.8.0"
}
},
- "node_modules/mdn-data": {
- "version": "2.0.30",
- "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz",
- "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==",
- "dev": true,
- "license": "CC0-1.0",
- "optional": true,
- "peer": true
- },
"node_modules/micromatch": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
@@ -5845,6 +5694,7 @@
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
+ "dev": true,
"dependencies": {
"loose-envify": "^1.1.0"
},
@@ -6339,21 +6189,6 @@
"integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==",
"dev": true
},
- "node_modules/srcset": {
- "version": "5.0.1",
- "resolved": "https://registry.npmjs.org/srcset/-/srcset-5.0.1.tgz",
- "integrity": "sha512-/P1UYbGfJVlxZag7aABNRrulEXAwCSDo7fklafOQrantuPTDmYgijJMks2zusPCVzgW9+4P69mq7w6pYuZpgxw==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "peer": true,
- "engines": {
- "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
"node_modules/ssr-window": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/ssr-window/-/ssr-window-3.0.0.tgz",
@@ -6544,34 +6379,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/svgo": {
- "version": "3.3.2",
- "resolved": "https://registry.npmjs.org/svgo/-/svgo-3.3.2.tgz",
- "integrity": "sha512-OoohrmuUlBs8B8o6MB2Aevn+pRIH9zDALSR+6hhqVfa6fRwG/Qw9VUMSMW9VNg2CFc/MTIfabtdOVl9ODIJjpw==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "peer": true,
- "dependencies": {
- "@trysound/sax": "0.2.0",
- "commander": "^7.2.0",
- "css-select": "^5.1.0",
- "css-tree": "^2.3.1",
- "css-what": "^6.1.0",
- "csso": "^5.0.5",
- "picocolors": "^1.0.0"
- },
- "bin": {
- "svgo": "bin/svgo"
- },
- "engines": {
- "node": ">=14.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/svgo"
- }
- },
"node_modules/swiper": {
"version": "6.8.4",
"resolved": "https://registry.npmjs.org/swiper/-/swiper-6.8.4.tgz",
diff --git a/zmsadmin/tests/Zmsadmin/fixtures/GET_processList_empty.json b/zmsadmin/tests/Zmsadmin/fixtures/GET_processList_empty.json
index 70a75536d..ebd499321 100644
--- a/zmsadmin/tests/Zmsadmin/fixtures/GET_processList_empty.json
+++ b/zmsadmin/tests/Zmsadmin/fixtures/GET_processList_empty.json
@@ -4,7 +4,7 @@
"$schema": "https://schema.berlin.de/queuemanagement/metaresult.json",
"error": false,
"generated": "2019-02-11T16:07:58+01:00",
- "server": "Zmsapi-ENV (v2.19.02-38-g0ba2cba)"
+ "server": "Zmsapi"
},
"data": []
}
\ No newline at end of file
diff --git a/zmsadmin/tests/Zmsadmin/fixtures/GET_processList_fake_entry.json b/zmsadmin/tests/Zmsadmin/fixtures/GET_processList_fake_entry.json
index b77ee7d25..2917b18ea 100644
--- a/zmsadmin/tests/Zmsadmin/fixtures/GET_processList_fake_entry.json
+++ b/zmsadmin/tests/Zmsadmin/fixtures/GET_processList_fake_entry.json
@@ -4,7 +4,7 @@
"$schema": "https://schema.berlin.de/queuemanagement/metaresult.json",
"error": false,
"generated": "2019-02-11T16:07:58+01:00",
- "server": "Zmsapi-ENV (v2.19.02-38-g0ba2cba)"
+ "server": "Zmsapi"
},
"data": [
{
diff --git a/zmsadmin/tests/Zmsadmin/fixtures/GET_source_unittest.json b/zmsadmin/tests/Zmsadmin/fixtures/GET_source_unittest.json
index bee3c0341..bb0f11de7 100644
--- a/zmsadmin/tests/Zmsadmin/fixtures/GET_source_unittest.json
+++ b/zmsadmin/tests/Zmsadmin/fixtures/GET_source_unittest.json
@@ -4,7 +4,7 @@
"$schema": "https://schema.berlin.de/queuemanagement/metaresult.json",
"error": false,
"generated": "2019-02-08T14:45:15+01:00",
- "server": "Zmsapi-ENV (v2.19.02-38-g0ba2cba)"
+ "server": "Zmsapi"
},
"data": {
"$schema": "https://schema.berlin.de/queuemanagement/source.json",
diff --git a/zmsapi/public/doc/README.md b/zmsapi/public/doc/README.md
index 69dbddfdb..5fba8c591 100644
--- a/zmsapi/public/doc/README.md
+++ b/zmsapi/public/doc/README.md
@@ -1,6 +1,22 @@
## How does the Open Api definition work
## Version 2.0
+```
+bin/configure
+npm i
+npm run build
+npm run doc
+swagger-cli bundle -o public/doc/swagger.json public/doc/swagger.yaml
+python3 -m http.server 8001
+```
+
+Reachable at:
+```
+http://[::]:8001/public/doc/
+https://zms.ddev.site/terminvereinbarung/api/2/doc/index.html
+https://it-at-m.github.io/eappointment/zmsapi/public/doc/index.html
+```
+
* Under /public/doc are the schema from zmsentities. A symbolic link points to the corresponding folder under vendor/eappointment/zmsentities/schema.
* Under /bin there is a build_swagger.js file. This is executed via ``npm run doc`` and validates the existing swagger.yaml file. If valid, the open api annotations are read from routing.php and the remaining information such as info, definitions, version and tags are compiled from the yaml files under ./partials into a complete swagger.yaml.
@@ -9,8 +25,4 @@
* To access all paths resolved via redoc or the open api documentation, a resolved swagger.json must be created from the swagger.yaml. This is done via the swagger cli with a call to ``bin/doc``. This call executes the above npm command ``npm run doc`` and subsequently creates a full swagger.json.
-To render the open-api doc by redoc and swagger, appropriate files such as swagger-ui files are fetched in the CI process and stored at https://eappointment.gitlab.io/zmsapi/.
-
-* If a new entity definition should be added, the reference must be set here under definitions.
-
-Translated with www.DeepL.com/Translator (free version)
\ No newline at end of file
+To render the open-api doc by redoc and swagger, appropriate files such as swagger-ui files are fetched in the CI process and stored at https://eappointment.gitlab.io/zmsapi/.
\ No newline at end of file
diff --git a/zmsapi/src/Zmsapi/Exception/Process/ProcessNotFound.php b/zmsapi/src/Zmsapi/Exception/Process/ProcessNotFound.php
index 933fa911c..1e2d86f6a 100644
--- a/zmsapi/src/Zmsapi/Exception/Process/ProcessNotFound.php
+++ b/zmsapi/src/Zmsapi/Exception/Process/ProcessNotFound.php
@@ -2,9 +2,6 @@
namespace BO\Zmsapi\Exception\Process;
-/**
- * example class to generate an exception
- */
class ProcessNotFound extends \Exception
{
protected $code = 404;
diff --git a/zmscalldisplay/package-lock.json b/zmscalldisplay/package-lock.json
index 5d6c23ed9..859ab3d8d 100644
--- a/zmscalldisplay/package-lock.json
+++ b/zmscalldisplay/package-lock.json
@@ -2682,108 +2682,6 @@
"postcss": "^8.0.9"
}
},
- "node_modules/css-select": {
- "version": "5.1.0",
- "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz",
- "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==",
- "dev": true,
- "license": "BSD-2-Clause",
- "optional": true,
- "peer": true,
- "dependencies": {
- "boolbase": "^1.0.0",
- "css-what": "^6.1.0",
- "domhandler": "^5.0.2",
- "domutils": "^3.0.1",
- "nth-check": "^2.0.1"
- },
- "funding": {
- "url": "https://github.com/sponsors/fb55"
- }
- },
- "node_modules/css-select/node_modules/dom-serializer": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
- "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "peer": true,
- "dependencies": {
- "domelementtype": "^2.3.0",
- "domhandler": "^5.0.2",
- "entities": "^4.2.0"
- },
- "funding": {
- "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
- }
- },
- "node_modules/css-select/node_modules/domhandler": {
- "version": "5.0.3",
- "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
- "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
- "dev": true,
- "license": "BSD-2-Clause",
- "optional": true,
- "peer": true,
- "dependencies": {
- "domelementtype": "^2.3.0"
- },
- "engines": {
- "node": ">= 4"
- },
- "funding": {
- "url": "https://github.com/fb55/domhandler?sponsor=1"
- }
- },
- "node_modules/css-select/node_modules/domutils": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz",
- "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==",
- "dev": true,
- "license": "BSD-2-Clause",
- "optional": true,
- "peer": true,
- "dependencies": {
- "dom-serializer": "^2.0.0",
- "domelementtype": "^2.3.0",
- "domhandler": "^5.0.3"
- },
- "funding": {
- "url": "https://github.com/fb55/domutils?sponsor=1"
- }
- },
- "node_modules/css-select/node_modules/entities": {
- "version": "4.5.0",
- "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
- "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
- "dev": true,
- "license": "BSD-2-Clause",
- "optional": true,
- "peer": true,
- "engines": {
- "node": ">=0.12"
- },
- "funding": {
- "url": "https://github.com/fb55/entities?sponsor=1"
- }
- },
- "node_modules/css-tree": {
- "version": "2.3.1",
- "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz",
- "integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "peer": true,
- "dependencies": {
- "mdn-data": "2.0.30",
- "source-map-js": "^1.0.1"
- },
- "engines": {
- "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0"
- }
- },
"node_modules/css-what": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz",
@@ -2885,48 +2783,6 @@
"postcss": "^8.2.15"
}
},
- "node_modules/csso": {
- "version": "5.0.5",
- "resolved": "https://registry.npmjs.org/csso/-/csso-5.0.5.tgz",
- "integrity": "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "peer": true,
- "dependencies": {
- "css-tree": "~2.2.0"
- },
- "engines": {
- "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0",
- "npm": ">=7.0.0"
- }
- },
- "node_modules/csso/node_modules/css-tree": {
- "version": "2.2.1",
- "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.2.1.tgz",
- "integrity": "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "peer": true,
- "dependencies": {
- "mdn-data": "2.0.28",
- "source-map-js": "^1.0.1"
- },
- "engines": {
- "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0",
- "npm": ">=7.0.0"
- }
- },
- "node_modules/csso/node_modules/mdn-data": {
- "version": "2.0.28",
- "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.28.tgz",
- "integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==",
- "dev": true,
- "license": "CC0-1.0",
- "optional": true,
- "peer": true
- },
"node_modules/data-view-buffer": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz",
@@ -4787,15 +4643,6 @@
"integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==",
"dev": true
},
- "node_modules/mdn-data": {
- "version": "2.0.30",
- "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz",
- "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==",
- "dev": true,
- "license": "CC0-1.0",
- "optional": true,
- "peer": true
- },
"node_modules/micromatch": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
@@ -6139,21 +5986,6 @@
"integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==",
"dev": true
},
- "node_modules/srcset": {
- "version": "5.0.1",
- "resolved": "https://registry.npmjs.org/srcset/-/srcset-5.0.1.tgz",
- "integrity": "sha512-/P1UYbGfJVlxZag7aABNRrulEXAwCSDo7fklafOQrantuPTDmYgijJMks2zusPCVzgW9+4P69mq7w6pYuZpgxw==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "peer": true,
- "engines": {
- "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
"node_modules/stable": {
"version": "0.1.8",
"resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz",
@@ -6298,34 +6130,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/svgo": {
- "version": "3.3.2",
- "resolved": "https://registry.npmjs.org/svgo/-/svgo-3.3.2.tgz",
- "integrity": "sha512-OoohrmuUlBs8B8o6MB2Aevn+pRIH9zDALSR+6hhqVfa6fRwG/Qw9VUMSMW9VNg2CFc/MTIfabtdOVl9ODIJjpw==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "peer": true,
- "dependencies": {
- "@trysound/sax": "0.2.0",
- "commander": "^7.2.0",
- "css-select": "^5.1.0",
- "css-tree": "^2.3.1",
- "css-what": "^6.1.0",
- "csso": "^5.0.5",
- "picocolors": "^1.0.0"
- },
- "bin": {
- "svgo": "bin/svgo"
- },
- "engines": {
- "node": ">=14.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/svgo"
- }
- },
"node_modules/table": {
"version": "6.8.2",
"resolved": "https://registry.npmjs.org/table/-/table-6.8.2.tgz",
diff --git a/zmscalldisplay/phpunit.xml b/zmscalldisplay/phpunit.xml
index aae7f1a39..d4fd86860 100644
--- a/zmscalldisplay/phpunit.xml
+++ b/zmscalldisplay/phpunit.xml
@@ -10,7 +10,7 @@
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
forceCoversAnnotation="false"
- stopOnFailure="true"
+ stopOnFailure="false"
verbose="true"
processIsolation="false"
>
diff --git a/zmscitizenapi/.gitignore b/zmscitizenapi/.gitignore
new file mode 100644
index 000000000..dbf690fac
--- /dev/null
+++ b/zmscitizenapi/.gitignore
@@ -0,0 +1,17 @@
+node_modules
+vendor
+vendor.zmsbase
+config.php
+tags
+.DS_*
+public/_tests
+public/doc/assets/redoc.min.js
+public/doc/swagger.*
+public/_test/assets/swagger-ui-bundle.js
+public/_test/assets/swagger-ui.css
+public/_test/assets/swagger-ui-standalone-preset.js
+cache
+vendor.old
+vendor.update
+VERSION
+.phpunit.result.cache
\ No newline at end of file
diff --git a/zmscitizenapi/Makefile b/zmscitizenapi/Makefile
new file mode 100644
index 000000000..896703b0a
--- /dev/null
+++ b/zmscitizenapi/Makefile
@@ -0,0 +1,32 @@
+.PHONY: help now dev live watch fix openapi coverage paratest
+
+help: # This help
+ @echo "Possible Targets:"
+ @grep -P "^\w+:" Makefile|sort|perl -pe 's/^(\w+):([^\#]+)(\#\s*(.*))?/ \1\n\t\4\n/'
+
+now: # Dummy target
+
+dev: # init development system
+ COMPOSER=composer.json composer update
+ npm install
+
+live: # init live system, delete unnecessary libs
+ composer install --no-dev --prefer-dist
+ bin/configure
+ npm install
+ npm run build
+ npm run doc
+ npx swagger-cli bundle -o public/doc/swagger.json public/doc/swagger.yaml
+
+fix: # run code fixing
+ php vendor/bin/phpcbf --standard=psr2 src/
+ php vendor/bin/phpcbf --standard=psr2 tests/
+
+openapi: # Swagger docs on changes
+ ./bin/doc
+
+coverage:
+ php vendor/bin/phpunit --coverage-html public/_tests/coverage/
+
+paratest: # init parallel unit testing with 5 processes
+ vendor/bin/paratest -p20 --coverage-html public/_tests/coverage/
diff --git a/zmscitizenapi/bin/build_swagger.js b/zmscitizenapi/bin/build_swagger.js
new file mode 100755
index 000000000..4529bb3d3
--- /dev/null
+++ b/zmscitizenapi/bin/build_swagger.js
@@ -0,0 +1,89 @@
+var fs = require('fs');
+const swaggerParser = require('swagger-parser');
+const swaggerJsdoc = require('swagger-jsdoc');
+const yaml = require('js-yaml');
+
+const options = {
+ definition: {
+ openapi: '2.0.0',
+ info: {
+ version: readApiVersion(),
+ title: "ZMS API"
+ },
+ },
+ apis: ['./routing.php']
+ };
+
+const openapiSpecification = swaggerJsdoc(options);
+
+buildSwagger();
+validateSwagger();
+
+function validateSwagger() {
+
+ fs.stat('public/doc/swagger.yaml', function(error, stats) {
+ var routessize = stats.size;
+ if (error) {
+ console.log(error);
+ } else {
+ console.log("Found public/doc/swagger.yaml with " + routessize + " bytes");
+ }
+
+ swaggerParser.validate('public/doc/swagger.yaml', (err, api) => {
+ if (err) {
+ console.error(err);
+ }
+ else {
+ console.log("Validated API %s, Version: %s", api.info.title, api.info.version);
+ }
+ })
+ });
+}
+
+function buildSwagger() {
+ let version = readFileContent('public/doc/partials/version.yaml') + "\n";
+ let info = readFileContent('public/doc/partials/info.yaml');
+ //append current api version to info
+ info = info + "\n version: '" + readFileContent("./VERSION").trim() + "'\n";
+
+ let basics = readFileContent('public/doc/partials/basic.yaml') + "\n";
+ let paths = {
+ paths:
+ openapiSpecification.paths,
+ }
+ let tags = readFileContent('public/doc/partials/tags.yaml');
+ let definitions = readFileContent('public/doc/partials/definitions.yaml');
+ writeSwaggerFile(version + info + basics + tags + yaml.dump(paths) + definitions)
+
+}
+
+function writeSwaggerFile(data)
+{
+ try {
+ fs.writeFileSync('public/doc/swagger.yaml', data, 'utf8');
+ console.log("Build new swagger file successfully!");
+ } catch (e) {
+ console.log(e);
+ }
+}
+
+function readFileContent(file) {
+ try {
+ const data = fs.readFileSync(file, 'utf8');
+ return data;
+ } catch (e) {
+ console.log(e);
+ }
+}
+
+function readApiVersion() {
+ fs.readFile('./VERSION', 'utf8' , (err, data) => {
+ if (err) {
+ console.error(err)
+ return
+ }
+ return data;
+ })
+}
+
+
diff --git a/zmscitizenapi/bin/configure b/zmscitizenapi/bin/configure
new file mode 100755
index 000000000..9b59a7b79
--- /dev/null
+++ b/zmscitizenapi/bin/configure
@@ -0,0 +1,28 @@
+#!/bin/bash
+
+REALPATH=$(which realpath || which readlink)
+REALPATH=$([[ "$REALPATH" =~ 'readlink' ]] && echo "$REALPATH -e" || echo "$REALPATH")
+DIR=$(dirname $($REALPATH ${BASH_SOURCE[0]}))
+ROOT=$($REALPATH $DIR/..)
+
+#echo "Get DLDB Exports"
+#$ROOT/bin/getDldbExport
+
+echo -n "Configuring application"
+mkdir -p $ROOT/.git/hooks
+ln -sf $ROOT/bin/test $ROOT/.git/hooks/pre-commit
+ln -sf $ROOT/bin/configure $ROOT/.git/hooks/post-checkout
+ln -sf $ROOT/bin/configure $ROOT/.git/hooks/post-commit
+ln -sf $ROOT/bin/configure $ROOT/.git/hooks/post-merge
+
+if [ ! -e $ROOT/config.php ]
+then
+ cp $ROOT/config.example.php $ROOT/config.php
+fi
+test -d $ROOT/cache && chmod -fR a+rwX $ROOT/cache || echo "Could not chmod cache files"
+
+
+#VERSION=`git symbolic-ref -q --short HEAD || git describe --tags --exact-match`
+VERSION=`git describe --tags --always`
+echo $VERSION > $ROOT/VERSION
+echo " $VERSION"
\ No newline at end of file
diff --git a/zmscitizenapi/bootstrap.php b/zmscitizenapi/bootstrap.php
new file mode 100644
index 000000000..ae9b74f6b
--- /dev/null
+++ b/zmscitizenapi/bootstrap.php
@@ -0,0 +1,77 @@
+addBodyParsingMiddleware();
+
+\App::$http = new \BO\Zmsclient\Http(\App::ZMS_API_URL);
+//\BO\Zmsclient\Psr7\Client::$curlopt = \App::$http_curl_config;
+
+$errorMiddleware = \App::$slim->getContainer()->get('errorMiddleware');
+$errorMiddleware->setDefaultErrorHandler(new \BO\Zmscitizenapi\Helper\ErrorHandler());
+
+// Initialize cache for rate limiting
+$cache = new \Symfony\Component\Cache\Psr16Cache(
+ new \Symfony\Component\Cache\Adapter\FilesystemAdapter()
+);
+
+
+$logger = new LoggerService();
+// Security middleware (order is important)
+App::$slim->add(new \BO\Zmscitizenapi\Middleware\RequestLoggingMiddleware($logger));
+App::$slim->add(new \BO\Zmscitizenapi\Middleware\SecurityHeadersMiddleware($logger));
+App::$slim->add(new \BO\Zmscitizenapi\Middleware\CorsMiddleware($logger));
+App::$slim->add(new \BO\Zmscitizenapi\Middleware\CsrfMiddleware($logger));
+App::$slim->add(new \BO\Zmscitizenapi\Middleware\RateLimitingMiddleware($cache, $logger));
+App::$slim->add(new \BO\Zmscitizenapi\Middleware\RequestSanitizerMiddleware($logger));
+App::$slim->add(new \BO\Zmscitizenapi\Middleware\RequestSizeLimitMiddleware($logger));
+App::$slim->add(new \BO\Zmscitizenapi\Middleware\IpFilterMiddleware($logger));
+
+// Add handler for Method Not Allowed
+$errorMiddleware->setErrorHandler(
+ \Slim\Exception\HttpMethodNotAllowedException::class,
+ function (
+ \Psr\Http\Message\ServerRequestInterface $request,
+ \Throwable $exception
+ ) {
+ $response = \App::$slim->getResponseFactory()->createResponse();
+ $response = $response->withStatus(405)
+ ->withHeader('Content-Type', 'application/json');
+
+ $responseBody = json_encode([
+ 'errors' => [
+ \BO\Zmscitizenapi\Localization\ErrorMessages::get('requestMethodNotAllowed')
+ ]
+ ]);
+
+ error_log($responseBody);
+
+ $response->getBody()->write($responseBody);
+ return $response;
+ }
+);
+
+// load routing
+\BO\Slim\Bootstrap::loadRouting(\App::APP_PATH . '/routing.php');
diff --git a/zmscitizenapi/composer.json b/zmscitizenapi/composer.json
new file mode 100644
index 000000000..b044bc226
--- /dev/null
+++ b/zmscitizenapi/composer.json
@@ -0,0 +1,55 @@
+{
+ "name": "eappointment/zmscitizenapi",
+ "description": "This application offers a REST-like interface for citizens on the internet.",
+ "license": "EUPL-1.2",
+ "authors": [],
+ "repositories": [
+ {
+ "type": "path",
+ "url": "../*",
+ "options": {
+ "symlink": true
+ }
+ }
+ ],
+ "config": {
+ "platform": {
+ "php": "8.0.2"
+ },
+ "allow-plugins": {
+ "php-http/discovery": true
+ }
+ },
+ "require-dev": {
+ "phpmd/phpmd": "@stable",
+ "squizlabs/php_codesniffer": "^3.7",
+ "phpunit/phpunit": "^9.5.4",
+ "helmich/phpunit-psr7-assert": "^4.3.0",
+ "phpspec/prophecy-phpunit": "^2.0.0"
+ },
+ "require": {
+ "eappointment/mellon": "@dev",
+ "eappointment/zmsslim": "@dev",
+ "eappointment/zmsclient": "@dev",
+ "eappointment/zmsentities": "@dev",
+ "symfony/cache": "^6.0",
+ "psr/simple-cache": "^3.0"
+ },
+ "scripts": {
+ "clean": "rm -f public/doc/assets/*.* && rm -f public/_test/assets/*.*",
+ "command": "bin/configure",
+ "post-install-cmd": [
+ "@wget-files"
+ ],
+ "post-update-cmd": [
+ "@wget-files"
+ ]
+ },
+ "bin": [],
+ "autoload": {
+ "psr-4": {
+ "BO\\Zmscitizenapi\\": "src/Zmscitizenapi/",
+ "BO\\Zmscitizenapi\\Tests\\": "tests/Zmscitizenapi/"
+ }
+ }
+}
diff --git a/zmscitizenapi/composer.lock b/zmscitizenapi/composer.lock
new file mode 100644
index 000000000..ca402f35f
--- /dev/null
+++ b/zmscitizenapi/composer.lock
@@ -0,0 +1,7126 @@
+{
+ "_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": "081a28bb42348afcfeb3908f7a84c0bd",
+ "packages": [
+ {
+ "name": "aronduby/dump",
+ "version": "0.9.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/aronduby/dump.git",
+ "reference": "8b3eb9b54248d520e7ad2f9f1295e2862056fb28"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/aronduby/dump/zipball/8b3eb9b54248d520e7ad2f9f1295e2862056fb28",
+ "reference": "8b3eb9b54248d520e7ad2f9f1295e2862056fb28",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "": "src/",
+ "D\\": "src/D/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "authors": [
+ {
+ "name": "Aron Duby",
+ "email": "aron.duby@gmail.com"
+ }
+ ],
+ "description": "D::ump - a PHP 5.4 print_r/var_dump replacement base on Krumo",
+ "keywords": [
+ "debug",
+ "debugging",
+ "dump",
+ "krumo",
+ "pretty",
+ "print",
+ "print_r",
+ "var_dump"
+ ],
+ "support": {
+ "issues": "https://github.com/aronduby/dump/issues",
+ "source": "https://github.com/aronduby/dump/tree/0.9.1"
+ },
+ "time": "2016-09-30T05:02:34+00:00"
+ },
+ {
+ "name": "clue/stream-filter",
+ "version": "v1.7.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/clue/stream-filter.git",
+ "reference": "049509fef80032cb3f051595029ab75b49a3c2f7"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/clue/stream-filter/zipball/049509fef80032cb3f051595029ab75b49a3c2f7",
+ "reference": "049509fef80032cb3f051595029ab75b49a3c2f7",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36"
+ },
+ "type": "library",
+ "autoload": {
+ "files": [
+ "src/functions_include.php"
+ ],
+ "psr-4": {
+ "Clue\\StreamFilter\\": "src/"
+ }
+ },
+ "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/stream-filter",
+ "keywords": [
+ "bucket brigade",
+ "callback",
+ "filter",
+ "php_user_filter",
+ "stream",
+ "stream_filter_append",
+ "stream_filter_register"
+ ],
+ "support": {
+ "issues": "https://github.com/clue/stream-filter/issues",
+ "source": "https://github.com/clue/stream-filter/tree/v1.7.0"
+ },
+ "funding": [
+ {
+ "url": "https://clue.engineering/support",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/clue",
+ "type": "github"
+ }
+ ],
+ "time": "2023-12-20T15:40:13+00:00"
+ },
+ {
+ "name": "eappointment/mellon",
+ "version": "dev-feature-MPDZBS-877-zmscitizenapi",
+ "dist": {
+ "type": "path",
+ "url": "../mellon",
+ "reference": "faff0b320f53873e8ba4716a8d5b07c931c15c8b"
+ },
+ "require": {
+ "ext-json": ">=0",
+ "ext-pcre": ">=0",
+ "php": ">=7.0.0"
+ },
+ "require-dev": {
+ "phpmd/phpmd": "^2.8.0",
+ "phpunit/phpunit": "^9.5.4",
+ "roave/security-advisories": "dev-latest",
+ "squizlabs/php_codesniffer": "*"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "BO\\Mellon\\": "src/Mellon/"
+ }
+ },
+ "scripts": {
+ "test": [
+ "php -dzend_extension=xdebug.so vendor/bin/phpunit --coverage-html coverage/"
+ ],
+ "command": [
+ "bin/configure"
+ ]
+ },
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Mathias Fischer",
+ "email": "mathias.fischer@berlinonline.de"
+ }
+ ],
+ "description": "Validator for parameters and validation helper",
+ "transport-options": {
+ "symlink": true,
+ "relative": true
+ }
+ },
+ {
+ "name": "eappointment/zmsclient",
+ "version": "dev-feature-MPDZBS-877-zmscitizenapi",
+ "dist": {
+ "type": "path",
+ "url": "../zmsclient",
+ "reference": "249f1aa96e13e3c2379353bf33539f0174fced53"
+ },
+ "require": {
+ "aronduby/dump": "^0.9",
+ "eappointment/mellon": "@dev",
+ "eappointment/zmsentities": "@dev",
+ "eappointment/zmsslim": "@dev",
+ "ext-curl": ">=0",
+ "ext-json": ">=1.0",
+ "ext-mbstring": ">=0",
+ "ext-pcre": ">=0",
+ "php": ">=7.3.0",
+ "php-http/curl-client": "^2.2",
+ "psr/http-message": "^1.0",
+ "slim/psr7": "^1.5",
+ "tracy/tracy": "^2.9",
+ "twig/twig": "3.*"
+ },
+ "require-dev": {
+ "helmich/phpunit-psr7-assert": "^4.3.0",
+ "phpmd/phpmd": "@stable",
+ "phpspec/prophecy-phpunit": "^2.0.0",
+ "phpunit/phpunit": "^9.5.4",
+ "squizlabs/php_codesniffer": "^3.5.8"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "BO\\Zmsclient\\": "src/Zmsclient/",
+ "BO\\Zmsclient\\Tests\\": "tests/Zmsclient/"
+ }
+ },
+ "scripts": {
+ "command": [
+ "bin/configure"
+ ]
+ },
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Mathias Fischer",
+ "email": "mathias.fischer@berlinonline.de"
+ }
+ ],
+ "description": "Use this library to fetch data from the eappointment API via HTTP.",
+ "transport-options": {
+ "symlink": true,
+ "relative": true
+ }
+ },
+ {
+ "name": "eappointment/zmsentities",
+ "version": "dev-feature-MPDZBS-877-zmscitizenapi",
+ "dist": {
+ "type": "path",
+ "url": "../zmsentities",
+ "reference": "faff0b320f53873e8ba4716a8d5b07c931c15c8b"
+ },
+ "require": {
+ "eappointment/mellon": "@dev",
+ "ext-curl": ">=0",
+ "ext-json": ">=1.0",
+ "ext-mbstring": ">=0",
+ "ext-pcre": ">=0",
+ "giggsey/libphonenumber-for-php": "^8.8.4",
+ "league/html-to-markdown": "^5.0",
+ "league/json-guard": "^1.0",
+ "php": ">=7.3.0",
+ "symfony/translation": "^5.4",
+ "symfony/twig-bridge": "^5.4",
+ "twig/intl-extra": "^3.4",
+ "twig/twig": "3.*"
+ },
+ "require-dev": {
+ "helmich/phpunit-psr7-assert": "^4.3.0",
+ "league/json-reference": "^1.0",
+ "phpmd/phpmd": "@stable",
+ "phpunit/phpunit": "^9.6",
+ "squizlabs/php_codesniffer": "^3.5.8",
+ "yuloh/json-guard-cli": "^0.3.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "BO\\Zmsentities\\": "src/Zmsentities/",
+ "BO\\Zmsentities\\Tests\\": "tests/Zmsentities/"
+ }
+ },
+ "scripts": {
+ "command": [
+ "bin/configure"
+ ]
+ },
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Mathias Fischer",
+ "email": "mathias.fischer@berlinonline.de"
+ }
+ ],
+ "description": "Entity definitions for eappoinment",
+ "transport-options": {
+ "symlink": true,
+ "relative": true
+ }
+ },
+ {
+ "name": "eappointment/zmsslim",
+ "version": "dev-feature-MPDZBS-877-zmscitizenapi",
+ "dist": {
+ "type": "path",
+ "url": "../zmsslim",
+ "reference": "faff0b320f53873e8ba4716a8d5b07c931c15c8b"
+ },
+ "require": {
+ "eappointment/mellon": "@dev",
+ "ext-json": "*",
+ "ext-posix": "*",
+ "monolog/monolog": "1.*",
+ "php": ">=7.3.0",
+ "slim/http-cache": "1.*",
+ "slim/psr7": "^1.5",
+ "slim/slim": "4.*",
+ "slim/twig-view": "3.*",
+ "stevenmaguire/oauth2-keycloak": "^4.0",
+ "symfony/translation": "^5.2",
+ "symfony/twig-bridge": "^5.2",
+ "tracy/tracy": "^2.9",
+ "twig/twig": "3.*"
+ },
+ "provide": {
+ "psr/container-implementation": "2.0"
+ },
+ "require-dev": {
+ "helmich/phpunit-psr7-assert": "^4.3.0",
+ "phpmd/phpmd": "@stable",
+ "phpunit/phpunit": "^9.5.4",
+ "squizlabs/php_codesniffer": "*"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "BO\\Slim\\": "src/Slim/",
+ "BO\\Slim\\Tests\\": "tests/Slim/"
+ }
+ },
+ "scripts": {
+ "command": [
+ "bin/configure"
+ ]
+ },
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Mathias Fischer",
+ "email": "mathias.fischer@berlinonline.de"
+ }
+ ],
+ "description": "Basic configuration for a slim framework",
+ "transport-options": {
+ "symlink": true,
+ "relative": true
+ }
+ },
+ {
+ "name": "fig/http-message-util",
+ "version": "1.1.5",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/http-message-util.git",
+ "reference": "9d94dc0154230ac39e5bf89398b324a86f63f765"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/http-message-util/zipball/9d94dc0154230ac39e5bf89398b324a86f63f765",
+ "reference": "9d94dc0154230ac39e5bf89398b324a86f63f765",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^5.3 || ^7.0 || ^8.0"
+ },
+ "suggest": {
+ "psr/http-message": "The package containing the PSR-7 interfaces"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.1.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Fig\\Http\\Message\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "https://www.php-fig.org/"
+ }
+ ],
+ "description": "Utility classes and constants for use with PSR-7 (psr/http-message)",
+ "keywords": [
+ "http",
+ "http-message",
+ "psr",
+ "psr-7",
+ "request",
+ "response"
+ ],
+ "support": {
+ "issues": "https://github.com/php-fig/http-message-util/issues",
+ "source": "https://github.com/php-fig/http-message-util/tree/1.1.5"
+ },
+ "time": "2020-11-24T22:02:12+00:00"
+ },
+ {
+ "name": "firebase/php-jwt",
+ "version": "v6.10.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/firebase/php-jwt.git",
+ "reference": "a49db6f0a5033aef5143295342f1c95521b075ff"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/firebase/php-jwt/zipball/a49db6f0a5033aef5143295342f1c95521b075ff",
+ "reference": "a49db6f0a5033aef5143295342f1c95521b075ff",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.4||^8.0"
+ },
+ "require-dev": {
+ "guzzlehttp/guzzle": "^6.5||^7.4",
+ "phpspec/prophecy-phpunit": "^2.0",
+ "phpunit/phpunit": "^9.5",
+ "psr/cache": "^1.0||^2.0",
+ "psr/http-client": "^1.0",
+ "psr/http-factory": "^1.0"
+ },
+ "suggest": {
+ "ext-sodium": "Support EdDSA (Ed25519) signatures",
+ "paragonie/sodium_compat": "Support EdDSA (Ed25519) signatures when libsodium is not present"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Firebase\\JWT\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Neuman Vong",
+ "email": "neuman+pear@twilio.com",
+ "role": "Developer"
+ },
+ {
+ "name": "Anant Narayanan",
+ "email": "anant@php.net",
+ "role": "Developer"
+ }
+ ],
+ "description": "A simple library to encode and decode JSON Web Tokens (JWT) in PHP. Should conform to the current spec.",
+ "homepage": "https://github.com/firebase/php-jwt",
+ "keywords": [
+ "jwt",
+ "php"
+ ],
+ "support": {
+ "issues": "https://github.com/firebase/php-jwt/issues",
+ "source": "https://github.com/firebase/php-jwt/tree/v6.10.0"
+ },
+ "time": "2023-12-01T16:26:39+00:00"
+ },
+ {
+ "name": "giggsey/libphonenumber-for-php",
+ "version": "8.13.36",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/giggsey/libphonenumber-for-php.git",
+ "reference": "9ca4179e4332d21578cb29f0c0406f0a2b8803e3"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/giggsey/libphonenumber-for-php/zipball/9ca4179e4332d21578cb29f0c0406f0a2b8803e3",
+ "reference": "9ca4179e4332d21578cb29f0c0406f0a2b8803e3",
+ "shasum": ""
+ },
+ "require": {
+ "giggsey/locale": "^1.7|^2.0",
+ "php": ">=5.3.2",
+ "symfony/polyfill-mbstring": "^1.17"
+ },
+ "replace": {
+ "giggsey/libphonenumber-for-php-lite": "self.version"
+ },
+ "require-dev": {
+ "pear/pear-core-minimal": "^1.9",
+ "pear/pear_exception": "^1.0",
+ "pear/versioncontrol_git": "^0.5",
+ "phing/phing": "^2.7",
+ "php-coveralls/php-coveralls": "^1.0|^2.0",
+ "symfony/console": "^2.8|^3.0|^v4.4|^v5.2",
+ "symfony/phpunit-bridge": "^4.2 || ^5"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "8.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "libphonenumber\\": "src/"
+ },
+ "exclude-from-classmap": [
+ "/src/data/",
+ "/src/carrier/data/",
+ "/src/geocoding/data/",
+ "/src/timezone/data/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "Apache-2.0"
+ ],
+ "authors": [
+ {
+ "name": "Joshua Gigg",
+ "email": "giggsey@gmail.com",
+ "homepage": "https://giggsey.com/"
+ }
+ ],
+ "description": "PHP Port of Google's libphonenumber",
+ "homepage": "https://github.com/giggsey/libphonenumber-for-php",
+ "keywords": [
+ "geocoding",
+ "geolocation",
+ "libphonenumber",
+ "mobile",
+ "phonenumber",
+ "validation"
+ ],
+ "support": {
+ "issues": "https://github.com/giggsey/libphonenumber-for-php/issues",
+ "source": "https://github.com/giggsey/libphonenumber-for-php"
+ },
+ "time": "2024-05-03T06:27:03+00:00"
+ },
+ {
+ "name": "giggsey/locale",
+ "version": "2.6",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/giggsey/Locale.git",
+ "reference": "37874fa473131247c348059fb7b8985efc18b5ea"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/giggsey/Locale/zipball/37874fa473131247c348059fb7b8985efc18b5ea",
+ "reference": "37874fa473131247c348059fb7b8985efc18b5ea",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2"
+ },
+ "require-dev": {
+ "ext-json": "*",
+ "pear/pear-core-minimal": "^1.9",
+ "pear/pear_exception": "^1.0",
+ "pear/versioncontrol_git": "^0.5",
+ "phing/phing": "^2.7",
+ "php-coveralls/php-coveralls": "^2.0",
+ "phpunit/phpunit": "^8.5|^9.5",
+ "symfony/console": "^5.0|^6.0",
+ "symfony/filesystem": "^5.0|^6.0",
+ "symfony/finder": "^5.0|^6.0",
+ "symfony/process": "^5.0|^6.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Giggsey\\Locale\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Joshua Gigg",
+ "email": "giggsey@gmail.com",
+ "homepage": "https://giggsey.com/"
+ }
+ ],
+ "description": "Locale functions required by libphonenumber-for-php",
+ "support": {
+ "issues": "https://github.com/giggsey/Locale/issues",
+ "source": "https://github.com/giggsey/Locale/tree/2.6"
+ },
+ "time": "2024-04-18T19:31:19+00:00"
+ },
+ {
+ "name": "guzzlehttp/guzzle",
+ "version": "7.8.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/guzzle/guzzle.git",
+ "reference": "41042bc7ab002487b876a0683fc8dce04ddce104"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/guzzle/guzzle/zipball/41042bc7ab002487b876a0683fc8dce04ddce104",
+ "reference": "41042bc7ab002487b876a0683fc8dce04ddce104",
+ "shasum": ""
+ },
+ "require": {
+ "ext-json": "*",
+ "guzzlehttp/promises": "^1.5.3 || ^2.0.1",
+ "guzzlehttp/psr7": "^1.9.1 || ^2.5.1",
+ "php": "^7.2.5 || ^8.0",
+ "psr/http-client": "^1.0",
+ "symfony/deprecation-contracts": "^2.2 || ^3.0"
+ },
+ "provide": {
+ "psr/http-client-implementation": "1.0"
+ },
+ "require-dev": {
+ "bamarni/composer-bin-plugin": "^1.8.2",
+ "ext-curl": "*",
+ "php-http/client-integration-tests": "dev-master#2c025848417c1135031fdf9c728ee53d0a7ceaee as 3.0.999",
+ "php-http/message-factory": "^1.1",
+ "phpunit/phpunit": "^8.5.36 || ^9.6.15",
+ "psr/log": "^1.1 || ^2.0 || ^3.0"
+ },
+ "suggest": {
+ "ext-curl": "Required for CURL handler support",
+ "ext-intl": "Required for Internationalized Domain Name (IDN) support",
+ "psr/log": "Required for using the Log middleware"
+ },
+ "type": "library",
+ "extra": {
+ "bamarni-bin": {
+ "bin-links": true,
+ "forward-command": false
+ }
+ },
+ "autoload": {
+ "files": [
+ "src/functions_include.php"
+ ],
+ "psr-4": {
+ "GuzzleHttp\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Graham Campbell",
+ "email": "hello@gjcampbell.co.uk",
+ "homepage": "https://github.com/GrahamCampbell"
+ },
+ {
+ "name": "Michael Dowling",
+ "email": "mtdowling@gmail.com",
+ "homepage": "https://github.com/mtdowling"
+ },
+ {
+ "name": "Jeremy Lindblom",
+ "email": "jeremeamia@gmail.com",
+ "homepage": "https://github.com/jeremeamia"
+ },
+ {
+ "name": "George Mponos",
+ "email": "gmponos@gmail.com",
+ "homepage": "https://github.com/gmponos"
+ },
+ {
+ "name": "Tobias Nyholm",
+ "email": "tobias.nyholm@gmail.com",
+ "homepage": "https://github.com/Nyholm"
+ },
+ {
+ "name": "Márk Sági-Kazár",
+ "email": "mark.sagikazar@gmail.com",
+ "homepage": "https://github.com/sagikazarmark"
+ },
+ {
+ "name": "Tobias Schultze",
+ "email": "webmaster@tubo-world.de",
+ "homepage": "https://github.com/Tobion"
+ }
+ ],
+ "description": "Guzzle is a PHP HTTP client library",
+ "keywords": [
+ "client",
+ "curl",
+ "framework",
+ "http",
+ "http client",
+ "psr-18",
+ "psr-7",
+ "rest",
+ "web service"
+ ],
+ "support": {
+ "issues": "https://github.com/guzzle/guzzle/issues",
+ "source": "https://github.com/guzzle/guzzle/tree/7.8.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/GrahamCampbell",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/Nyholm",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/guzzle",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2023-12-03T20:35:24+00:00"
+ },
+ {
+ "name": "guzzlehttp/promises",
+ "version": "2.0.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/guzzle/promises.git",
+ "reference": "bbff78d96034045e58e13dedd6ad91b5d1253223"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/guzzle/promises/zipball/bbff78d96034045e58e13dedd6ad91b5d1253223",
+ "reference": "bbff78d96034045e58e13dedd6ad91b5d1253223",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.2.5 || ^8.0"
+ },
+ "require-dev": {
+ "bamarni/composer-bin-plugin": "^1.8.2",
+ "phpunit/phpunit": "^8.5.36 || ^9.6.15"
+ },
+ "type": "library",
+ "extra": {
+ "bamarni-bin": {
+ "bin-links": true,
+ "forward-command": false
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "GuzzleHttp\\Promise\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Graham Campbell",
+ "email": "hello@gjcampbell.co.uk",
+ "homepage": "https://github.com/GrahamCampbell"
+ },
+ {
+ "name": "Michael Dowling",
+ "email": "mtdowling@gmail.com",
+ "homepage": "https://github.com/mtdowling"
+ },
+ {
+ "name": "Tobias Nyholm",
+ "email": "tobias.nyholm@gmail.com",
+ "homepage": "https://github.com/Nyholm"
+ },
+ {
+ "name": "Tobias Schultze",
+ "email": "webmaster@tubo-world.de",
+ "homepage": "https://github.com/Tobion"
+ }
+ ],
+ "description": "Guzzle promises library",
+ "keywords": [
+ "promise"
+ ],
+ "support": {
+ "issues": "https://github.com/guzzle/promises/issues",
+ "source": "https://github.com/guzzle/promises/tree/2.0.2"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/GrahamCampbell",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/Nyholm",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/promises",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2023-12-03T20:19:20+00:00"
+ },
+ {
+ "name": "guzzlehttp/psr7",
+ "version": "2.6.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/guzzle/psr7.git",
+ "reference": "45b30f99ac27b5ca93cb4831afe16285f57b8221"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/guzzle/psr7/zipball/45b30f99ac27b5ca93cb4831afe16285f57b8221",
+ "reference": "45b30f99ac27b5ca93cb4831afe16285f57b8221",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.2.5 || ^8.0",
+ "psr/http-factory": "^1.0",
+ "psr/http-message": "^1.1 || ^2.0",
+ "ralouphie/getallheaders": "^3.0"
+ },
+ "provide": {
+ "psr/http-factory-implementation": "1.0",
+ "psr/http-message-implementation": "1.0"
+ },
+ "require-dev": {
+ "bamarni/composer-bin-plugin": "^1.8.2",
+ "http-interop/http-factory-tests": "^0.9",
+ "phpunit/phpunit": "^8.5.36 || ^9.6.15"
+ },
+ "suggest": {
+ "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses"
+ },
+ "type": "library",
+ "extra": {
+ "bamarni-bin": {
+ "bin-links": true,
+ "forward-command": false
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "GuzzleHttp\\Psr7\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Graham Campbell",
+ "email": "hello@gjcampbell.co.uk",
+ "homepage": "https://github.com/GrahamCampbell"
+ },
+ {
+ "name": "Michael Dowling",
+ "email": "mtdowling@gmail.com",
+ "homepage": "https://github.com/mtdowling"
+ },
+ {
+ "name": "George Mponos",
+ "email": "gmponos@gmail.com",
+ "homepage": "https://github.com/gmponos"
+ },
+ {
+ "name": "Tobias Nyholm",
+ "email": "tobias.nyholm@gmail.com",
+ "homepage": "https://github.com/Nyholm"
+ },
+ {
+ "name": "Márk Sági-Kazár",
+ "email": "mark.sagikazar@gmail.com",
+ "homepage": "https://github.com/sagikazarmark"
+ },
+ {
+ "name": "Tobias Schultze",
+ "email": "webmaster@tubo-world.de",
+ "homepage": "https://github.com/Tobion"
+ },
+ {
+ "name": "Márk Sági-Kazár",
+ "email": "mark.sagikazar@gmail.com",
+ "homepage": "https://sagikazarmark.hu"
+ }
+ ],
+ "description": "PSR-7 message implementation that also provides common utility methods",
+ "keywords": [
+ "http",
+ "message",
+ "psr-7",
+ "request",
+ "response",
+ "stream",
+ "uri",
+ "url"
+ ],
+ "support": {
+ "issues": "https://github.com/guzzle/psr7/issues",
+ "source": "https://github.com/guzzle/psr7/tree/2.6.2"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/GrahamCampbell",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/Nyholm",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2023-12-03T20:05:35+00:00"
+ },
+ {
+ "name": "league/html-to-markdown",
+ "version": "5.1.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/thephpleague/html-to-markdown.git",
+ "reference": "0b4066eede55c48f38bcee4fb8f0aa85654390fd"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/thephpleague/html-to-markdown/zipball/0b4066eede55c48f38bcee4fb8f0aa85654390fd",
+ "reference": "0b4066eede55c48f38bcee4fb8f0aa85654390fd",
+ "shasum": ""
+ },
+ "require": {
+ "ext-dom": "*",
+ "ext-xml": "*",
+ "php": "^7.2.5 || ^8.0"
+ },
+ "require-dev": {
+ "mikehaertl/php-shellcommand": "^1.1.0",
+ "phpstan/phpstan": "^1.8.8",
+ "phpunit/phpunit": "^8.5 || ^9.2",
+ "scrutinizer/ocular": "^1.6",
+ "unleashedtech/php-coding-standard": "^2.7 || ^3.0",
+ "vimeo/psalm": "^4.22 || ^5.0"
+ },
+ "bin": [
+ "bin/html-to-markdown"
+ ],
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "5.2-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "League\\HTMLToMarkdown\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Colin O'Dell",
+ "email": "colinodell@gmail.com",
+ "homepage": "https://www.colinodell.com",
+ "role": "Lead Developer"
+ },
+ {
+ "name": "Nick Cernis",
+ "email": "nick@cern.is",
+ "homepage": "http://modernnerd.net",
+ "role": "Original Author"
+ }
+ ],
+ "description": "An HTML-to-markdown conversion helper for PHP",
+ "homepage": "https://github.com/thephpleague/html-to-markdown",
+ "keywords": [
+ "html",
+ "markdown"
+ ],
+ "support": {
+ "issues": "https://github.com/thephpleague/html-to-markdown/issues",
+ "source": "https://github.com/thephpleague/html-to-markdown/tree/5.1.1"
+ },
+ "funding": [
+ {
+ "url": "https://www.colinodell.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://www.paypal.me/colinpodell/10.00",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/colinodell",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/league/html-to-markdown",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2023-07-12T21:21:09+00:00"
+ },
+ {
+ "name": "league/json-guard",
+ "version": "1.0.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/thephpleague/json-guard.git",
+ "reference": "596059d2c013bcea1a8a1386bd0e60d32ef39eb9"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/thephpleague/json-guard/zipball/596059d2c013bcea1a8a1386bd0e60d32ef39eb9",
+ "reference": "596059d2c013bcea1a8a1386bd0e60d32ef39eb9",
+ "shasum": ""
+ },
+ "require": {
+ "ext-bcmath": "*",
+ "php": ">=5.6.0",
+ "psr/container": "^1.0"
+ },
+ "require-dev": {
+ "ext-curl": "*",
+ "json-schema/json-schema-test-suite": "1.2.0",
+ "league/json-reference": "1.0.0",
+ "phpunit/phpunit": "4.*",
+ "scrutinizer/ocular": "~1.1",
+ "squizlabs/php_codesniffer": "~2.3"
+ },
+ "type": "library",
+ "autoload": {
+ "files": [
+ "src/functions.php"
+ ],
+ "psr-4": {
+ "League\\JsonGuard\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Matthew Allan",
+ "email": "matthew.james.allan@gmail.com",
+ "homepage": "https://mattallan.org",
+ "role": "Developer"
+ }
+ ],
+ "description": "A validator for JSON using json-schema.",
+ "homepage": "https://github.com/thephpleague/json-guard",
+ "keywords": [
+ "json",
+ "json-schema",
+ "json-schema.org",
+ "schema",
+ "validation"
+ ],
+ "support": {
+ "issues": "https://github.com/thephpleague/json-guard/issues",
+ "source": "https://github.com/thephpleague/json-guard/tree/master"
+ },
+ "abandoned": true,
+ "time": "2017-05-03T21:12:30+00:00"
+ },
+ {
+ "name": "league/oauth2-client",
+ "version": "2.7.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/thephpleague/oauth2-client.git",
+ "reference": "160d6274b03562ebeb55ed18399281d8118b76c8"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/thephpleague/oauth2-client/zipball/160d6274b03562ebeb55ed18399281d8118b76c8",
+ "reference": "160d6274b03562ebeb55ed18399281d8118b76c8",
+ "shasum": ""
+ },
+ "require": {
+ "guzzlehttp/guzzle": "^6.0 || ^7.0",
+ "paragonie/random_compat": "^1 || ^2 || ^9.99",
+ "php": "^5.6 || ^7.0 || ^8.0"
+ },
+ "require-dev": {
+ "mockery/mockery": "^1.3.5",
+ "php-parallel-lint/php-parallel-lint": "^1.3.1",
+ "phpunit/phpunit": "^5.7 || ^6.0 || ^9.5",
+ "squizlabs/php_codesniffer": "^2.3 || ^3.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-2.x": "2.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "League\\OAuth2\\Client\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Alex Bilbie",
+ "email": "hello@alexbilbie.com",
+ "homepage": "http://www.alexbilbie.com",
+ "role": "Developer"
+ },
+ {
+ "name": "Woody Gilk",
+ "homepage": "https://github.com/shadowhand",
+ "role": "Contributor"
+ }
+ ],
+ "description": "OAuth 2.0 Client Library",
+ "keywords": [
+ "Authentication",
+ "SSO",
+ "authorization",
+ "identity",
+ "idp",
+ "oauth",
+ "oauth2",
+ "single sign on"
+ ],
+ "support": {
+ "issues": "https://github.com/thephpleague/oauth2-client/issues",
+ "source": "https://github.com/thephpleague/oauth2-client/tree/2.7.0"
+ },
+ "time": "2023-04-16T18:19:15+00:00"
+ },
+ {
+ "name": "monolog/monolog",
+ "version": "1.27.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/Seldaek/monolog.git",
+ "reference": "904713c5929655dc9b97288b69cfeedad610c9a1"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/Seldaek/monolog/zipball/904713c5929655dc9b97288b69cfeedad610c9a1",
+ "reference": "904713c5929655dc9b97288b69cfeedad610c9a1",
+ "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"
+ ],
+ "support": {
+ "issues": "https://github.com/Seldaek/monolog/issues",
+ "source": "https://github.com/Seldaek/monolog/tree/1.27.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/Seldaek",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/monolog/monolog",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2022-06-09T08:53:42+00:00"
+ },
+ {
+ "name": "nikic/fast-route",
+ "version": "v1.3.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/nikic/FastRoute.git",
+ "reference": "181d480e08d9476e61381e04a71b34dc0432e812"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/nikic/FastRoute/zipball/181d480e08d9476e61381e04a71b34dc0432e812",
+ "reference": "181d480e08d9476e61381e04a71b34dc0432e812",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.4.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^4.8.35|~5.7"
+ },
+ "type": "library",
+ "autoload": {
+ "files": [
+ "src/functions.php"
+ ],
+ "psr-4": {
+ "FastRoute\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Nikita Popov",
+ "email": "nikic@php.net"
+ }
+ ],
+ "description": "Fast request router for PHP",
+ "keywords": [
+ "router",
+ "routing"
+ ],
+ "support": {
+ "issues": "https://github.com/nikic/FastRoute/issues",
+ "source": "https://github.com/nikic/FastRoute/tree/master"
+ },
+ "time": "2018-02-13T20:26:39+00:00"
+ },
+ {
+ "name": "paragonie/random_compat",
+ "version": "v9.99.100",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/paragonie/random_compat.git",
+ "reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/paragonie/random_compat/zipball/996434e5492cb4c3edcb9168db6fbb1359ef965a",
+ "reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">= 7"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "4.*|5.*",
+ "vimeo/psalm": "^1"
+ },
+ "suggest": {
+ "ext-libsodium": "Provides a modern crypto API that can be used to generate random bytes."
+ },
+ "type": "library",
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Paragon Initiative Enterprises",
+ "email": "security@paragonie.com",
+ "homepage": "https://paragonie.com"
+ }
+ ],
+ "description": "PHP 5.x polyfill for random_bytes() and random_int() from PHP 7",
+ "keywords": [
+ "csprng",
+ "polyfill",
+ "pseudorandom",
+ "random"
+ ],
+ "support": {
+ "email": "info@paragonie.com",
+ "issues": "https://github.com/paragonie/random_compat/issues",
+ "source": "https://github.com/paragonie/random_compat"
+ },
+ "time": "2020-10-15T08:29:30+00:00"
+ },
+ {
+ "name": "php-http/curl-client",
+ "version": "2.3.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-http/curl-client.git",
+ "reference": "0b869922458b1cde9137374545ed4fff7ac83623"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-http/curl-client/zipball/0b869922458b1cde9137374545ed4fff7ac83623",
+ "reference": "0b869922458b1cde9137374545ed4fff7ac83623",
+ "shasum": ""
+ },
+ "require": {
+ "ext-curl": "*",
+ "php": "^7.4 || ^8.0",
+ "php-http/discovery": "^1.6",
+ "php-http/httplug": "^2.0",
+ "php-http/message": "^1.2",
+ "psr/http-client": "^1.0",
+ "psr/http-factory-implementation": "^1.0",
+ "symfony/options-resolver": "^3.4 || ^4.0 || ^5.0 || ^6.0 || ^7.0"
+ },
+ "provide": {
+ "php-http/async-client-implementation": "1.0",
+ "php-http/client-implementation": "1.0",
+ "psr/http-client-implementation": "1.0"
+ },
+ "require-dev": {
+ "guzzlehttp/psr7": "^2.0",
+ "laminas/laminas-diactoros": "^2.0",
+ "php-http/client-integration-tests": "^3.0",
+ "php-http/message-factory": "^1.1",
+ "phpunit/phpunit": "^7.5 || ^9.4"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Http\\Client\\Curl\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Михаил Красильников",
+ "email": "m.krasilnikov@yandex.ru"
+ }
+ ],
+ "description": "PSR-18 and HTTPlug Async client with cURL",
+ "homepage": "http://php-http.org",
+ "keywords": [
+ "curl",
+ "http",
+ "psr-18"
+ ],
+ "support": {
+ "issues": "https://github.com/php-http/curl-client/issues",
+ "source": "https://github.com/php-http/curl-client/tree/2.3.2"
+ },
+ "time": "2024-03-03T08:21:07+00:00"
+ },
+ {
+ "name": "php-http/discovery",
+ "version": "1.19.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-http/discovery.git",
+ "reference": "0700efda8d7526335132360167315fdab3aeb599"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-http/discovery/zipball/0700efda8d7526335132360167315fdab3aeb599",
+ "reference": "0700efda8d7526335132360167315fdab3aeb599",
+ "shasum": ""
+ },
+ "require": {
+ "composer-plugin-api": "^1.0|^2.0",
+ "php": "^7.1 || ^8.0"
+ },
+ "conflict": {
+ "nyholm/psr7": "<1.0",
+ "zendframework/zend-diactoros": "*"
+ },
+ "provide": {
+ "php-http/async-client-implementation": "*",
+ "php-http/client-implementation": "*",
+ "psr/http-client-implementation": "*",
+ "psr/http-factory-implementation": "*",
+ "psr/http-message-implementation": "*"
+ },
+ "require-dev": {
+ "composer/composer": "^1.0.2|^2.0",
+ "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 || ^7.3",
+ "sebastian/comparator": "^3.0.5 || ^4.0.8",
+ "symfony/phpunit-bridge": "^6.4.4 || ^7.0.1"
+ },
+ "type": "composer-plugin",
+ "extra": {
+ "class": "Http\\Discovery\\Composer\\Plugin",
+ "plugin-optional": true
+ },
+ "autoload": {
+ "psr-4": {
+ "Http\\Discovery\\": "src/"
+ },
+ "exclude-from-classmap": [
+ "src/Composer/Plugin.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Márk Sági-Kazár",
+ "email": "mark.sagikazar@gmail.com"
+ }
+ ],
+ "description": "Finds and installs PSR-7, PSR-17, PSR-18 and HTTPlug implementations",
+ "homepage": "http://php-http.org",
+ "keywords": [
+ "adapter",
+ "client",
+ "discovery",
+ "factory",
+ "http",
+ "message",
+ "psr17",
+ "psr7"
+ ],
+ "support": {
+ "issues": "https://github.com/php-http/discovery/issues",
+ "source": "https://github.com/php-http/discovery/tree/1.19.4"
+ },
+ "time": "2024-03-29T13:00:05+00:00"
+ },
+ {
+ "name": "php-http/httplug",
+ "version": "2.4.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-http/httplug.git",
+ "reference": "625ad742c360c8ac580fcc647a1541d29e257f67"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-http/httplug/zipball/625ad742c360c8ac580fcc647a1541d29e257f67",
+ "reference": "625ad742c360c8ac580fcc647a1541d29e257f67",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.1 || ^8.0",
+ "php-http/promise": "^1.1",
+ "psr/http-client": "^1.0",
+ "psr/http-message": "^1.0 || ^2.0"
+ },
+ "require-dev": {
+ "friends-of-phpspec/phpspec-code-coverage": "^4.1 || ^5.0 || ^6.0",
+ "phpspec/phpspec": "^5.1 || ^6.0 || ^7.0"
+ },
+ "type": "library",
+ "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"
+ ],
+ "support": {
+ "issues": "https://github.com/php-http/httplug/issues",
+ "source": "https://github.com/php-http/httplug/tree/2.4.0"
+ },
+ "time": "2023-04-14T15:10:03+00:00"
+ },
+ {
+ "name": "php-http/message",
+ "version": "1.16.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-http/message.git",
+ "reference": "5997f3289332c699fa2545c427826272498a2088"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-http/message/zipball/5997f3289332c699fa2545c427826272498a2088",
+ "reference": "5997f3289332c699fa2545c427826272498a2088",
+ "shasum": ""
+ },
+ "require": {
+ "clue/stream-filter": "^1.5",
+ "php": "^7.2 || ^8.0",
+ "psr/http-message": "^1.1 || ^2.0"
+ },
+ "provide": {
+ "php-http/message-factory-implementation": "1.0"
+ },
+ "require-dev": {
+ "ergebnis/composer-normalize": "^2.6",
+ "ext-zlib": "*",
+ "guzzlehttp/psr7": "^1.0 || ^2.0",
+ "laminas/laminas-diactoros": "^2.0 || ^3.0",
+ "php-http/message-factory": "^1.0.2",
+ "phpspec/phpspec": "^5.1 || ^6.3 || ^7.1",
+ "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",
+ "autoload": {
+ "files": [
+ "src/filters.php"
+ ],
+ "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": "HTTP Message related tools",
+ "homepage": "http://php-http.org",
+ "keywords": [
+ "http",
+ "message",
+ "psr-7"
+ ],
+ "support": {
+ "issues": "https://github.com/php-http/message/issues",
+ "source": "https://github.com/php-http/message/tree/1.16.1"
+ },
+ "time": "2024-03-07T13:22:09+00:00"
+ },
+ {
+ "name": "php-http/promise",
+ "version": "1.3.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-http/promise.git",
+ "reference": "fc85b1fba37c169a69a07ef0d5a8075770cc1f83"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-http/promise/zipball/fc85b1fba37c169a69a07ef0d5a8075770cc1f83",
+ "reference": "fc85b1fba37c169a69a07ef0d5a8075770cc1f83",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.1 || ^8.0"
+ },
+ "require-dev": {
+ "friends-of-phpspec/phpspec-code-coverage": "^4.3.2 || ^6.3",
+ "phpspec/phpspec": "^5.1.2 || ^6.2 || ^7.4"
+ },
+ "type": "library",
+ "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"
+ ],
+ "support": {
+ "issues": "https://github.com/php-http/promise/issues",
+ "source": "https://github.com/php-http/promise/tree/1.3.1"
+ },
+ "time": "2024-03-15T13:55:21+00:00"
+ },
+ {
+ "name": "psr/cache",
+ "version": "2.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/cache.git",
+ "reference": "213f9dbc5b9bfbc4f8db86d2838dc968752ce13b"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/cache/zipball/213f9dbc5b9bfbc4f8db86d2838dc968752ce13b",
+ "reference": "213f9dbc5b9bfbc4f8db86d2838dc968752ce13b",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.0.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": "https://www.php-fig.org/"
+ }
+ ],
+ "description": "Common interface for caching libraries",
+ "keywords": [
+ "cache",
+ "psr",
+ "psr-6"
+ ],
+ "support": {
+ "source": "https://github.com/php-fig/cache/tree/2.0.0"
+ },
+ "time": "2021-02-03T23:23:37+00:00"
+ },
+ {
+ "name": "psr/container",
+ "version": "1.1.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/container.git",
+ "reference": "513e0666f7216c7459170d56df27dfcefe1689ea"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/container/zipball/513e0666f7216c7459170d56df27dfcefe1689ea",
+ "reference": "513e0666f7216c7459170d56df27dfcefe1689ea",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.4.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"
+ ],
+ "support": {
+ "issues": "https://github.com/php-fig/container/issues",
+ "source": "https://github.com/php-fig/container/tree/1.1.2"
+ },
+ "time": "2021-11-05T16:50:12+00:00"
+ },
+ {
+ "name": "psr/http-client",
+ "version": "1.0.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/http-client.git",
+ "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90",
+ "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.0 || ^8.0",
+ "psr/http-message": "^1.0 || ^2.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": "https://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"
+ ],
+ "support": {
+ "source": "https://github.com/php-fig/http-client"
+ },
+ "time": "2023-09-23T14:17:50+00:00"
+ },
+ {
+ "name": "psr/http-factory",
+ "version": "1.1.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/http-factory.git",
+ "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a",
+ "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.1",
+ "psr/http-message": "^1.0 || ^2.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": "https://www.php-fig.org/"
+ }
+ ],
+ "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories",
+ "keywords": [
+ "factory",
+ "http",
+ "message",
+ "psr",
+ "psr-17",
+ "psr-7",
+ "request",
+ "response"
+ ],
+ "support": {
+ "source": "https://github.com/php-fig/http-factory"
+ },
+ "time": "2024-04-15T12:06:14+00:00"
+ },
+ {
+ "name": "psr/http-message",
+ "version": "1.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/http-message.git",
+ "reference": "cb6ce4845ce34a8ad9e68117c10ee90a29919eba"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/http-message/zipball/cb6ce4845ce34a8ad9e68117c10ee90a29919eba",
+ "reference": "cb6ce4845ce34a8ad9e68117c10ee90a29919eba",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.2 || ^8.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.1.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\Http\\Message\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "http://www.php-fig.org/"
+ }
+ ],
+ "description": "Common interface for HTTP messages",
+ "homepage": "https://github.com/php-fig/http-message",
+ "keywords": [
+ "http",
+ "http-message",
+ "psr",
+ "psr-7",
+ "request",
+ "response"
+ ],
+ "support": {
+ "source": "https://github.com/php-fig/http-message/tree/1.1"
+ },
+ "time": "2023-04-04T09:50:52+00:00"
+ },
+ {
+ "name": "psr/http-server-handler",
+ "version": "1.0.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/http-server-handler.git",
+ "reference": "84c4fb66179be4caaf8e97bd239203245302e7d4"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/http-server-handler/zipball/84c4fb66179be4caaf8e97bd239203245302e7d4",
+ "reference": "84c4fb66179be4caaf8e97bd239203245302e7d4",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.0",
+ "psr/http-message": "^1.0 || ^2.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\Http\\Server\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "https://www.php-fig.org/"
+ }
+ ],
+ "description": "Common interface for HTTP server-side request handler",
+ "keywords": [
+ "handler",
+ "http",
+ "http-interop",
+ "psr",
+ "psr-15",
+ "psr-7",
+ "request",
+ "response",
+ "server"
+ ],
+ "support": {
+ "source": "https://github.com/php-fig/http-server-handler/tree/1.0.2"
+ },
+ "time": "2023-04-10T20:06:20+00:00"
+ },
+ {
+ "name": "psr/http-server-middleware",
+ "version": "1.0.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/http-server-middleware.git",
+ "reference": "c1481f747daaa6a0782775cd6a8c26a1bf4a3829"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/http-server-middleware/zipball/c1481f747daaa6a0782775cd6a8c26a1bf4a3829",
+ "reference": "c1481f747daaa6a0782775cd6a8c26a1bf4a3829",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.0",
+ "psr/http-message": "^1.0 || ^2.0",
+ "psr/http-server-handler": "^1.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\Http\\Server\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "https://www.php-fig.org/"
+ }
+ ],
+ "description": "Common interface for HTTP server-side middleware",
+ "keywords": [
+ "http",
+ "http-interop",
+ "middleware",
+ "psr",
+ "psr-15",
+ "psr-7",
+ "request",
+ "response"
+ ],
+ "support": {
+ "issues": "https://github.com/php-fig/http-server-middleware/issues",
+ "source": "https://github.com/php-fig/http-server-middleware/tree/1.0.2"
+ },
+ "time": "2023-04-11T06:14:47+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"
+ ],
+ "support": {
+ "source": "https://github.com/php-fig/log/tree/1.1.4"
+ },
+ "time": "2021-05-03T11:20:27+00:00"
+ },
+ {
+ "name": "psr/simple-cache",
+ "version": "3.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/simple-cache.git",
+ "reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/simple-cache/zipball/764e0b3939f5ca87cb904f570ef9be2d78a07865",
+ "reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.0.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "3.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\SimpleCache\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "https://www.php-fig.org/"
+ }
+ ],
+ "description": "Common interfaces for simple caching",
+ "keywords": [
+ "cache",
+ "caching",
+ "psr",
+ "psr-16",
+ "simple-cache"
+ ],
+ "support": {
+ "source": "https://github.com/php-fig/simple-cache/tree/3.0.0"
+ },
+ "time": "2021-10-29T13:26:27+00:00"
+ },
+ {
+ "name": "ralouphie/getallheaders",
+ "version": "3.0.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/ralouphie/getallheaders.git",
+ "reference": "120b605dfeb996808c31b6477290a714d356e822"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822",
+ "reference": "120b605dfeb996808c31b6477290a714d356e822",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.6"
+ },
+ "require-dev": {
+ "php-coveralls/php-coveralls": "^2.1",
+ "phpunit/phpunit": "^5 || ^6.5"
+ },
+ "type": "library",
+ "autoload": {
+ "files": [
+ "src/getallheaders.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Ralph Khattar",
+ "email": "ralph.khattar@gmail.com"
+ }
+ ],
+ "description": "A polyfill for getallheaders.",
+ "support": {
+ "issues": "https://github.com/ralouphie/getallheaders/issues",
+ "source": "https://github.com/ralouphie/getallheaders/tree/develop"
+ },
+ "time": "2019-03-08T08:55:37+00:00"
+ },
+ {
+ "name": "slim/http-cache",
+ "version": "1.1.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/slimphp/Slim-HttpCache.git",
+ "reference": "d1a091aca45695a2159194132872f4a544416bc9"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/slimphp/Slim-HttpCache/zipball/d1a091aca45695a2159194132872f4a544416bc9",
+ "reference": "d1a091aca45695a2159194132872f4a544416bc9",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.2 || ^8.0",
+ "psr/http-message": "^1.0",
+ "psr/http-server-middleware": "^1.0"
+ },
+ "require-dev": {
+ "phpstan/phpstan": "^0.12.28",
+ "phpunit/phpunit": "^8.5.13 || ^9.3.8",
+ "slim/psr7": "^1.1",
+ "squizlabs/php_codesniffer": "^3.5"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Slim\\HttpCache\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Josh Lockhart",
+ "email": "hello@joshlockhart.com",
+ "homepage": "http://joshlockhart.com"
+ }
+ ],
+ "description": "Slim Framework HTTP cache middleware and service provider",
+ "homepage": "https://www.slimframework.com",
+ "keywords": [
+ "cache",
+ "framework",
+ "middleware",
+ "slim"
+ ],
+ "support": {
+ "issues": "https://github.com/slimphp/Slim-HttpCache/issues",
+ "source": "https://github.com/slimphp/Slim-HttpCache/tree/1.1.0"
+ },
+ "time": "2020-12-08T17:32:05+00:00"
+ },
+ {
+ "name": "slim/psr7",
+ "version": "1.6.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/slimphp/Slim-Psr7.git",
+ "reference": "72d2b2bac94ab4575d369f605dbfafbe168d3163"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/slimphp/Slim-Psr7/zipball/72d2b2bac94ab4575d369f605dbfafbe168d3163",
+ "reference": "72d2b2bac94ab4575d369f605dbfafbe168d3163",
+ "shasum": ""
+ },
+ "require": {
+ "fig/http-message-util": "^1.1.5",
+ "php": "^7.4 || ^8.0",
+ "psr/http-factory": "^1.0",
+ "psr/http-message": "^1.0",
+ "ralouphie/getallheaders": "^3.0",
+ "symfony/polyfill-php80": "^1.26"
+ },
+ "provide": {
+ "psr/http-factory-implementation": "1.0",
+ "psr/http-message-implementation": "1.0"
+ },
+ "require-dev": {
+ "adriansuter/php-autoload-override": "^1.3",
+ "ext-json": "*",
+ "http-interop/http-factory-tests": "^0.9.0",
+ "php-http/psr7-integration-tests": "1.1",
+ "phpspec/prophecy": "^1.15",
+ "phpspec/prophecy-phpunit": "^2.0",
+ "phpstan/phpstan": "^1.8",
+ "phpunit/phpunit": "^9.5",
+ "squizlabs/php_codesniffer": "^3.7"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Slim\\Psr7\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Josh Lockhart",
+ "email": "hello@joshlockhart.com",
+ "homepage": "http://joshlockhart.com"
+ },
+ {
+ "name": "Andrew Smith",
+ "email": "a.smith@silentworks.co.uk",
+ "homepage": "http://silentworks.co.uk"
+ },
+ {
+ "name": "Rob Allen",
+ "email": "rob@akrabat.com",
+ "homepage": "http://akrabat.com"
+ },
+ {
+ "name": "Pierre Berube",
+ "email": "pierre@lgse.com",
+ "homepage": "http://www.lgse.com"
+ }
+ ],
+ "description": "Strict PSR-7 implementation",
+ "homepage": "https://www.slimframework.com",
+ "keywords": [
+ "http",
+ "psr-7",
+ "psr7"
+ ],
+ "support": {
+ "issues": "https://github.com/slimphp/Slim-Psr7/issues",
+ "source": "https://github.com/slimphp/Slim-Psr7/tree/1.6.1"
+ },
+ "time": "2023-04-17T16:02:20+00:00"
+ },
+ {
+ "name": "slim/slim",
+ "version": "4.13.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/slimphp/Slim.git",
+ "reference": "038fd5713d5a41636fdff0e8dcceedecdd17fc17"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/slimphp/Slim/zipball/038fd5713d5a41636fdff0e8dcceedecdd17fc17",
+ "reference": "038fd5713d5a41636fdff0e8dcceedecdd17fc17",
+ "shasum": ""
+ },
+ "require": {
+ "ext-json": "*",
+ "nikic/fast-route": "^1.3",
+ "php": "^7.4 || ^8.0",
+ "psr/container": "^1.0 || ^2.0",
+ "psr/http-factory": "^1.0",
+ "psr/http-message": "^1.1 || ^2.0",
+ "psr/http-server-handler": "^1.0",
+ "psr/http-server-middleware": "^1.0",
+ "psr/log": "^1.1 || ^2.0 || ^3.0"
+ },
+ "require-dev": {
+ "adriansuter/php-autoload-override": "^1.4",
+ "ext-simplexml": "*",
+ "guzzlehttp/psr7": "^2.6",
+ "httpsoft/http-message": "^1.1",
+ "httpsoft/http-server-request": "^1.1",
+ "laminas/laminas-diactoros": "^2.17 || ^3",
+ "nyholm/psr7": "^1.8",
+ "nyholm/psr7-server": "^1.1",
+ "phpspec/prophecy": "^1.19",
+ "phpspec/prophecy-phpunit": "^2.1",
+ "phpstan/phpstan": "^1.10",
+ "phpunit/phpunit": "^9.6",
+ "slim/http": "^1.3",
+ "slim/psr7": "^1.6",
+ "squizlabs/php_codesniffer": "^3.9"
+ },
+ "suggest": {
+ "ext-simplexml": "Needed to support XML format in BodyParsingMiddleware",
+ "ext-xml": "Needed to support XML format in BodyParsingMiddleware",
+ "php-di/php-di": "PHP-DI is the recommended container library to be used with Slim",
+ "slim/psr7": "Slim PSR-7 implementation. See https://www.slimframework.com/docs/v4/start/installation.html for more information."
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Slim\\": "Slim"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Josh Lockhart",
+ "email": "hello@joshlockhart.com",
+ "homepage": "https://joshlockhart.com"
+ },
+ {
+ "name": "Andrew Smith",
+ "email": "a.smith@silentworks.co.uk",
+ "homepage": "http://silentworks.co.uk"
+ },
+ {
+ "name": "Rob Allen",
+ "email": "rob@akrabat.com",
+ "homepage": "http://akrabat.com"
+ },
+ {
+ "name": "Pierre Berube",
+ "email": "pierre@lgse.com",
+ "homepage": "http://www.lgse.com"
+ },
+ {
+ "name": "Gabriel Manricks",
+ "email": "gmanricks@me.com",
+ "homepage": "http://gabrielmanricks.com"
+ }
+ ],
+ "description": "Slim is a PHP micro framework that helps you quickly write simple yet powerful web applications and APIs",
+ "homepage": "https://www.slimframework.com",
+ "keywords": [
+ "api",
+ "framework",
+ "micro",
+ "router"
+ ],
+ "support": {
+ "docs": "https://www.slimframework.com/docs/v4/",
+ "forum": "https://discourse.slimframework.com/",
+ "irc": "irc://irc.freenode.net:6667/slimphp",
+ "issues": "https://github.com/slimphp/Slim/issues",
+ "rss": "https://www.slimframework.com/blog/feed.rss",
+ "slack": "https://slimphp.slack.com/",
+ "source": "https://github.com/slimphp/Slim",
+ "wiki": "https://github.com/slimphp/Slim/wiki"
+ },
+ "funding": [
+ {
+ "url": "https://opencollective.com/slimphp",
+ "type": "open_collective"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/slim/slim",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2024-03-03T21:25:30+00:00"
+ },
+ {
+ "name": "slim/twig-view",
+ "version": "3.4.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/slimphp/Twig-View.git",
+ "reference": "1b351536b9a07ed90a3563ee9d71a987c5d74610"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/slimphp/Twig-View/zipball/1b351536b9a07ed90a3563ee9d71a987c5d74610",
+ "reference": "1b351536b9a07ed90a3563ee9d71a987c5d74610",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.4 || ^8.0",
+ "psr/http-message": "^1.1 || ^2.0",
+ "slim/slim": "^4.12",
+ "symfony/polyfill-php81": "^1.29",
+ "twig/twig": "^3.8"
+ },
+ "require-dev": {
+ "phpspec/prophecy-phpunit": "^2.0",
+ "phpstan/phpstan": "^1.10.59",
+ "phpunit/phpunit": "^9.6",
+ "psr/http-factory": "^1.0",
+ "squizlabs/php_codesniffer": "^3.9"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Slim\\Views\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Josh Lockhart",
+ "email": "hello@joshlockhart.com",
+ "homepage": "http://joshlockhart.com"
+ },
+ {
+ "name": "Pierre Berube",
+ "email": "pierre@lgse.com",
+ "homepage": "http://www.lgse.com"
+ }
+ ],
+ "description": "Slim Framework 4 view helper built on top of the Twig 3 templating component",
+ "homepage": "https://www.slimframework.com",
+ "keywords": [
+ "framework",
+ "slim",
+ "template",
+ "twig",
+ "view"
+ ],
+ "support": {
+ "issues": "https://github.com/slimphp/Twig-View/issues",
+ "source": "https://github.com/slimphp/Twig-View/tree/3.4.0"
+ },
+ "time": "2024-04-28T20:36:39+00:00"
+ },
+ {
+ "name": "stevenmaguire/oauth2-keycloak",
+ "version": "4.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/stevenmaguire/oauth2-keycloak.git",
+ "reference": "05ead6bb6bcd2b6f96dfae87c769dcd3e5f6129d"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/stevenmaguire/oauth2-keycloak/zipball/05ead6bb6bcd2b6f96dfae87c769dcd3e5f6129d",
+ "reference": "05ead6bb6bcd2b6f96dfae87c769dcd3e5f6129d",
+ "shasum": ""
+ },
+ "require": {
+ "firebase/php-jwt": "^4.0 || ^5.0 || ^6.0",
+ "league/oauth2-client": "^2.0",
+ "php": "~7.2 || ~8.0"
+ },
+ "require-dev": {
+ "mockery/mockery": "~1.5.0",
+ "phpunit/phpunit": "~9.6.4",
+ "squizlabs/php_codesniffer": "~3.7.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Stevenmaguire\\OAuth2\\Client\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Steven Maguire",
+ "email": "stevenmaguire@gmail.com",
+ "homepage": "https://github.com/stevenmaguire"
+ }
+ ],
+ "description": "Keycloak OAuth 2.0 Client Provider for The PHP League OAuth2-Client",
+ "keywords": [
+ "authorisation",
+ "authorization",
+ "client",
+ "keycloak",
+ "oauth",
+ "oauth2"
+ ],
+ "support": {
+ "issues": "https://github.com/stevenmaguire/oauth2-keycloak/issues",
+ "source": "https://github.com/stevenmaguire/oauth2-keycloak/tree/4.0.0"
+ },
+ "time": "2023-03-14T09:43:47+00:00"
+ },
+ {
+ "name": "symfony/cache",
+ "version": "v6.0.19",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/cache.git",
+ "reference": "81ca309f056e836480928b20280ec52ce8369bb3"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/cache/zipball/81ca309f056e836480928b20280ec52ce8369bb3",
+ "reference": "81ca309f056e836480928b20280ec52ce8369bb3",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.0.2",
+ "psr/cache": "^2.0|^3.0",
+ "psr/log": "^1.1|^2|^3",
+ "symfony/cache-contracts": "^1.1.7|^2|^3",
+ "symfony/service-contracts": "^1.1|^2|^3",
+ "symfony/var-exporter": "^5.4|^6.0"
+ },
+ "conflict": {
+ "doctrine/dbal": "<2.13.1",
+ "symfony/dependency-injection": "<5.4",
+ "symfony/http-kernel": "<5.4",
+ "symfony/var-dumper": "<5.4"
+ },
+ "provide": {
+ "psr/cache-implementation": "2.0|3.0",
+ "psr/simple-cache-implementation": "1.0|2.0|3.0",
+ "symfony/cache-implementation": "1.1|2.0|3.0"
+ },
+ "require-dev": {
+ "cache/integration-tests": "dev-master",
+ "doctrine/dbal": "^2.13.1|^3.0",
+ "predis/predis": "^1.1",
+ "psr/simple-cache": "^1.0|^2.0|^3.0",
+ "symfony/config": "^5.4|^6.0",
+ "symfony/dependency-injection": "^5.4|^6.0",
+ "symfony/filesystem": "^5.4|^6.0",
+ "symfony/http-kernel": "^5.4|^6.0",
+ "symfony/messenger": "^5.4|^6.0",
+ "symfony/var-dumper": "^5.4|^6.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 extended PSR-6, PSR-16 (and tags) implementations",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "caching",
+ "psr6"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/cache/tree/v6.0.19"
+ },
+ "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": "2023-01-20T17:44:14+00:00"
+ },
+ {
+ "name": "symfony/cache-contracts",
+ "version": "v2.5.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/cache-contracts.git",
+ "reference": "517c3a3619dadfa6952c4651767fcadffb4df65e"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/cache-contracts/zipball/517c3a3619dadfa6952c4651767fcadffb4df65e",
+ "reference": "517c3a3619dadfa6952c4651767fcadffb4df65e",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2.5",
+ "psr/cache": "^1.0|^2.0|^3.0"
+ },
+ "suggest": {
+ "symfony/cache-implementation": ""
+ },
+ "type": "library",
+ "extra": {
+ "thanks": {
+ "url": "https://github.com/symfony/contracts",
+ "name": "symfony/contracts"
+ },
+ "branch-alias": {
+ "dev-main": "2.5-dev"
+ }
+ },
+ "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"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/cache-contracts/tree/v2.5.4"
+ },
+ "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": "2024-09-25T14:11:13+00:00"
+ },
+ {
+ "name": "symfony/deprecation-contracts",
+ "version": "v2.5.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/deprecation-contracts.git",
+ "reference": "80d075412b557d41002320b96a096ca65aa2c98d"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/80d075412b557d41002320b96a096ca65aa2c98d",
+ "reference": "80d075412b557d41002320b96a096ca65aa2c98d",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.1"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "2.5-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",
+ "support": {
+ "source": "https://github.com/symfony/deprecation-contracts/tree/v2.5.3"
+ },
+ "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": "2023-01-24T14:02:46+00:00"
+ },
+ {
+ "name": "symfony/intl",
+ "version": "v5.4.39",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/intl.git",
+ "reference": "0ae24e7ead0761e3e29e89c3336353b991c90f96"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/intl/zipball/0ae24e7ead0761e3e29e89c3336353b991c90f96",
+ "reference": "0ae24e7ead0761e3e29e89c3336353b991c90f96",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2.5",
+ "symfony/deprecation-contracts": "^2.1|^3",
+ "symfony/polyfill-php80": "^1.16"
+ },
+ "require-dev": {
+ "symfony/filesystem": "^4.4|^5.0|^6.0",
+ "symfony/var-exporter": "^5.4|^6.0"
+ },
+ "type": "library",
+ "autoload": {
+ "files": [
+ "Resources/functions.php"
+ ],
+ "psr-4": {
+ "Symfony\\Component\\Intl\\": ""
+ },
+ "classmap": [
+ "Resources/stubs"
+ ],
+ "exclude-from-classmap": [
+ "/Tests/",
+ "/Resources/data/"
+ ]
+ },
+ "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"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/intl/tree/v5.4.39"
+ },
+ "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": "2024-04-18T08:26:06+00:00"
+ },
+ {
+ "name": "symfony/options-resolver",
+ "version": "v5.4.39",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/options-resolver.git",
+ "reference": "1303bb73d6c3882f07c618129295503085dfddb9"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/options-resolver/zipball/1303bb73d6c3882f07c618129295503085dfddb9",
+ "reference": "1303bb73d6c3882f07c618129295503085dfddb9",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2.5",
+ "symfony/deprecation-contracts": "^2.1|^3",
+ "symfony/polyfill-php73": "~1.0",
+ "symfony/polyfill-php80": "^1.16"
+ },
+ "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"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/options-resolver/tree/v5.4.39"
+ },
+ "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": "2024-04-18T08:26:06+00:00"
+ },
+ {
+ "name": "symfony/polyfill-ctype",
+ "version": "v1.29.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-ctype.git",
+ "reference": "ef4d7e442ca910c4764bce785146269b30cb5fc4"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/ef4d7e442ca910c4764bce785146269b30cb5fc4",
+ "reference": "ef4d7e442ca910c4764bce785146269b30cb5fc4",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.1"
+ },
+ "provide": {
+ "ext-ctype": "*"
+ },
+ "suggest": {
+ "ext-ctype": "For best performance"
+ },
+ "type": "library",
+ "extra": {
+ "thanks": {
+ "name": "symfony/polyfill",
+ "url": "https://github.com/symfony/polyfill"
+ }
+ },
+ "autoload": {
+ "files": [
+ "bootstrap.php"
+ ],
+ "psr-4": {
+ "Symfony\\Polyfill\\Ctype\\": ""
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Gert de Pagter",
+ "email": "BackEndTea@gmail.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony polyfill for ctype functions",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "compatibility",
+ "ctype",
+ "polyfill",
+ "portable"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/polyfill-ctype/tree/v1.29.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2024-01-29T20:11:03+00:00"
+ },
+ {
+ "name": "symfony/polyfill-mbstring",
+ "version": "v1.29.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-mbstring.git",
+ "reference": "9773676c8a1bb1f8d4340a62efe641cf76eda7ec"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/9773676c8a1bb1f8d4340a62efe641cf76eda7ec",
+ "reference": "9773676c8a1bb1f8d4340a62efe641cf76eda7ec",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.1"
+ },
+ "provide": {
+ "ext-mbstring": "*"
+ },
+ "suggest": {
+ "ext-mbstring": "For best performance"
+ },
+ "type": "library",
+ "extra": {
+ "thanks": {
+ "name": "symfony/polyfill",
+ "url": "https://github.com/symfony/polyfill"
+ }
+ },
+ "autoload": {
+ "files": [
+ "bootstrap.php"
+ ],
+ "psr-4": {
+ "Symfony\\Polyfill\\Mbstring\\": ""
+ }
+ },
+ "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"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.29.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2024-01-29T20:11:03+00:00"
+ },
+ {
+ "name": "symfony/polyfill-php73",
+ "version": "v1.29.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-php73.git",
+ "reference": "21bd091060673a1177ae842c0ef8fe30893114d2"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/21bd091060673a1177ae842c0ef8fe30893114d2",
+ "reference": "21bd091060673a1177ae842c0ef8fe30893114d2",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.1"
+ },
+ "type": "library",
+ "extra": {
+ "thanks": {
+ "name": "symfony/polyfill",
+ "url": "https://github.com/symfony/polyfill"
+ }
+ },
+ "autoload": {
+ "files": [
+ "bootstrap.php"
+ ],
+ "psr-4": {
+ "Symfony\\Polyfill\\Php73\\": ""
+ },
+ "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"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/polyfill-php73/tree/v1.29.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2024-01-29T20:11:03+00:00"
+ },
+ {
+ "name": "symfony/polyfill-php80",
+ "version": "v1.29.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-php80.git",
+ "reference": "87b68208d5c1188808dd7839ee1e6c8ec3b02f1b"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/87b68208d5c1188808dd7839ee1e6c8ec3b02f1b",
+ "reference": "87b68208d5c1188808dd7839ee1e6c8ec3b02f1b",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.1"
+ },
+ "type": "library",
+ "extra": {
+ "thanks": {
+ "name": "symfony/polyfill",
+ "url": "https://github.com/symfony/polyfill"
+ }
+ },
+ "autoload": {
+ "files": [
+ "bootstrap.php"
+ ],
+ "psr-4": {
+ "Symfony\\Polyfill\\Php80\\": ""
+ },
+ "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"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/polyfill-php80/tree/v1.29.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2024-01-29T20:11:03+00:00"
+ },
+ {
+ "name": "symfony/polyfill-php81",
+ "version": "v1.29.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-php81.git",
+ "reference": "c565ad1e63f30e7477fc40738343c62b40bc672d"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/c565ad1e63f30e7477fc40738343c62b40bc672d",
+ "reference": "c565ad1e63f30e7477fc40738343c62b40bc672d",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.1"
+ },
+ "type": "library",
+ "extra": {
+ "thanks": {
+ "name": "symfony/polyfill",
+ "url": "https://github.com/symfony/polyfill"
+ }
+ },
+ "autoload": {
+ "files": [
+ "bootstrap.php"
+ ],
+ "psr-4": {
+ "Symfony\\Polyfill\\Php81\\": ""
+ },
+ "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 8.1+ features to lower PHP versions",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "compatibility",
+ "polyfill",
+ "portable",
+ "shim"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/polyfill-php81/tree/v1.29.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2024-01-29T20:11:03+00:00"
+ },
+ {
+ "name": "symfony/service-contracts",
+ "version": "v2.5.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/service-contracts.git",
+ "reference": "a2329596ddc8fd568900e3fc76cba42489ecc7f3"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/service-contracts/zipball/a2329596ddc8fd568900e3fc76cba42489ecc7f3",
+ "reference": "a2329596ddc8fd568900e3fc76cba42489ecc7f3",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2.5",
+ "psr/container": "^1.1",
+ "symfony/deprecation-contracts": "^2.1|^3"
+ },
+ "conflict": {
+ "ext-psr": "<1.1|>=2"
+ },
+ "suggest": {
+ "symfony/service-implementation": ""
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "2.5-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"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/service-contracts/tree/v2.5.3"
+ },
+ "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": "2023-04-21T15:04:16+00:00"
+ },
+ {
+ "name": "symfony/translation",
+ "version": "v5.4.39",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/translation.git",
+ "reference": "0fabede35e3985c4f96089edeeefe8313e15ca3a"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/translation/zipball/0fabede35e3985c4f96089edeeefe8313e15ca3a",
+ "reference": "0fabede35e3985c4f96089edeeefe8313e15ca3a",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2.5",
+ "symfony/deprecation-contracts": "^2.1|^3",
+ "symfony/polyfill-mbstring": "~1.0",
+ "symfony/polyfill-php80": "^1.16",
+ "symfony/translation-contracts": "^2.3"
+ },
+ "conflict": {
+ "symfony/config": "<4.4",
+ "symfony/console": "<5.3",
+ "symfony/dependency-injection": "<5.0",
+ "symfony/http-kernel": "<5.0",
+ "symfony/twig-bundle": "<5.0",
+ "symfony/yaml": "<4.4"
+ },
+ "provide": {
+ "symfony/translation-implementation": "2.3"
+ },
+ "require-dev": {
+ "psr/log": "^1|^2|^3",
+ "symfony/config": "^4.4|^5.0|^6.0",
+ "symfony/console": "^5.4|^6.0",
+ "symfony/dependency-injection": "^5.0|^6.0",
+ "symfony/finder": "^4.4|^5.0|^6.0",
+ "symfony/http-client-contracts": "^1.1|^2.0|^3.0",
+ "symfony/http-kernel": "^5.0|^6.0",
+ "symfony/intl": "^4.4|^5.0|^6.0",
+ "symfony/polyfill-intl-icu": "^1.21",
+ "symfony/service-contracts": "^1.1.2|^2|^3",
+ "symfony/yaml": "^4.4|^5.0|^6.0"
+ },
+ "suggest": {
+ "psr/log-implementation": "To use logging capability in translator",
+ "symfony/config": "",
+ "symfony/yaml": ""
+ },
+ "type": "library",
+ "autoload": {
+ "files": [
+ "Resources/functions.php"
+ ],
+ "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",
+ "support": {
+ "source": "https://github.com/symfony/translation/tree/v5.4.39"
+ },
+ "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": "2024-04-18T08:26:06+00:00"
+ },
+ {
+ "name": "symfony/translation-contracts",
+ "version": "v2.5.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/translation-contracts.git",
+ "reference": "b0073a77ac0b7ea55131020e87b1e3af540f4664"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/b0073a77ac0b7ea55131020e87b1e3af540f4664",
+ "reference": "b0073a77ac0b7ea55131020e87b1e3af540f4664",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2.5"
+ },
+ "suggest": {
+ "symfony/translation-implementation": ""
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "2.5-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"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/translation-contracts/tree/v2.5.3"
+ },
+ "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": "2024-01-23T13:51:25+00:00"
+ },
+ {
+ "name": "symfony/twig-bridge",
+ "version": "v5.4.39",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/twig-bridge.git",
+ "reference": "1384448132165f95f76ca67cd722c560e29b8245"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/twig-bridge/zipball/1384448132165f95f76ca67cd722c560e29b8245",
+ "reference": "1384448132165f95f76ca67cd722c560e29b8245",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2.5",
+ "symfony/polyfill-php80": "^1.16",
+ "symfony/translation-contracts": "^1.1|^2|^3",
+ "twig/twig": "^2.13|^3.0.4"
+ },
+ "conflict": {
+ "phpdocumentor/reflection-docblock": "<3.2.2",
+ "phpdocumentor/type-resolver": "<1.4.0",
+ "symfony/console": "<5.3",
+ "symfony/form": "<5.4.21|>=6,<6.2.7",
+ "symfony/http-foundation": "<5.3",
+ "symfony/http-kernel": "<4.4",
+ "symfony/translation": "<5.2",
+ "symfony/workflow": "<5.2"
+ },
+ "require-dev": {
+ "doctrine/annotations": "^1.12|^2",
+ "egulias/email-validator": "^2.1.10|^3|^4",
+ "phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0",
+ "symfony/asset": "^4.4|^5.0|^6.0",
+ "symfony/console": "^5.3|^6.0",
+ "symfony/dependency-injection": "^4.4|^5.0|^6.0",
+ "symfony/expression-language": "^4.4|^5.0|^6.0",
+ "symfony/finder": "^4.4|^5.0|^6.0",
+ "symfony/form": "^5.4.21|^6.2.7",
+ "symfony/http-foundation": "^5.3|^6.0",
+ "symfony/http-kernel": "^4.4|^5.0|^6.0",
+ "symfony/intl": "^4.4|^5.0|^6.0",
+ "symfony/mime": "^5.2|^6.0",
+ "symfony/polyfill-intl-icu": "~1.0",
+ "symfony/property-info": "^4.4|^5.1|^6.0",
+ "symfony/routing": "^4.4|^5.0|^6.0",
+ "symfony/security-acl": "^2.8|^3.0",
+ "symfony/security-core": "^4.4|^5.0|^6.0",
+ "symfony/security-csrf": "^4.4|^5.0|^6.0",
+ "symfony/security-http": "^4.4|^5.0|^6.0",
+ "symfony/serializer": "^5.4.35|~6.3.12|^6.4.3",
+ "symfony/stopwatch": "^4.4|^5.0|^6.0",
+ "symfony/translation": "^5.2|^6.0",
+ "symfony/web-link": "^4.4|^5.0|^6.0",
+ "symfony/workflow": "^5.2|^6.0",
+ "symfony/yaml": "^4.4|^5.0|^6.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/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",
+ "support": {
+ "source": "https://github.com/symfony/twig-bridge/tree/v5.4.39"
+ },
+ "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": "2024-04-18T08:26:06+00:00"
+ },
+ {
+ "name": "symfony/var-exporter",
+ "version": "v5.4.45",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/var-exporter.git",
+ "reference": "862700068db0ddfd8c5b850671e029a90246ec75"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/var-exporter/zipball/862700068db0ddfd8c5b850671e029a90246ec75",
+ "reference": "862700068db0ddfd8c5b850671e029a90246ec75",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2.5",
+ "symfony/polyfill-php80": "^1.16"
+ },
+ "require-dev": {
+ "symfony/var-dumper": "^4.4.9|^5.0.9|^6.0"
+ },
+ "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"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/var-exporter/tree/v5.4.45"
+ },
+ "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": "2024-09-25T14:11:13+00:00"
+ },
+ {
+ "name": "tracy/tracy",
+ "version": "v2.10.7",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/nette/tracy.git",
+ "reference": "7e7b25ba103968d5318d37db330b2e9c755dc765"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/nette/tracy/zipball/7e7b25ba103968d5318d37db330b2e9c755dc765",
+ "reference": "7e7b25ba103968d5318d37db330b2e9c755dc765",
+ "shasum": ""
+ },
+ "require": {
+ "ext-json": "*",
+ "ext-session": "*",
+ "php": ">=8.0 <8.4"
+ },
+ "conflict": {
+ "nette/di": "<3.0"
+ },
+ "require-dev": {
+ "latte/latte": "^2.5",
+ "nette/di": "^3.0",
+ "nette/http": "^3.0",
+ "nette/mail": "^3.0",
+ "nette/tester": "^2.2",
+ "nette/utils": "^3.0",
+ "phpstan/phpstan": "^1.0",
+ "psr/log": "^1.0 || ^2.0 || ^3.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.10-dev"
+ }
+ },
+ "autoload": {
+ "files": [
+ "src/Tracy/functions.php"
+ ],
+ "classmap": [
+ "src"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "David Grudl",
+ "homepage": "https://davidgrudl.com"
+ },
+ {
+ "name": "Nette Community",
+ "homepage": "https://nette.org/contributors"
+ }
+ ],
+ "description": "😎 Tracy: the addictive tool to ease debugging PHP code for cool developers. Friendly design, logging, profiler, advanced features like debugging AJAX calls or CLI support. You will love it.",
+ "homepage": "https://tracy.nette.org",
+ "keywords": [
+ "Xdebug",
+ "debug",
+ "debugger",
+ "nette",
+ "profiler"
+ ],
+ "support": {
+ "issues": "https://github.com/nette/tracy/issues",
+ "source": "https://github.com/nette/tracy/tree/v2.10.7"
+ },
+ "time": "2024-04-29T11:44:00+00:00"
+ },
+ {
+ "name": "twig/intl-extra",
+ "version": "v3.10.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/twigphp/intl-extra.git",
+ "reference": "693f6beb8ca91fc6323e01b3addf983812f65c93"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/twigphp/intl-extra/zipball/693f6beb8ca91fc6323e01b3addf983812f65c93",
+ "reference": "693f6beb8ca91fc6323e01b3addf983812f65c93",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2.5",
+ "symfony/intl": "^5.4|^6.4|^7.0",
+ "twig/twig": "^3.10"
+ },
+ "require-dev": {
+ "symfony/phpunit-bridge": "^6.4|^7.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Twig\\Extra\\Intl\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com",
+ "homepage": "http://fabien.potencier.org",
+ "role": "Lead Developer"
+ }
+ ],
+ "description": "A Twig extension for Intl",
+ "homepage": "https://twig.symfony.com",
+ "keywords": [
+ "intl",
+ "twig"
+ ],
+ "support": {
+ "source": "https://github.com/twigphp/intl-extra/tree/v3.10.0"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/twig/twig",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2024-05-11T07:35:57+00:00"
+ },
+ {
+ "name": "twig/twig",
+ "version": "v3.10.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/twigphp/Twig.git",
+ "reference": "7aaed0b8311a557cc8c4047a71fd03153a00e755"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/twigphp/Twig/zipball/7aaed0b8311a557cc8c4047a71fd03153a00e755",
+ "reference": "7aaed0b8311a557cc8c4047a71fd03153a00e755",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2.5",
+ "symfony/deprecation-contracts": "^2.5|^3",
+ "symfony/polyfill-ctype": "^1.8",
+ "symfony/polyfill-mbstring": "^1.3",
+ "symfony/polyfill-php80": "^1.22"
+ },
+ "require-dev": {
+ "psr/container": "^1.0|^2.0",
+ "symfony/phpunit-bridge": "^5.4.9|^6.4|^7.0"
+ },
+ "type": "library",
+ "autoload": {
+ "files": [
+ "src/Resources/core.php",
+ "src/Resources/debug.php",
+ "src/Resources/escaper.php",
+ "src/Resources/string_loader.php"
+ ],
+ "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"
+ ],
+ "support": {
+ "issues": "https://github.com/twigphp/Twig/issues",
+ "source": "https://github.com/twigphp/Twig/tree/v3.10.2"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/twig/twig",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2024-05-14T06:04:16+00:00"
+ }
+ ],
+ "packages-dev": [
+ {
+ "name": "composer/pcre",
+ "version": "3.1.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/composer/pcre.git",
+ "reference": "5b16e25a5355f1f3afdfc2f954a0a80aec4826a8"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/composer/pcre/zipball/5b16e25a5355f1f3afdfc2f954a0a80aec4826a8",
+ "reference": "5b16e25a5355f1f3afdfc2f954a0a80aec4826a8",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.4 || ^8.0"
+ },
+ "require-dev": {
+ "phpstan/phpstan": "^1.3",
+ "phpstan/phpstan-strict-rules": "^1.1",
+ "symfony/phpunit-bridge": "^5"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "3.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Composer\\Pcre\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Jordi Boggiano",
+ "email": "j.boggiano@seld.be",
+ "homepage": "http://seld.be"
+ }
+ ],
+ "description": "PCRE wrapping library that offers type-safe preg_* replacements.",
+ "keywords": [
+ "PCRE",
+ "preg",
+ "regex",
+ "regular expression"
+ ],
+ "support": {
+ "issues": "https://github.com/composer/pcre/issues",
+ "source": "https://github.com/composer/pcre/tree/3.1.3"
+ },
+ "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": "2024-03-19T10:26:25+00:00"
+ },
+ {
+ "name": "composer/xdebug-handler",
+ "version": "3.0.5",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/composer/xdebug-handler.git",
+ "reference": "6c1925561632e83d60a44492e0b344cf48ab85ef"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/6c1925561632e83d60a44492e0b344cf48ab85ef",
+ "reference": "6c1925561632e83d60a44492e0b344cf48ab85ef",
+ "shasum": ""
+ },
+ "require": {
+ "composer/pcre": "^1 || ^2 || ^3",
+ "php": "^7.2.5 || ^8.0",
+ "psr/log": "^1 || ^2 || ^3"
+ },
+ "require-dev": {
+ "phpstan/phpstan": "^1.0",
+ "phpstan/phpstan-strict-rules": "^1.1",
+ "phpunit/phpunit": "^8.5 || ^9.6 || ^10.5"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Composer\\XdebugHandler\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "John Stevenson",
+ "email": "john-stevenson@blueyonder.co.uk"
+ }
+ ],
+ "description": "Restarts a process without Xdebug.",
+ "keywords": [
+ "Xdebug",
+ "performance"
+ ],
+ "support": {
+ "irc": "ircs://irc.libera.chat:6697/composer",
+ "issues": "https://github.com/composer/xdebug-handler/issues",
+ "source": "https://github.com/composer/xdebug-handler/tree/3.0.5"
+ },
+ "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": "2024-05-06T16:37:16+00:00"
+ },
+ {
+ "name": "doctrine/deprecations",
+ "version": "1.1.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/doctrine/deprecations.git",
+ "reference": "dfbaa3c2d2e9a9df1118213f3b8b0c597bb99fab"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/doctrine/deprecations/zipball/dfbaa3c2d2e9a9df1118213f3b8b0c597bb99fab",
+ "reference": "dfbaa3c2d2e9a9df1118213f3b8b0c597bb99fab",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.1 || ^8.0"
+ },
+ "require-dev": {
+ "doctrine/coding-standard": "^9",
+ "phpstan/phpstan": "1.4.10 || 1.10.15",
+ "phpstan/phpstan-phpunit": "^1.0",
+ "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5",
+ "psalm/plugin-phpunit": "0.18.4",
+ "psr/log": "^1 || ^2 || ^3",
+ "vimeo/psalm": "4.30.0 || 5.12.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/",
+ "support": {
+ "issues": "https://github.com/doctrine/deprecations/issues",
+ "source": "https://github.com/doctrine/deprecations/tree/1.1.3"
+ },
+ "time": "2024-01-30T19:34:25+00:00"
+ },
+ {
+ "name": "doctrine/instantiator",
+ "version": "1.5.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/doctrine/instantiator.git",
+ "reference": "0a0fa9780f5d4e507415a065172d26a98d02047b"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/doctrine/instantiator/zipball/0a0fa9780f5d4e507415a065172d26a98d02047b",
+ "reference": "0a0fa9780f5d4e507415a065172d26a98d02047b",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.1 || ^8.0"
+ },
+ "require-dev": {
+ "doctrine/coding-standard": "^9 || ^11",
+ "ext-pdo": "*",
+ "ext-phar": "*",
+ "phpbench/phpbench": "^0.16 || ^1",
+ "phpstan/phpstan": "^1.4",
+ "phpstan/phpstan-phpunit": "^1",
+ "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5",
+ "vimeo/psalm": "^4.30 || ^5.4"
+ },
+ "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"
+ ],
+ "support": {
+ "issues": "https://github.com/doctrine/instantiator/issues",
+ "source": "https://github.com/doctrine/instantiator/tree/1.5.0"
+ },
+ "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": "2022-12-30T00:15:36+00:00"
+ },
+ {
+ "name": "helmich/phpunit-json-assert",
+ "version": "v3.5.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/martin-helmich/phpunit-json-assert.git",
+ "reference": "5a3a53e46f4d1f1b324c5cc2e33d87ad2d37260c"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/martin-helmich/phpunit-json-assert/zipball/5a3a53e46f4d1f1b324c5cc2e33d87ad2d37260c",
+ "reference": "5a3a53e46f4d1f1b324c5cc2e33d87ad2d37260c",
+ "shasum": ""
+ },
+ "require": {
+ "justinrainbow/json-schema": "^5.0",
+ "php": "^8.0",
+ "softcreatr/jsonpath": "^0.8"
+ },
+ "conflict": {
+ "phpunit/phpunit": "<8.0 || >= 11.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^8.0 || ^9.0 || ^10.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Helmich\\JsonAssert\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Martin Helmich",
+ "email": "m.helmich@mittwald.de"
+ }
+ ],
+ "description": "PHPUnit assertions for JSON documents",
+ "support": {
+ "issues": "https://github.com/martin-helmich/phpunit-json-assert/issues",
+ "source": "https://github.com/martin-helmich/phpunit-json-assert/tree/v3.5.1"
+ },
+ "funding": [
+ {
+ "url": "https://donate.helmich.me",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/martin-helmich",
+ "type": "github"
+ }
+ ],
+ "time": "2023-03-03T14:09:38+00:00"
+ },
+ {
+ "name": "helmich/phpunit-psr7-assert",
+ "version": "v4.4.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/martin-helmich/phpunit-psr7-assert.git",
+ "reference": "f35fa69e07cc16977b52805d3abd873cc16747fd"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/martin-helmich/phpunit-psr7-assert/zipball/f35fa69e07cc16977b52805d3abd873cc16747fd",
+ "reference": "f35fa69e07cc16977b52805d3abd873cc16747fd",
+ "shasum": ""
+ },
+ "require": {
+ "helmich/phpunit-json-assert": "^3.4",
+ "php": "^8.0",
+ "psr/http-message": "^1.1 || ^2.0"
+ },
+ "conflict": {
+ "phpunit/phpunit": "<8.0 || >= 11.0"
+ },
+ "require-dev": {
+ "guzzlehttp/psr7": "^2.4",
+ "mockery/mockery": "^1.4.1",
+ "phpunit/phpunit": "^8.0 || ^9.0 || ^10.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Helmich\\Psr7Assert\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Martin Helmich",
+ "email": "m.helmich@mittwald.de"
+ }
+ ],
+ "description": "PHPUnit assertions for testing PSR7-compliant applications",
+ "support": {
+ "issues": "https://github.com/martin-helmich/phpunit-psr7-assert/issues",
+ "source": "https://github.com/martin-helmich/phpunit-psr7-assert/tree/v4.4.1"
+ },
+ "funding": [
+ {
+ "url": "https://donate.helmich.me",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/martin-helmich",
+ "type": "github"
+ }
+ ],
+ "time": "2023-07-26T19:04:29+00:00"
+ },
+ {
+ "name": "justinrainbow/json-schema",
+ "version": "v5.2.13",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/justinrainbow/json-schema.git",
+ "reference": "fbbe7e5d79f618997bc3332a6f49246036c45793"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/justinrainbow/json-schema/zipball/fbbe7e5d79f618997bc3332a6f49246036c45793",
+ "reference": "fbbe7e5d79f618997bc3332a6f49246036c45793",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.3"
+ },
+ "require-dev": {
+ "friendsofphp/php-cs-fixer": "~2.2.20||~2.15.1",
+ "json-schema/json-schema-test-suite": "1.2.0",
+ "phpunit/phpunit": "^4.8.35"
+ },
+ "bin": [
+ "bin/validate-json"
+ ],
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "5.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "JsonSchema\\": "src/JsonSchema/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Bruno Prieto Reis",
+ "email": "bruno.p.reis@gmail.com"
+ },
+ {
+ "name": "Justin Rainbow",
+ "email": "justin.rainbow@gmail.com"
+ },
+ {
+ "name": "Igor Wiedler",
+ "email": "igor@wiedler.ch"
+ },
+ {
+ "name": "Robert Schönthal",
+ "email": "seroscho@googlemail.com"
+ }
+ ],
+ "description": "A library to validate a json schema.",
+ "homepage": "https://github.com/justinrainbow/json-schema",
+ "keywords": [
+ "json",
+ "schema"
+ ],
+ "support": {
+ "issues": "https://github.com/justinrainbow/json-schema/issues",
+ "source": "https://github.com/justinrainbow/json-schema/tree/v5.2.13"
+ },
+ "time": "2023-09-26T02:20:38+00:00"
+ },
+ {
+ "name": "myclabs/deep-copy",
+ "version": "1.11.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/myclabs/DeepCopy.git",
+ "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/7284c22080590fb39f2ffa3e9057f10a4ddd0e0c",
+ "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.1 || ^8.0"
+ },
+ "conflict": {
+ "doctrine/collections": "<1.6.8",
+ "doctrine/common": "<2.13.3 || >=3,<3.2.2"
+ },
+ "require-dev": {
+ "doctrine/collections": "^1.6.8",
+ "doctrine/common": "^2.13.3 || ^3.2.2",
+ "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13"
+ },
+ "type": "library",
+ "autoload": {
+ "files": [
+ "src/DeepCopy/deep_copy.php"
+ ],
+ "psr-4": {
+ "DeepCopy\\": "src/DeepCopy/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "Create deep copies (clones) of your objects",
+ "keywords": [
+ "clone",
+ "copy",
+ "duplicate",
+ "object",
+ "object graph"
+ ],
+ "support": {
+ "issues": "https://github.com/myclabs/DeepCopy/issues",
+ "source": "https://github.com/myclabs/DeepCopy/tree/1.11.1"
+ },
+ "funding": [
+ {
+ "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2023-03-08T13:26:56+00:00"
+ },
+ {
+ "name": "nikic/php-parser",
+ "version": "v5.0.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/nikic/PHP-Parser.git",
+ "reference": "139676794dc1e9231bf7bcd123cfc0c99182cb13"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/139676794dc1e9231bf7bcd123cfc0c99182cb13",
+ "reference": "139676794dc1e9231bf7bcd123cfc0c99182cb13",
+ "shasum": ""
+ },
+ "require": {
+ "ext-ctype": "*",
+ "ext-json": "*",
+ "ext-tokenizer": "*",
+ "php": ">=7.4"
+ },
+ "require-dev": {
+ "ircmaxell/php-yacc": "^0.0.7",
+ "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0"
+ },
+ "bin": [
+ "bin/php-parse"
+ ],
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "5.0-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"
+ ],
+ "support": {
+ "issues": "https://github.com/nikic/PHP-Parser/issues",
+ "source": "https://github.com/nikic/PHP-Parser/tree/v5.0.2"
+ },
+ "time": "2024-03-05T20:51:40+00:00"
+ },
+ {
+ "name": "pdepend/pdepend",
+ "version": "2.16.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/pdepend/pdepend.git",
+ "reference": "f942b208dc2a0868454d01b29f0c75bbcfc6ed58"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/pdepend/pdepend/zipball/f942b208dc2a0868454d01b29f0c75bbcfc6ed58",
+ "reference": "f942b208dc2a0868454d01b29f0c75bbcfc6ed58",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.7",
+ "symfony/config": "^2.3.0|^3|^4|^5|^6.0|^7.0",
+ "symfony/dependency-injection": "^2.3.0|^3|^4|^5|^6.0|^7.0",
+ "symfony/filesystem": "^2.3.0|^3|^4|^5|^6.0|^7.0",
+ "symfony/polyfill-mbstring": "^1.19"
+ },
+ "require-dev": {
+ "easy-doc/easy-doc": "0.0.0|^1.2.3",
+ "gregwar/rst": "^1.0",
+ "squizlabs/php_codesniffer": "^2.0.0"
+ },
+ "bin": [
+ "src/bin/pdepend"
+ ],
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "PDepend\\": "src/main/php/PDepend"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "description": "Official version of pdepend to be handled with Composer",
+ "keywords": [
+ "PHP Depend",
+ "PHP_Depend",
+ "dev",
+ "pdepend"
+ ],
+ "support": {
+ "issues": "https://github.com/pdepend/pdepend/issues",
+ "source": "https://github.com/pdepend/pdepend/tree/2.16.2"
+ },
+ "funding": [
+ {
+ "url": "https://tidelift.com/funding/github/packagist/pdepend/pdepend",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2023-12-17T18:09:59+00:00"
+ },
+ {
+ "name": "phar-io/manifest",
+ "version": "2.0.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/phar-io/manifest.git",
+ "reference": "54750ef60c58e43759730615a392c31c80e23176"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176",
+ "reference": "54750ef60c58e43759730615a392c31c80e23176",
+ "shasum": ""
+ },
+ "require": {
+ "ext-dom": "*",
+ "ext-libxml": "*",
+ "ext-phar": "*",
+ "ext-xmlwriter": "*",
+ "phar-io/version": "^3.0.1",
+ "php": "^7.2 || ^8.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.0.x-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Arne Blankerts",
+ "email": "arne@blankerts.de",
+ "role": "Developer"
+ },
+ {
+ "name": "Sebastian Heuer",
+ "email": "sebastian@phpeople.de",
+ "role": "Developer"
+ },
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "Developer"
+ }
+ ],
+ "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)",
+ "support": {
+ "issues": "https://github.com/phar-io/manifest/issues",
+ "source": "https://github.com/phar-io/manifest/tree/2.0.4"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/theseer",
+ "type": "github"
+ }
+ ],
+ "time": "2024-03-03T12:33:53+00:00"
+ },
+ {
+ "name": "phar-io/version",
+ "version": "3.2.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/phar-io/version.git",
+ "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74",
+ "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.2 || ^8.0"
+ },
+ "type": "library",
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Arne Blankerts",
+ "email": "arne@blankerts.de",
+ "role": "Developer"
+ },
+ {
+ "name": "Sebastian Heuer",
+ "email": "sebastian@phpeople.de",
+ "role": "Developer"
+ },
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "Developer"
+ }
+ ],
+ "description": "Library for handling version information and constraints",
+ "support": {
+ "issues": "https://github.com/phar-io/version/issues",
+ "source": "https://github.com/phar-io/version/tree/3.2.1"
+ },
+ "time": "2022-02-21T01:04:05+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"
+ ],
+ "support": {
+ "issues": "https://github.com/phpDocumentor/ReflectionCommon/issues",
+ "source": "https://github.com/phpDocumentor/ReflectionCommon/tree/2.x"
+ },
+ "time": "2020-06-27T09:03:43+00:00"
+ },
+ {
+ "name": "phpdocumentor/reflection-docblock",
+ "version": "5.4.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git",
+ "reference": "9d07b3f7fdcf5efec5d1609cba3c19c5ea2bdc9c"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/9d07b3f7fdcf5efec5d1609cba3c19c5ea2bdc9c",
+ "reference": "9d07b3f7fdcf5efec5d1609cba3c19c5ea2bdc9c",
+ "shasum": ""
+ },
+ "require": {
+ "doctrine/deprecations": "^1.1",
+ "ext-filter": "*",
+ "php": "^7.4 || ^8.0",
+ "phpdocumentor/reflection-common": "^2.2",
+ "phpdocumentor/type-resolver": "^1.7",
+ "phpstan/phpdoc-parser": "^1.7",
+ "webmozart/assert": "^1.9.1"
+ },
+ "require-dev": {
+ "mockery/mockery": "~1.3.5",
+ "phpstan/extension-installer": "^1.1",
+ "phpstan/phpstan": "^1.8",
+ "phpstan/phpstan-mockery": "^1.1",
+ "phpstan/phpstan-webmozart-assert": "^1.2",
+ "phpunit/phpunit": "^9.5",
+ "vimeo/psalm": "^5.13"
+ },
+ "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": "opensource@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.",
+ "support": {
+ "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues",
+ "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/5.4.1"
+ },
+ "time": "2024-05-21T05:55:05+00:00"
+ },
+ {
+ "name": "phpdocumentor/type-resolver",
+ "version": "1.8.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/phpDocumentor/TypeResolver.git",
+ "reference": "153ae662783729388a584b4361f2545e4d841e3c"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/153ae662783729388a584b4361f2545e4d841e3c",
+ "reference": "153ae662783729388a584b4361f2545e4d841e3c",
+ "shasum": ""
+ },
+ "require": {
+ "doctrine/deprecations": "^1.0",
+ "php": "^7.3 || ^8.0",
+ "phpdocumentor/reflection-common": "^2.0",
+ "phpstan/phpdoc-parser": "^1.13"
+ },
+ "require-dev": {
+ "ext-tokenizer": "*",
+ "phpbench/phpbench": "^1.2",
+ "phpstan/extension-installer": "^1.1",
+ "phpstan/phpstan": "^1.8",
+ "phpstan/phpstan-phpunit": "^1.1",
+ "phpunit/phpunit": "^9.5",
+ "rector/rector": "^0.13.9",
+ "vimeo/psalm": "^4.25"
+ },
+ "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",
+ "support": {
+ "issues": "https://github.com/phpDocumentor/TypeResolver/issues",
+ "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.8.2"
+ },
+ "time": "2024-02-23T11:10:43+00:00"
+ },
+ {
+ "name": "phpmd/phpmd",
+ "version": "2.15.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/phpmd/phpmd.git",
+ "reference": "74a1f56e33afad4128b886e334093e98e1b5e7c0"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phpmd/phpmd/zipball/74a1f56e33afad4128b886e334093e98e1b5e7c0",
+ "reference": "74a1f56e33afad4128b886e334093e98e1b5e7c0",
+ "shasum": ""
+ },
+ "require": {
+ "composer/xdebug-handler": "^1.0 || ^2.0 || ^3.0",
+ "ext-xml": "*",
+ "pdepend/pdepend": "^2.16.1",
+ "php": ">=5.3.9"
+ },
+ "require-dev": {
+ "easy-doc/easy-doc": "0.0.0 || ^1.3.2",
+ "ext-json": "*",
+ "ext-simplexml": "*",
+ "gregwar/rst": "^1.0",
+ "mikey179/vfsstream": "^1.6.8",
+ "squizlabs/php_codesniffer": "^2.9.2 || ^3.7.2"
+ },
+ "bin": [
+ "src/bin/phpmd"
+ ],
+ "type": "library",
+ "autoload": {
+ "psr-0": {
+ "PHPMD\\": "src/main/php"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Manuel Pichler",
+ "email": "github@manuel-pichler.de",
+ "homepage": "https://github.com/manuelpichler",
+ "role": "Project Founder"
+ },
+ {
+ "name": "Marc Würth",
+ "email": "ravage@bluewin.ch",
+ "homepage": "https://github.com/ravage84",
+ "role": "Project Maintainer"
+ },
+ {
+ "name": "Other contributors",
+ "homepage": "https://github.com/phpmd/phpmd/graphs/contributors",
+ "role": "Contributors"
+ }
+ ],
+ "description": "PHPMD is a spin-off project of PHP Depend and aims to be a PHP equivalent of the well known Java tool PMD.",
+ "homepage": "https://phpmd.org/",
+ "keywords": [
+ "dev",
+ "mess detection",
+ "mess detector",
+ "pdepend",
+ "phpmd",
+ "pmd"
+ ],
+ "support": {
+ "irc": "irc://irc.freenode.org/phpmd",
+ "issues": "https://github.com/phpmd/phpmd/issues",
+ "source": "https://github.com/phpmd/phpmd/tree/2.15.0"
+ },
+ "funding": [
+ {
+ "url": "https://tidelift.com/funding/github/packagist/phpmd/phpmd",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2023-12-11T08:22:20+00:00"
+ },
+ {
+ "name": "phpspec/prophecy",
+ "version": "v1.19.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/phpspec/prophecy.git",
+ "reference": "67a759e7d8746d501c41536ba40cd9c0a07d6a87"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phpspec/prophecy/zipball/67a759e7d8746d501c41536ba40cd9c0a07d6a87",
+ "reference": "67a759e7d8746d501c41536ba40cd9c0a07d6a87",
+ "shasum": ""
+ },
+ "require": {
+ "doctrine/instantiator": "^1.2 || ^2.0",
+ "php": "^7.2 || 8.0.* || 8.1.* || 8.2.* || 8.3.*",
+ "phpdocumentor/reflection-docblock": "^5.2",
+ "sebastian/comparator": "^3.0 || ^4.0 || ^5.0 || ^6.0",
+ "sebastian/recursion-context": "^3.0 || ^4.0 || ^5.0 || ^6.0"
+ },
+ "require-dev": {
+ "phpspec/phpspec": "^6.0 || ^7.0",
+ "phpstan/phpstan": "^1.9",
+ "phpunit/phpunit": "^8.0 || ^9.0 || ^10.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Prophecy\\": "src/Prophecy"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Konstantin Kudryashov",
+ "email": "ever.zet@gmail.com",
+ "homepage": "http://everzet.com"
+ },
+ {
+ "name": "Marcello Duarte",
+ "email": "marcello.duarte@gmail.com"
+ }
+ ],
+ "description": "Highly opinionated mocking framework for PHP 5.3+",
+ "homepage": "https://github.com/phpspec/prophecy",
+ "keywords": [
+ "Double",
+ "Dummy",
+ "dev",
+ "fake",
+ "mock",
+ "spy",
+ "stub"
+ ],
+ "support": {
+ "issues": "https://github.com/phpspec/prophecy/issues",
+ "source": "https://github.com/phpspec/prophecy/tree/v1.19.0"
+ },
+ "time": "2024-02-29T11:52:51+00:00"
+ },
+ {
+ "name": "phpspec/prophecy-phpunit",
+ "version": "v2.2.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/phpspec/prophecy-phpunit.git",
+ "reference": "16e1247e139434bce0bac09848bc5c8d882940fc"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phpspec/prophecy-phpunit/zipball/16e1247e139434bce0bac09848bc5c8d882940fc",
+ "reference": "16e1247e139434bce0bac09848bc5c8d882940fc",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.3 || ^8",
+ "phpspec/prophecy": "^1.18",
+ "phpunit/phpunit": "^9.1 || ^10.1 || ^11.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Prophecy\\PhpUnit\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Christophe Coevoet",
+ "email": "stof@notk.org"
+ }
+ ],
+ "description": "Integrating the Prophecy mocking library in PHPUnit test cases",
+ "homepage": "http://phpspec.net",
+ "keywords": [
+ "phpunit",
+ "prophecy"
+ ],
+ "support": {
+ "issues": "https://github.com/phpspec/prophecy-phpunit/issues",
+ "source": "https://github.com/phpspec/prophecy-phpunit/tree/v2.2.0"
+ },
+ "time": "2024-03-01T08:33:58+00:00"
+ },
+ {
+ "name": "phpstan/phpdoc-parser",
+ "version": "1.29.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/phpstan/phpdoc-parser.git",
+ "reference": "536889f2b340489d328f5ffb7b02bb6b183ddedc"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/536889f2b340489d328f5ffb7b02bb6b183ddedc",
+ "reference": "536889f2b340489d328f5ffb7b02bb6b183ddedc",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.2 || ^8.0"
+ },
+ "require-dev": {
+ "doctrine/annotations": "^2.0",
+ "nikic/php-parser": "^4.15",
+ "php-parallel-lint/php-parallel-lint": "^1.2",
+ "phpstan/extension-installer": "^1.0",
+ "phpstan/phpstan": "^1.5",
+ "phpstan/phpstan-phpunit": "^1.1",
+ "phpstan/phpstan-strict-rules": "^1.0",
+ "phpunit/phpunit": "^9.5",
+ "symfony/process": "^5.2"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "PHPStan\\PhpDocParser\\": [
+ "src/"
+ ]
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "PHPDoc parser with support for nullable, intersection and generic types",
+ "support": {
+ "issues": "https://github.com/phpstan/phpdoc-parser/issues",
+ "source": "https://github.com/phpstan/phpdoc-parser/tree/1.29.0"
+ },
+ "time": "2024-05-06T12:04:23+00:00"
+ },
+ {
+ "name": "phpunit/php-code-coverage",
+ "version": "9.2.31",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/php-code-coverage.git",
+ "reference": "48c34b5d8d983006bd2adc2d0de92963b9155965"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/48c34b5d8d983006bd2adc2d0de92963b9155965",
+ "reference": "48c34b5d8d983006bd2adc2d0de92963b9155965",
+ "shasum": ""
+ },
+ "require": {
+ "ext-dom": "*",
+ "ext-libxml": "*",
+ "ext-xmlwriter": "*",
+ "nikic/php-parser": "^4.18 || ^5.0",
+ "php": ">=7.3",
+ "phpunit/php-file-iterator": "^3.0.3",
+ "phpunit/php-text-template": "^2.0.2",
+ "sebastian/code-unit-reverse-lookup": "^2.0.2",
+ "sebastian/complexity": "^2.0",
+ "sebastian/environment": "^5.1.2",
+ "sebastian/lines-of-code": "^1.0.3",
+ "sebastian/version": "^3.0.1",
+ "theseer/tokenizer": "^1.2.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "suggest": {
+ "ext-pcov": "PHP extension that provides line coverage",
+ "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "9.2-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.",
+ "homepage": "https://github.com/sebastianbergmann/php-code-coverage",
+ "keywords": [
+ "coverage",
+ "testing",
+ "xunit"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues",
+ "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy",
+ "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.31"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2024-03-02T06:37:42+00:00"
+ },
+ {
+ "name": "phpunit/php-file-iterator",
+ "version": "3.0.6",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/php-file-iterator.git",
+ "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf",
+ "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "3.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "FilterIterator implementation that filters files based on a list of suffixes.",
+ "homepage": "https://github.com/sebastianbergmann/php-file-iterator/",
+ "keywords": [
+ "filesystem",
+ "iterator"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues",
+ "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/3.0.6"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2021-12-02T12:48:52+00:00"
+ },
+ {
+ "name": "phpunit/php-invoker",
+ "version": "3.1.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/php-invoker.git",
+ "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/5a10147d0aaf65b58940a0b72f71c9ac0423cc67",
+ "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "ext-pcntl": "*",
+ "phpunit/phpunit": "^9.3"
+ },
+ "suggest": {
+ "ext-pcntl": "*"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "3.1-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Invoke callables with a timeout",
+ "homepage": "https://github.com/sebastianbergmann/php-invoker/",
+ "keywords": [
+ "process"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/php-invoker/issues",
+ "source": "https://github.com/sebastianbergmann/php-invoker/tree/3.1.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-09-28T05:58:55+00:00"
+ },
+ {
+ "name": "phpunit/php-text-template",
+ "version": "2.0.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/php-text-template.git",
+ "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28",
+ "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Simple template engine.",
+ "homepage": "https://github.com/sebastianbergmann/php-text-template/",
+ "keywords": [
+ "template"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/php-text-template/issues",
+ "source": "https://github.com/sebastianbergmann/php-text-template/tree/2.0.4"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-10-26T05:33:50+00:00"
+ },
+ {
+ "name": "phpunit/php-timer",
+ "version": "5.0.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/php-timer.git",
+ "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2",
+ "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "5.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Utility class for timing",
+ "homepage": "https://github.com/sebastianbergmann/php-timer/",
+ "keywords": [
+ "timer"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/php-timer/issues",
+ "source": "https://github.com/sebastianbergmann/php-timer/tree/5.0.3"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-10-26T13:16:10+00:00"
+ },
+ {
+ "name": "phpunit/phpunit",
+ "version": "9.6.19",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/phpunit.git",
+ "reference": "a1a54a473501ef4cdeaae4e06891674114d79db8"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/a1a54a473501ef4cdeaae4e06891674114d79db8",
+ "reference": "a1a54a473501ef4cdeaae4e06891674114d79db8",
+ "shasum": ""
+ },
+ "require": {
+ "doctrine/instantiator": "^1.3.1 || ^2",
+ "ext-dom": "*",
+ "ext-json": "*",
+ "ext-libxml": "*",
+ "ext-mbstring": "*",
+ "ext-xml": "*",
+ "ext-xmlwriter": "*",
+ "myclabs/deep-copy": "^1.10.1",
+ "phar-io/manifest": "^2.0.3",
+ "phar-io/version": "^3.0.2",
+ "php": ">=7.3",
+ "phpunit/php-code-coverage": "^9.2.28",
+ "phpunit/php-file-iterator": "^3.0.5",
+ "phpunit/php-invoker": "^3.1.1",
+ "phpunit/php-text-template": "^2.0.3",
+ "phpunit/php-timer": "^5.0.2",
+ "sebastian/cli-parser": "^1.0.1",
+ "sebastian/code-unit": "^1.0.6",
+ "sebastian/comparator": "^4.0.8",
+ "sebastian/diff": "^4.0.3",
+ "sebastian/environment": "^5.1.3",
+ "sebastian/exporter": "^4.0.5",
+ "sebastian/global-state": "^5.0.1",
+ "sebastian/object-enumerator": "^4.0.3",
+ "sebastian/resource-operations": "^3.0.3",
+ "sebastian/type": "^3.2",
+ "sebastian/version": "^3.0.2"
+ },
+ "suggest": {
+ "ext-soap": "To be able to generate mocks based on WSDL files",
+ "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage"
+ },
+ "bin": [
+ "phpunit"
+ ],
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "9.6-dev"
+ }
+ },
+ "autoload": {
+ "files": [
+ "src/Framework/Assert/Functions.php"
+ ],
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "The PHP Unit Testing framework.",
+ "homepage": "https://phpunit.de/",
+ "keywords": [
+ "phpunit",
+ "testing",
+ "xunit"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/phpunit/issues",
+ "security": "https://github.com/sebastianbergmann/phpunit/security/policy",
+ "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.19"
+ },
+ "funding": [
+ {
+ "url": "https://phpunit.de/sponsors.html",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2024-04-05T04:35:58+00:00"
+ },
+ {
+ "name": "sebastian/cli-parser",
+ "version": "1.0.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/cli-parser.git",
+ "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/2b56bea83a09de3ac06bb18b92f068e60cc6f50b",
+ "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Library for parsing CLI options",
+ "homepage": "https://github.com/sebastianbergmann/cli-parser",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/cli-parser/issues",
+ "source": "https://github.com/sebastianbergmann/cli-parser/tree/1.0.2"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2024-03-02T06:27:43+00:00"
+ },
+ {
+ "name": "sebastian/code-unit",
+ "version": "1.0.8",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/code-unit.git",
+ "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/1fc9f64c0927627ef78ba436c9b17d967e68e120",
+ "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Collection of value objects that represent the PHP code units",
+ "homepage": "https://github.com/sebastianbergmann/code-unit",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/code-unit/issues",
+ "source": "https://github.com/sebastianbergmann/code-unit/tree/1.0.8"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-10-26T13:08:54+00:00"
+ },
+ {
+ "name": "sebastian/code-unit-reverse-lookup",
+ "version": "2.0.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git",
+ "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5",
+ "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ }
+ ],
+ "description": "Looks up which function or method a line of code belongs to",
+ "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues",
+ "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/2.0.3"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-09-28T05:30:19+00:00"
+ },
+ {
+ "name": "sebastian/comparator",
+ "version": "4.0.8",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/comparator.git",
+ "reference": "fa0f136dd2334583309d32b62544682ee972b51a"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/fa0f136dd2334583309d32b62544682ee972b51a",
+ "reference": "fa0f136dd2334583309d32b62544682ee972b51a",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3",
+ "sebastian/diff": "^4.0",
+ "sebastian/exporter": "^4.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "4.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ },
+ {
+ "name": "Jeff Welch",
+ "email": "whatthejeff@gmail.com"
+ },
+ {
+ "name": "Volker Dusch",
+ "email": "github@wallbash.com"
+ },
+ {
+ "name": "Bernhard Schussek",
+ "email": "bschussek@2bepublished.at"
+ }
+ ],
+ "description": "Provides the functionality to compare PHP values for equality",
+ "homepage": "https://github.com/sebastianbergmann/comparator",
+ "keywords": [
+ "comparator",
+ "compare",
+ "equality"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/comparator/issues",
+ "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.8"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2022-09-14T12:41:17+00:00"
+ },
+ {
+ "name": "sebastian/complexity",
+ "version": "2.0.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/complexity.git",
+ "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/25f207c40d62b8b7aa32f5ab026c53561964053a",
+ "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a",
+ "shasum": ""
+ },
+ "require": {
+ "nikic/php-parser": "^4.18 || ^5.0",
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Library for calculating the complexity of PHP code units",
+ "homepage": "https://github.com/sebastianbergmann/complexity",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/complexity/issues",
+ "source": "https://github.com/sebastianbergmann/complexity/tree/2.0.3"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2023-12-22T06:19:30+00:00"
+ },
+ {
+ "name": "sebastian/diff",
+ "version": "4.0.6",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/diff.git",
+ "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/ba01945089c3a293b01ba9badc29ad55b106b0bc",
+ "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3",
+ "symfony/process": "^4.2 || ^5"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "4.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ },
+ {
+ "name": "Kore Nordmann",
+ "email": "mail@kore-nordmann.de"
+ }
+ ],
+ "description": "Diff implementation",
+ "homepage": "https://github.com/sebastianbergmann/diff",
+ "keywords": [
+ "diff",
+ "udiff",
+ "unidiff",
+ "unified diff"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/diff/issues",
+ "source": "https://github.com/sebastianbergmann/diff/tree/4.0.6"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2024-03-02T06:30:58+00:00"
+ },
+ {
+ "name": "sebastian/environment",
+ "version": "5.1.5",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/environment.git",
+ "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/830c43a844f1f8d5b7a1f6d6076b784454d8b7ed",
+ "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "suggest": {
+ "ext-posix": "*"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "5.1-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ }
+ ],
+ "description": "Provides functionality to handle HHVM/PHP environments",
+ "homepage": "http://www.github.com/sebastianbergmann/environment",
+ "keywords": [
+ "Xdebug",
+ "environment",
+ "hhvm"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/environment/issues",
+ "source": "https://github.com/sebastianbergmann/environment/tree/5.1.5"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2023-02-03T06:03:51+00:00"
+ },
+ {
+ "name": "sebastian/exporter",
+ "version": "4.0.6",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/exporter.git",
+ "reference": "78c00df8f170e02473b682df15bfcdacc3d32d72"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/78c00df8f170e02473b682df15bfcdacc3d32d72",
+ "reference": "78c00df8f170e02473b682df15bfcdacc3d32d72",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3",
+ "sebastian/recursion-context": "^4.0"
+ },
+ "require-dev": {
+ "ext-mbstring": "*",
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "4.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ },
+ {
+ "name": "Jeff Welch",
+ "email": "whatthejeff@gmail.com"
+ },
+ {
+ "name": "Volker Dusch",
+ "email": "github@wallbash.com"
+ },
+ {
+ "name": "Adam Harvey",
+ "email": "aharvey@php.net"
+ },
+ {
+ "name": "Bernhard Schussek",
+ "email": "bschussek@gmail.com"
+ }
+ ],
+ "description": "Provides the functionality to export PHP variables for visualization",
+ "homepage": "https://www.github.com/sebastianbergmann/exporter",
+ "keywords": [
+ "export",
+ "exporter"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/exporter/issues",
+ "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.6"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2024-03-02T06:33:00+00:00"
+ },
+ {
+ "name": "sebastian/global-state",
+ "version": "5.0.7",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/global-state.git",
+ "reference": "bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9",
+ "reference": "bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3",
+ "sebastian/object-reflector": "^2.0",
+ "sebastian/recursion-context": "^4.0"
+ },
+ "require-dev": {
+ "ext-dom": "*",
+ "phpunit/phpunit": "^9.3"
+ },
+ "suggest": {
+ "ext-uopz": "*"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "5.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ }
+ ],
+ "description": "Snapshotting of global state",
+ "homepage": "http://www.github.com/sebastianbergmann/global-state",
+ "keywords": [
+ "global state"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/global-state/issues",
+ "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.7"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2024-03-02T06:35:11+00:00"
+ },
+ {
+ "name": "sebastian/lines-of-code",
+ "version": "1.0.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/lines-of-code.git",
+ "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/e1e4a170560925c26d424b6a03aed157e7dcc5c5",
+ "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5",
+ "shasum": ""
+ },
+ "require": {
+ "nikic/php-parser": "^4.18 || ^5.0",
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Library for counting the lines of code in PHP source code",
+ "homepage": "https://github.com/sebastianbergmann/lines-of-code",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/lines-of-code/issues",
+ "source": "https://github.com/sebastianbergmann/lines-of-code/tree/1.0.4"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2023-12-22T06:20:34+00:00"
+ },
+ {
+ "name": "sebastian/object-enumerator",
+ "version": "4.0.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/object-enumerator.git",
+ "reference": "5c9eeac41b290a3712d88851518825ad78f45c71"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/5c9eeac41b290a3712d88851518825ad78f45c71",
+ "reference": "5c9eeac41b290a3712d88851518825ad78f45c71",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3",
+ "sebastian/object-reflector": "^2.0",
+ "sebastian/recursion-context": "^4.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "4.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ }
+ ],
+ "description": "Traverses array structures and object graphs to enumerate all referenced objects",
+ "homepage": "https://github.com/sebastianbergmann/object-enumerator/",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/object-enumerator/issues",
+ "source": "https://github.com/sebastianbergmann/object-enumerator/tree/4.0.4"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-10-26T13:12:34+00:00"
+ },
+ {
+ "name": "sebastian/object-reflector",
+ "version": "2.0.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/object-reflector.git",
+ "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/b4f479ebdbf63ac605d183ece17d8d7fe49c15c7",
+ "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ }
+ ],
+ "description": "Allows reflection of object attributes, including inherited and non-public ones",
+ "homepage": "https://github.com/sebastianbergmann/object-reflector/",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/object-reflector/issues",
+ "source": "https://github.com/sebastianbergmann/object-reflector/tree/2.0.4"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-10-26T13:14:26+00:00"
+ },
+ {
+ "name": "sebastian/recursion-context",
+ "version": "4.0.5",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/recursion-context.git",
+ "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1",
+ "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "4.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ },
+ {
+ "name": "Jeff Welch",
+ "email": "whatthejeff@gmail.com"
+ },
+ {
+ "name": "Adam Harvey",
+ "email": "aharvey@php.net"
+ }
+ ],
+ "description": "Provides functionality to recursively process PHP variables",
+ "homepage": "https://github.com/sebastianbergmann/recursion-context",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/recursion-context/issues",
+ "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.5"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2023-02-03T06:07:39+00:00"
+ },
+ {
+ "name": "sebastian/resource-operations",
+ "version": "3.0.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/resource-operations.git",
+ "reference": "05d5692a7993ecccd56a03e40cd7e5b09b1d404e"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/05d5692a7993ecccd56a03e40cd7e5b09b1d404e",
+ "reference": "05d5692a7993ecccd56a03e40cd7e5b09b1d404e",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "3.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ }
+ ],
+ "description": "Provides a list of PHP built-in functions that operate on resources",
+ "homepage": "https://www.github.com/sebastianbergmann/resource-operations",
+ "support": {
+ "source": "https://github.com/sebastianbergmann/resource-operations/tree/3.0.4"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2024-03-14T16:00:52+00:00"
+ },
+ {
+ "name": "sebastian/type",
+ "version": "3.2.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/type.git",
+ "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7",
+ "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.5"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "3.2-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Collection of value objects that represent the types of the PHP type system",
+ "homepage": "https://github.com/sebastianbergmann/type",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/type/issues",
+ "source": "https://github.com/sebastianbergmann/type/tree/3.2.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2023-02-03T06:13:03+00:00"
+ },
+ {
+ "name": "sebastian/version",
+ "version": "3.0.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/version.git",
+ "reference": "c6c1022351a901512170118436c764e473f6de8c"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c6c1022351a901512170118436c764e473f6de8c",
+ "reference": "c6c1022351a901512170118436c764e473f6de8c",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "3.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Library that helps with managing the version number of Git-hosted PHP projects",
+ "homepage": "https://github.com/sebastianbergmann/version",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/version/issues",
+ "source": "https://github.com/sebastianbergmann/version/tree/3.0.2"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-09-28T06:39:44+00:00"
+ },
+ {
+ "name": "softcreatr/jsonpath",
+ "version": "0.8.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/SoftCreatR/JSONPath.git",
+ "reference": "fc12dee0b46f3fa3a175c4051dbab60984acef4b"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/SoftCreatR/JSONPath/zipball/fc12dee0b46f3fa3a175c4051dbab60984acef4b",
+ "reference": "fc12dee0b46f3fa3a175c4051dbab60984acef4b",
+ "shasum": ""
+ },
+ "require": {
+ "ext-json": "*",
+ "php": ">=8.0"
+ },
+ "replace": {
+ "flow/jsonpath": "*"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.6",
+ "roave/security-advisories": "dev-latest"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Flow\\JSONPath\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Stephen Frank",
+ "email": "stephen@flowsa.com",
+ "homepage": "https://prismaticbytes.com",
+ "role": "Developer"
+ },
+ {
+ "name": "Sascha Greuel",
+ "email": "hello@1-2.dev",
+ "homepage": "https://1-2.dev",
+ "role": "Developer"
+ }
+ ],
+ "description": "JSONPath implementation for parsing, searching and flattening arrays",
+ "support": {
+ "email": "hello@1-2.dev",
+ "forum": "https://github.com/SoftCreatR/JSONPath/discussions",
+ "issues": "https://github.com/SoftCreatR/JSONPath/issues",
+ "source": "https://github.com/SoftCreatR/JSONPath"
+ },
+ "funding": [
+ {
+ "url": "https://ecologi.com/softcreatr?r=61212ab3fc69b8eb8a2014f4",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/softcreatr",
+ "type": "github"
+ }
+ ],
+ "time": "2023-08-17T20:14:00+00:00"
+ },
+ {
+ "name": "squizlabs/php_codesniffer",
+ "version": "3.9.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git",
+ "reference": "aac1f6f347a5c5ac6bc98ad395007df00990f480"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/aac1f6f347a5c5ac6bc98ad395007df00990f480",
+ "reference": "aac1f6f347a5c5ac6bc98ad395007df00990f480",
+ "shasum": ""
+ },
+ "require": {
+ "ext-simplexml": "*",
+ "ext-tokenizer": "*",
+ "ext-xmlwriter": "*",
+ "php": ">=5.4.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.3.4"
+ },
+ "bin": [
+ "bin/phpcbf",
+ "bin/phpcs"
+ ],
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "3.x-dev"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Greg Sherwood",
+ "role": "Former lead"
+ },
+ {
+ "name": "Juliette Reinders Folmer",
+ "role": "Current lead"
+ },
+ {
+ "name": "Contributors",
+ "homepage": "https://github.com/PHPCSStandards/PHP_CodeSniffer/graphs/contributors"
+ }
+ ],
+ "description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.",
+ "homepage": "https://github.com/PHPCSStandards/PHP_CodeSniffer",
+ "keywords": [
+ "phpcs",
+ "standards",
+ "static analysis"
+ ],
+ "support": {
+ "issues": "https://github.com/PHPCSStandards/PHP_CodeSniffer/issues",
+ "security": "https://github.com/PHPCSStandards/PHP_CodeSniffer/security/policy",
+ "source": "https://github.com/PHPCSStandards/PHP_CodeSniffer",
+ "wiki": "https://github.com/PHPCSStandards/PHP_CodeSniffer/wiki"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/PHPCSStandards",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/jrfnl",
+ "type": "github"
+ },
+ {
+ "url": "https://opencollective.com/php_codesniffer",
+ "type": "open_collective"
+ }
+ ],
+ "time": "2024-04-23T20:25:34+00:00"
+ },
+ {
+ "name": "symfony/config",
+ "version": "v5.4.39",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/config.git",
+ "reference": "62cec4a067931552624a9962002c210c502d42fd"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/config/zipball/62cec4a067931552624a9962002c210c502d42fd",
+ "reference": "62cec4a067931552624a9962002c210c502d42fd",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2.5",
+ "symfony/deprecation-contracts": "^2.1|^3",
+ "symfony/filesystem": "^4.4|^5.0|^6.0",
+ "symfony/polyfill-ctype": "~1.8",
+ "symfony/polyfill-php80": "^1.16",
+ "symfony/polyfill-php81": "^1.22"
+ },
+ "conflict": {
+ "symfony/finder": "<4.4"
+ },
+ "require-dev": {
+ "symfony/event-dispatcher": "^4.4|^5.0|^6.0",
+ "symfony/finder": "^4.4|^5.0|^6.0",
+ "symfony/messenger": "^4.4|^5.0|^6.0",
+ "symfony/service-contracts": "^1.1|^2|^3",
+ "symfony/yaml": "^4.4|^5.0|^6.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",
+ "support": {
+ "source": "https://github.com/symfony/config/tree/v5.4.39"
+ },
+ "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": "2024-04-18T08:26:06+00:00"
+ },
+ {
+ "name": "symfony/dependency-injection",
+ "version": "v5.4.39",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/dependency-injection.git",
+ "reference": "5b4505f2afbe1d11d43a3917d0c1c178a38f6f19"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/5b4505f2afbe1d11d43a3917d0c1c178a38f6f19",
+ "reference": "5b4505f2afbe1d11d43a3917d0c1c178a38f6f19",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2.5",
+ "psr/container": "^1.1.1",
+ "symfony/deprecation-contracts": "^2.1|^3",
+ "symfony/polyfill-php80": "^1.16",
+ "symfony/polyfill-php81": "^1.22",
+ "symfony/service-contracts": "^1.1.6|^2"
+ },
+ "conflict": {
+ "ext-psr": "<1.1|>=2",
+ "symfony/config": "<5.3",
+ "symfony/finder": "<4.4",
+ "symfony/proxy-manager-bridge": "<4.4",
+ "symfony/yaml": "<4.4.26"
+ },
+ "provide": {
+ "psr/container-implementation": "1.0",
+ "symfony/service-implementation": "1.0|2.0"
+ },
+ "require-dev": {
+ "symfony/config": "^5.3|^6.0",
+ "symfony/expression-language": "^4.4|^5.0|^6.0",
+ "symfony/yaml": "^4.4.26|^5.0|^6.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",
+ "support": {
+ "source": "https://github.com/symfony/dependency-injection/tree/v5.4.39"
+ },
+ "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": "2024-04-18T08:26:06+00:00"
+ },
+ {
+ "name": "symfony/filesystem",
+ "version": "v5.4.39",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/filesystem.git",
+ "reference": "e6edd875d5d39b03de51f3c3951148cfa79a4d12"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/filesystem/zipball/e6edd875d5d39b03de51f3c3951148cfa79a4d12",
+ "reference": "e6edd875d5d39b03de51f3c3951148cfa79a4d12",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2.5",
+ "symfony/polyfill-ctype": "~1.8",
+ "symfony/polyfill-mbstring": "~1.8",
+ "symfony/polyfill-php80": "^1.16",
+ "symfony/process": "^5.4|^6.4"
+ },
+ "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",
+ "support": {
+ "source": "https://github.com/symfony/filesystem/tree/v5.4.39"
+ },
+ "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": "2024-04-18T08:26:06+00:00"
+ },
+ {
+ "name": "symfony/process",
+ "version": "v5.4.39",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/process.git",
+ "reference": "85a554acd7c28522241faf2e97b9541247a0d3d5"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/process/zipball/85a554acd7c28522241faf2e97b9541247a0d3d5",
+ "reference": "85a554acd7c28522241faf2e97b9541247a0d3d5",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2.5",
+ "symfony/polyfill-php80": "^1.16"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\Process\\": ""
+ },
+ "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": "Executes commands in sub-processes",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/process/tree/v5.4.39"
+ },
+ "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": "2024-04-18T08:26:06+00:00"
+ },
+ {
+ "name": "theseer/tokenizer",
+ "version": "1.2.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/theseer/tokenizer.git",
+ "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/theseer/tokenizer/zipball/737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2",
+ "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2",
+ "shasum": ""
+ },
+ "require": {
+ "ext-dom": "*",
+ "ext-tokenizer": "*",
+ "ext-xmlwriter": "*",
+ "php": "^7.2 || ^8.0"
+ },
+ "type": "library",
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Arne Blankerts",
+ "email": "arne@blankerts.de",
+ "role": "Developer"
+ }
+ ],
+ "description": "A small library for converting tokenized PHP source code into XML and potentially other formats",
+ "support": {
+ "issues": "https://github.com/theseer/tokenizer/issues",
+ "source": "https://github.com/theseer/tokenizer/tree/1.2.3"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/theseer",
+ "type": "github"
+ }
+ ],
+ "time": "2024-03-03T12:36:25+00:00"
+ },
+ {
+ "name": "webmozart/assert",
+ "version": "1.11.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/webmozarts/assert.git",
+ "reference": "11cb2199493b2f8a3b53e7f19068fc6aac760991"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/webmozarts/assert/zipball/11cb2199493b2f8a3b53e7f19068fc6aac760991",
+ "reference": "11cb2199493b2f8a3b53e7f19068fc6aac760991",
+ "shasum": ""
+ },
+ "require": {
+ "ext-ctype": "*",
+ "php": "^7.2 || ^8.0"
+ },
+ "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"
+ ],
+ "support": {
+ "issues": "https://github.com/webmozarts/assert/issues",
+ "source": "https://github.com/webmozarts/assert/tree/1.11.0"
+ },
+ "time": "2022-06-03T18:03:27+00:00"
+ }
+ ],
+ "aliases": [],
+ "minimum-stability": "stable",
+ "stability-flags": {
+ "eappointment/mellon": 20,
+ "eappointment/zmsclient": 20,
+ "eappointment/zmsentities": 20,
+ "eappointment/zmsslim": 20,
+ "phpmd/phpmd": 0
+ },
+ "prefer-stable": false,
+ "prefer-lowest": false,
+ "platform": {},
+ "platform-dev": {},
+ "platform-overrides": {
+ "php": "8.0.2"
+ },
+ "plugin-api-version": "2.6.0"
+}
diff --git a/zmscitizenapi/config.example.php b/zmscitizenapi/config.example.php
new file mode 100644
index 000000000..b20799d9b
--- /dev/null
+++ b/zmscitizenapi/config.example.php
@@ -0,0 +1,16 @@
+=10"
+ }
+ },
+ "node_modules/@apidevtools/swagger-cli": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/@apidevtools/swagger-cli/-/swagger-cli-4.0.4.tgz",
+ "integrity": "sha512-hdDT3B6GLVovCsRZYDi3+wMcB1HfetTU20l2DC8zD3iFRNMC6QNAZG5fo/6PYeHWBEv7ri4MvnlKodhNB0nt7g==",
+ "deprecated": "This package has been abandoned. Please switch to using the actively maintained @redocly/cli",
+ "license": "MIT",
+ "dependencies": {
+ "@apidevtools/swagger-parser": "^10.0.1",
+ "chalk": "^4.1.0",
+ "js-yaml": "^3.14.0",
+ "yargs": "^15.4.1"
+ },
+ "bin": {
+ "swagger-cli": "bin/swagger-cli.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@apidevtools/swagger-cli/node_modules/argparse": {
+ "version": "1.0.10",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
+ "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
+ "license": "MIT",
+ "dependencies": {
+ "sprintf-js": "~1.0.2"
+ }
+ },
+ "node_modules/@apidevtools/swagger-cli/node_modules/js-yaml": {
+ "version": "3.14.1",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
+ "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
+ "license": "MIT",
+ "dependencies": {
+ "argparse": "^1.0.7",
+ "esprima": "^4.0.0"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/@apidevtools/swagger-methods": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/@apidevtools/swagger-methods/-/swagger-methods-3.0.2.tgz",
+ "integrity": "sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg==",
+ "license": "MIT"
+ },
+ "node_modules/@apidevtools/swagger-parser": {
+ "version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/@apidevtools/swagger-parser/-/swagger-parser-10.1.0.tgz",
+ "integrity": "sha512-9Kt7EuS/7WbMAUv2gSziqjvxwDbFSg3Xeyfuj5laUODX8o/k/CpsAKiQ8W7/R88eXFTMbJYg6+7uAmOWNKmwnw==",
+ "license": "MIT",
+ "dependencies": {
+ "@apidevtools/json-schema-ref-parser": "9.0.6",
+ "@apidevtools/openapi-schemas": "^2.1.0",
+ "@apidevtools/swagger-methods": "^3.0.2",
+ "@jsdevtools/ono": "^7.1.3",
+ "ajv": "^8.6.3",
+ "ajv-draft-04": "^1.0.0",
+ "call-me-maybe": "^1.0.1"
+ },
+ "peerDependencies": {
+ "openapi-types": ">=7"
+ }
+ },
+ "node_modules/@jsdevtools/ono": {
+ "version": "7.1.3",
+ "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz",
+ "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==",
+ "license": "MIT"
+ },
+ "node_modules/ajv": {
+ "version": "8.17.1",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
+ "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.3",
+ "fast-uri": "^3.0.1",
+ "json-schema-traverse": "^1.0.0",
+ "require-from-string": "^2.0.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/ajv-draft-04": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/ajv-draft-04/-/ajv-draft-04-1.0.0.tgz",
+ "integrity": "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "ajv": "^8.5.0"
+ },
+ "peerDependenciesMeta": {
+ "ajv": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/argparse": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+ "dev": true,
+ "license": "Python-2.0"
+ },
+ "node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/brace-expansion": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/call-me-maybe": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz",
+ "integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==",
+ "license": "MIT"
+ },
+ "node_modules/camelcase": {
+ "version": "5.3.1",
+ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
+ "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/cliui": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
+ "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==",
+ "license": "ISC",
+ "dependencies": {
+ "string-width": "^4.2.0",
+ "strip-ansi": "^6.0.0",
+ "wrap-ansi": "^6.2.0"
+ }
+ },
+ "node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "license": "MIT"
+ },
+ "node_modules/commander": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.0.tgz",
+ "integrity": "sha512-zP4jEKbe8SHzKJYQmq8Y9gYjtO/POJLgIdKgV7B9qNmABVFVc+ctqSX6iXh4mCpJfRBOabiZ2YKPg8ciDw6C+Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/decamelize": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
+ "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/doctrine": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
+ "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "esutils": "^2.0.2"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "license": "MIT"
+ },
+ "node_modules/esprima": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
+ "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
+ "license": "BSD-2-Clause",
+ "bin": {
+ "esparse": "bin/esparse.js",
+ "esvalidate": "bin/esvalidate.js"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/esutils": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
+ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/fast-deep-equal": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+ "license": "MIT"
+ },
+ "node_modules/fast-uri": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.3.tgz",
+ "integrity": "sha512-aLrHthzCjH5He4Z2H9YZ+v6Ujb9ocRuW6ZzkJQOrTxleEijANq4v1TsaPaVG1PZcuurEzrLcWRyYBYXD5cEiaw==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/find-up": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
+ "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
+ "license": "MIT",
+ "dependencies": {
+ "locate-path": "^5.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/fs.realpath": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/get-caller-file": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
+ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
+ "license": "ISC",
+ "engines": {
+ "node": "6.* || 8.* || >= 10.*"
+ }
+ },
+ "node_modules/glob": {
+ "version": "7.1.6",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
+ "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==",
+ "deprecated": "Glob versions prior to v9 are no longer supported",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.0.4",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ },
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/inflight": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+ "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
+ "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "once": "^1.3.0",
+ "wrappy": "1"
+ }
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/js-yaml": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
+ "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "argparse": "^2.0.1"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/json-schema-traverse": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
+ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
+ "license": "MIT"
+ },
+ "node_modules/locate-path": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
+ "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
+ "license": "MIT",
+ "dependencies": {
+ "p-locate": "^4.1.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/lodash.get": {
+ "version": "4.4.2",
+ "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
+ "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/lodash.isequal": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
+ "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/lodash.mergewith": {
+ "version": "4.6.2",
+ "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz",
+ "integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/once": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+ "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "wrappy": "1"
+ }
+ },
+ "node_modules/openapi-types": {
+ "version": "10.0.0",
+ "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-10.0.0.tgz",
+ "integrity": "sha512-Y8xOCT2eiKGYDzMW9R4x5cmfc3vGaaI4EL2pwhDmodWw1HlK18YcZ4uJxc7Rdp7/gGzAygzH9SXr6GKYIXbRcQ==",
+ "license": "MIT"
+ },
+ "node_modules/p-limit": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
+ "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
+ "license": "MIT",
+ "dependencies": {
+ "p-try": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-locate": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
+ "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
+ "license": "MIT",
+ "dependencies": {
+ "p-limit": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/p-try": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
+ "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/path-exists": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-is-absolute": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+ "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/require-directory": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
+ "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/require-from-string": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
+ "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/require-main-filename": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
+ "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
+ "license": "ISC"
+ },
+ "node_modules/set-blocking": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
+ "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
+ "license": "ISC"
+ },
+ "node_modules/sprintf-js": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
+ "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/swagger-jsdoc": {
+ "version": "6.2.8",
+ "resolved": "https://registry.npmjs.org/swagger-jsdoc/-/swagger-jsdoc-6.2.8.tgz",
+ "integrity": "sha512-VPvil1+JRpmJ55CgAtn8DIcpBs0bL5L3q5bVQvF4tAW/k/9JYSj7dCpaYCAv5rufe0vcCbBRQXGvzpkWjvLklQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "commander": "6.2.0",
+ "doctrine": "3.0.0",
+ "glob": "7.1.6",
+ "lodash.mergewith": "^4.6.2",
+ "swagger-parser": "^10.0.3",
+ "yaml": "2.0.0-1"
+ },
+ "bin": {
+ "swagger-jsdoc": "bin/swagger-jsdoc.js"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
+ "node_modules/swagger-parser": {
+ "version": "10.0.3",
+ "resolved": "https://registry.npmjs.org/swagger-parser/-/swagger-parser-10.0.3.tgz",
+ "integrity": "sha512-nF7oMeL4KypldrQhac8RyHerJeGPD1p2xDh900GPvc+Nk7nWP6jX2FcC7WmkinMoAmoO774+AFXcWsW8gMWEIg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@apidevtools/swagger-parser": "10.0.3"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/swagger-parser/node_modules/@apidevtools/swagger-parser": {
+ "version": "10.0.3",
+ "resolved": "https://registry.npmjs.org/@apidevtools/swagger-parser/-/swagger-parser-10.0.3.tgz",
+ "integrity": "sha512-sNiLY51vZOmSPFZA5TF35KZ2HbgYklQnTSDnkghamzLb3EkNtcQnrBQEj5AOCxHpTtXpqMCRM1CrmV2rG6nw4g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@apidevtools/json-schema-ref-parser": "^9.0.6",
+ "@apidevtools/openapi-schemas": "^2.0.4",
+ "@apidevtools/swagger-methods": "^3.0.2",
+ "@jsdevtools/ono": "^7.1.3",
+ "call-me-maybe": "^1.0.1",
+ "z-schema": "^5.0.1"
+ },
+ "peerDependencies": {
+ "openapi-types": ">=7"
+ }
+ },
+ "node_modules/validator": {
+ "version": "13.12.0",
+ "resolved": "https://registry.npmjs.org/validator/-/validator-13.12.0.tgz",
+ "integrity": "sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/which-module": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz",
+ "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==",
+ "license": "ISC"
+ },
+ "node_modules/wrap-ansi": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
+ "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/wrappy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/y18n": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz",
+ "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==",
+ "license": "ISC"
+ },
+ "node_modules/yaml": {
+ "version": "2.0.0-1",
+ "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.0.0-1.tgz",
+ "integrity": "sha512-W7h5dEhywMKenDJh2iX/LABkbFnBxasD27oyXWDS/feDsxiw0dD5ncXdYXgkvAsXIY2MpW/ZKkr9IU30DBdMNQ==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/yargs": {
+ "version": "15.4.1",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz",
+ "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==",
+ "license": "MIT",
+ "dependencies": {
+ "cliui": "^6.0.0",
+ "decamelize": "^1.2.0",
+ "find-up": "^4.1.0",
+ "get-caller-file": "^2.0.1",
+ "require-directory": "^2.1.1",
+ "require-main-filename": "^2.0.0",
+ "set-blocking": "^2.0.0",
+ "string-width": "^4.2.0",
+ "which-module": "^2.0.0",
+ "y18n": "^4.0.0",
+ "yargs-parser": "^18.1.2"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/yargs-parser": {
+ "version": "18.1.3",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz",
+ "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==",
+ "license": "ISC",
+ "dependencies": {
+ "camelcase": "^5.0.0",
+ "decamelize": "^1.2.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/z-schema": {
+ "version": "5.0.5",
+ "resolved": "https://registry.npmjs.org/z-schema/-/z-schema-5.0.5.tgz",
+ "integrity": "sha512-D7eujBWkLa3p2sIpJA0d1pr7es+a7m0vFAnZLlCEKq/Ij2k0MLi9Br2UPxoxdYystm5K1yeBGzub0FlYUEWj2Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "lodash.get": "^4.4.2",
+ "lodash.isequal": "^4.5.0",
+ "validator": "^13.7.0"
+ },
+ "bin": {
+ "z-schema": "bin/z-schema"
+ },
+ "engines": {
+ "node": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "commander": "^9.4.1"
+ }
+ },
+ "node_modules/z-schema/node_modules/commander": {
+ "version": "9.5.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz",
+ "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": "^12.20.0 || >=14"
+ }
+ }
+ }
+}
diff --git a/zmscitizenapi/package.json b/zmscitizenapi/package.json
new file mode 100644
index 000000000..dda7ae5a6
--- /dev/null
+++ b/zmscitizenapi/package.json
@@ -0,0 +1,25 @@
+{
+ "name": "zmscitizenapi",
+ "version": "1.0.0",
+ "description": "This application offers a REST-like interface for ZMS functions and database access.",
+ "private": true,
+ "scripts": {
+ "preinstall": "",
+ "build": "node ./bin/build_swagger.js",
+ "doc": "npm run build && npx swagger-cli bundle -o public/doc/swagger.json public/doc/swagger.yaml"
+ },
+ "keywords": [
+ "eappointment"
+ ],
+ "author": "it@m - opensource@muenchen.de",
+ "license": "SEE LICENSE IN LICENSE FILE",
+ "dependencies": {
+ "@apidevtools/swagger-cli": "^4.0.4"
+ },
+ "devDependencies": {
+ "swagger-jsdoc": "^6.1.0",
+ "swagger-parser": "^10.0.0",
+ "openapi-types": "^10.0.0",
+ "js-yaml": "^4.1.0"
+ }
+}
diff --git a/zmscitizenapi/phpunit.xml b/zmscitizenapi/phpunit.xml
new file mode 100644
index 000000000..00617d8f7
--- /dev/null
+++ b/zmscitizenapi/phpunit.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+ ./tests/Zmscitizenapi/
+
+
+
+
+ ./src
+
+
+
diff --git a/zmscitizenapi/public/doc/README.md b/zmscitizenapi/public/doc/README.md
new file mode 100644
index 000000000..b968509cb
--- /dev/null
+++ b/zmscitizenapi/public/doc/README.md
@@ -0,0 +1,29 @@
+## How does the Open Api definition work
+## Version 2.0
+
+```
+bin/configure
+npm i
+npm run build
+npm run doc
+swagger-cli bundle -o public/doc/swagger.json public/doc/swagger.yaml
+python3 -m http.server 8000
+```
+
+Reachable at:
+```
+http://[::]:8000/public/doc/
+https://zms.ddev.site/terminvereinbarung/api/citizen/doc/index.html
+https://it-at-m.github.io/eappointment/zmscitizenapi/public/doc/index.html
+```
+
+
+* Under /public/doc are the schema from zmsentities. A symbolic link points to the corresponding folder under vendor/eappointment/zmsentities/schema.
+
+* Under /bin there is a build_swagger.js file. This is executed via ``npm run doc`` and validates the existing swagger.yaml file. If valid, the open api annotations are read from routing.php and the remaining information such as info, definitions, version and tags are compiled from the yaml files under ./partials into a complete swagger.yaml.
+
+* a bin/configure must be executed before a bin/doc so that the latest API version is in the ./VERSION file.
+
+* To access all paths resolved via redoc or the open api documentation, a resolved swagger.json must be created from the swagger.yaml. This is done via the swagger cli with a call to ``bin/doc``. This call executes the above npm command ``npm run doc`` and subsequently creates a full swagger.json.
+
+To render the open-api doc by redoc and swagger, appropriate files such as swagger-ui files are fetched in the CI process and stored at https://eappointment.gitlab.io/zmsapi/.
\ No newline at end of file
diff --git a/zmscitizenapi/public/doc/assets/.gitkeep b/zmscitizenapi/public/doc/assets/.gitkeep
new file mode 100644
index 000000000..e69de29bb
diff --git a/zmscitizenapi/public/doc/index.html b/zmscitizenapi/public/doc/index.html
new file mode 100644
index 000000000..5537aa266
--- /dev/null
+++ b/zmscitizenapi/public/doc/index.html
@@ -0,0 +1,23 @@
+
+
+
+ ReDoc
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/zmscitizenapi/public/doc/logo.png b/zmscitizenapi/public/doc/logo.png
new file mode 100644
index 000000000..1a1014b9e
Binary files /dev/null and b/zmscitizenapi/public/doc/logo.png differ
diff --git a/zmscitizenapi/public/doc/partials/basic.yaml b/zmscitizenapi/public/doc/partials/basic.yaml
new file mode 100644
index 000000000..b92be7750
--- /dev/null
+++ b/zmscitizenapi/public/doc/partials/basic.yaml
@@ -0,0 +1,7 @@
+basePath: /terminvereinbarung/api/citizen
+schemes:
+ - https
+consumes:
+ - application/json
+produces:
+ - application/json
\ No newline at end of file
diff --git a/zmscitizenapi/public/doc/partials/definitions.yaml b/zmscitizenapi/public/doc/partials/definitions.yaml
new file mode 100644
index 000000000..b7bb359b2
--- /dev/null
+++ b/zmscitizenapi/public/doc/partials/definitions.yaml
@@ -0,0 +1,33 @@
+definitions:
+ appointment:
+ $ref: "schema/citizenapi/thinnedProcess.json"
+ appointmentCancel:
+ $ref: "schema/citizenapi/appointmentCancel.json"
+ appointmentConfirm:
+ $ref: "schema/citizenapi/appointmentConfirm.json"
+ appointmentPreconfirm:
+ $ref: "schema/citizenapi/appointmentPreconfirm.json"
+ appointmentReserve:
+ $ref: "schema/citizenapi/appointmentReserve.json"
+ appointmentUpdate:
+ $ref: "schema/citizenapi/appointmentUpdate.json"
+ availableAppointments:
+ $ref: "schema/citizenapi/availableAppointments.json"
+ availableDays:
+ $ref: "schema/citizenapi/availableDays.json"
+ captchaDetails:
+ $ref: "schema/citizenapi/captcha/friendlyCaptcha.json"
+ offices:
+ $ref: "schema/citizenapi/collections/officeList.json"
+ officesAndServices:
+ $ref: "schema/citizenapi/collections/officeServiceAndRelationList.json"
+ officesByService:
+ $ref: "schema/citizenapi/collections/officeList.json"
+ scope:
+ $ref: "schema/citizenapi/thinnedScope.json"
+ scopes:
+ $ref: "schema/citizenapi/collections/thinnedScopeList.json"
+ services:
+ $ref: "schema/citizenapi/collections/serviceList.json"
+ servicesByOffice:
+ $ref: "schema/citizenapi/collections/serviceList.json"
\ No newline at end of file
diff --git a/zmscitizenapi/public/doc/partials/info.yaml b/zmscitizenapi/public/doc/partials/info.yaml
new file mode 100644
index 000000000..1f96e255b
--- /dev/null
+++ b/zmscitizenapi/public/doc/partials/info.yaml
@@ -0,0 +1,20 @@
+info:
+ title: ZMSCitizenAPI
+ x-logo:
+ url: "./logo.png"
+ description: |
+ The ZMSCitizenAPI builds upon the ZMSApi to provide an interface specifically designed for external citizens. Its key features include:
+
+ * Citizens can select services and providers (locations) from ZMS sources.
+ * Citizens can book ZMS appointments via an integrated calendar and email system, enabling a seamless process for managing appointments.
+ * Citizens can modify or reschedule ZMS appointments through the same calendar and email functionality.
+
+ This documentation provides detailed API-level guidance for accessing the Citizen Frontend (Bürgeransicht) features.
+ termsOfService: 'http://service.berlin.de/terminvereinbarung/'
+ contact:
+ name: ''
+ email: 'example@example.com'
+ url: 'https://opensource.muenchen.de/software/zeitmanagementsystem.html'
+ license:
+ name: 'MIT'
+ url: ''
diff --git a/zmscitizenapi/public/doc/partials/tags.yaml b/zmscitizenapi/public/doc/partials/tags.yaml
new file mode 100644
index 000000000..e2944a00f
--- /dev/null
+++ b/zmscitizenapi/public/doc/partials/tags.yaml
@@ -0,0 +1,6 @@
+tags:
+ - name: zmscitizenapi
+ description: |
+ The zmscitizenapi is a streamlined API designed specifically for the citizen-facing frontend application "Bürgeransicht." It acts as an intermediary between the Bürgeransicht frontend and various backend services, including the broader zmsapi system. The zmscitizenapi consolidates and simplifies endpoints from zmsapi and other integrated services to provide a cohesive experience for users, enabling actions such as appointment booking, service requests, and other interactions with municipal services. Its purpose is to enhance the usability of the citizen interface by providing a lightweight and efficient layer on top of the complex zmsapi structure.
+
+
diff --git a/zmscitizenapi/public/doc/partials/version.yaml b/zmscitizenapi/public/doc/partials/version.yaml
new file mode 100644
index 000000000..5b308b04f
--- /dev/null
+++ b/zmscitizenapi/public/doc/partials/version.yaml
@@ -0,0 +1 @@
+swagger: "2.0"
\ No newline at end of file
diff --git a/zmscitizenapi/public/doc/schema b/zmscitizenapi/public/doc/schema
new file mode 120000
index 000000000..c83e4ce91
--- /dev/null
+++ b/zmscitizenapi/public/doc/schema
@@ -0,0 +1 @@
+../../vendor/eappointment/zmsentities/schema
\ No newline at end of file
diff --git a/zmscitizenapi/public/index.php b/zmscitizenapi/public/index.php
new file mode 100644
index 000000000..1c83fad51
--- /dev/null
+++ b/zmscitizenapi/public/index.php
@@ -0,0 +1,4 @@
+run();
diff --git a/zmscitizenapi/routing.php b/zmscitizenapi/routing.php
new file mode 100644
index 000000000..e7ae8fc79
--- /dev/null
+++ b/zmscitizenapi/routing.php
@@ -0,0 +1,602 @@
+get(
+ '/services/',
+ '\BO\Zmscitizenapi\Controllers\Service\ServicesListController'
+)
+ ->setName("ServicesListController");
+
+/**
+ * @swagger
+ * /scopes/:
+ * get:
+ * summary: Get the list of scopes
+ * tags:
+ * - scopes
+ * responses:
+ * 200:
+ * description: List of scopes
+ * schema:
+ * type: object
+ * properties:
+ * meta:
+ * $ref: "schema/metaresult.json"
+ * data:
+ * $ref: "schema/citizenapi/collections/thinnedScopeList.json"
+ */
+\App::$slim->get(
+ '/scopes/',
+ '\BO\Zmscitizenapi\Controllers\Scope\ScopesListController'
+)
+ ->setName("ScopesListController");
+
+/**
+ * @swagger
+ * /offices/:
+ * get:
+ * summary: Get the list of offices
+ * tags:
+ * - offices
+ * responses:
+ * 200:
+ * description: List of offices
+ * schema:
+ * type: object
+ * properties:
+ * meta:
+ * $ref: "schema/metaresult.json"
+ * data:
+ * $ref: "schema/citizenapi/collections/officeList.json"
+ */
+\App::$slim->get(
+ '/offices/',
+ '\BO\Zmscitizenapi\Controllers\Office\OfficesListController'
+)
+ ->setName("OfficesListController");
+
+/**
+ * @swagger
+ * /offices-and-services/:
+ * get:
+ * summary: Get the relations between offices and services
+ * tags:
+ * - offices-services
+ * responses:
+ * 200:
+ * description: List of office-service relations
+ * schema:
+ * type: object
+ * properties:
+ * meta:
+ * $ref: "schema/metaresult.json"
+ * data:
+ * $ref: "schema/citizenapi/collections/officeServiceAndRelationList.json"
+ */
+\App::$slim->get(
+ '/offices-and-services/',
+ '\BO\Zmscitizenapi\Controllers\Office\OfficesServicesRelationsController'
+)
+ ->setName("OfficesServicesRelationsController");
+
+/**
+ * @swagger
+ * /scope-by-id/:
+ * get:
+ * summary: Get a scope by ID
+ * tags:
+ * - scopes
+ * parameters:
+ * - name: scopeId
+ * description: Scope ID
+ * in: query
+ * required: true
+ * type: integer
+ * responses:
+ * 200:
+ * description: Scope details
+ * schema:
+ * type: object
+ * properties:
+ * meta:
+ * $ref: "schema/metaresult.json"
+ * data:
+ * $ref: "schema/citizenapi/thinnedScope.json"
+ * 404:
+ * description: Scope not found
+ */
+\App::$slim->get(
+ '/scope-by-id/',
+ '\BO\Zmscitizenapi\Controllers\Scope\ScopeByIdController'
+)
+ ->setName("ScopeByIdController");
+
+/**
+ * @swagger
+ * /services-by-office/:
+ * get:
+ * summary: Get the services offered by a specific office
+ * tags:
+ * - services
+ * parameters:
+ * - name: officeId
+ * description: Office ID
+ * in: query
+ * required: true
+ * type: integer
+ * responses:
+ * 200:
+ * description: List of services for the office
+ * schema:
+ * type: object
+ * properties:
+ * meta:
+ * $ref: "schema/metaresult.json"
+ * data:
+ * $ref: "schema/citizenapi/collections/serviceList.json"
+ */
+\App::$slim->get(
+ '/services-by-office/',
+ '\BO\Zmscitizenapi\Controllers\Service\ServiceListByOfficeController'
+)
+ ->setName("ServiceListByOfficeController");
+
+/**
+ * @swagger
+ * /offices-by-service/:
+ * get:
+ * summary: Get the offices that offer a specific service
+ * tags:
+ * - offices
+ * parameters:
+ * - name: serviceId
+ * description: Service ID
+ * in: query
+ * required: true
+ * type: integer
+ * responses:
+ * 200:
+ * description: List of offices offering the service
+ * schema:
+ * type: object
+ * properties:
+ * meta:
+ * $ref: "schema/metaresult.json"
+ * data:
+ * $ref: "schema/citizenapi/collections/officeList.json"
+ */
+\App::$slim->get(
+ '/offices-by-service/',
+ '\BO\Zmscitizenapi\Controllers\Office\OfficeListByServiceController'
+)
+ ->setName("OfficeListByServiceController");
+
+/**
+ * @swagger
+ * /available-days/:
+ * get:
+ * summary: Get the list of available days for appointments
+ * tags:
+ * - appointments
+ * parameters:
+ * - name: officeId
+ * description: Office ID
+ * in: query
+ * required: true
+ * type: integer
+ * - name: serviceId
+ * description: Service ID
+ * in: query
+ * required: true
+ * type: integer
+ * responses:
+ * 200:
+ * description: List of available days
+ * schema:
+ * type: object
+ * properties:
+ * meta:
+ * $ref: "schema/metaresult.json"
+ * data:
+ * $ref: "schema/citizenapi/availableDays.json"
+ */
+\App::$slim->get(
+ '/available-days/',
+ '\BO\Zmscitizenapi\Controllers\Availability\AvailableDaysListController'
+)
+ ->setName("AvailableDaysListController");
+
+/**
+ * @swagger
+ * /available-appointments/:
+ * get:
+ * summary: Get available appointments for a specific day
+ * tags:
+ * - appointments
+ * parameters:
+ * - name: date
+ * description: Date in format YYYY-MM-DD
+ * in: query
+ * required: true
+ * type: string
+ * - name: officeId
+ * description: Office ID
+ * in: query
+ * required: true
+ * type: integer
+ * - name: serviceId
+ * description: Service ID
+ * in: query
+ * required: true
+ * type: integer
+ * responses:
+ * 200:
+ * description: List of available appointments
+ * schema:
+ * type: object
+ * properties:
+ * meta:
+ * $ref: "schema/metaresult.json"
+ * data:
+ * $ref: "schema/citizenapi/availableAppointments.json"
+ */
+\App::$slim->get(
+ '/available-appointments/',
+ '\BO\Zmscitizenapi\Controllers\Availability\AvailableAppointmentsListController'
+)
+ ->setName("AvailableAppointmentsListController");
+
+/**
+ * @swagger
+ * /appointment/:
+ * get:
+ * summary: Get an appointment by process ID
+ * tags:
+ * - appointments
+ * parameters:
+ * - name: processId
+ * description: The unique identifier for the process. Must be an integer starting with 10 or 11, e.g., 100348.
+ * in: query
+ * required: true
+ * type: integer
+ * - name: authKey
+ * description: The authentication key consisting of 4 to 5 alphanumeric characters, e.g., 42a3.
+ * in: query
+ * required: true
+ * type: string
+ * responses:
+ * 200:
+ * description: Appointment details
+ * schema:
+ * type: object
+ * properties:
+ * meta:
+ * $ref: "schema/metaresult.json"
+ * data:
+ * $ref: "schema/citizenapi/thinnedProcess.json"
+ * 400:
+ * description: Invalid input
+ * schema:
+ * type: object
+ * properties:
+ * errors:
+ * type: array
+ * items:
+ * type: object
+ * properties:
+ * type:
+ * type: string
+ * msg:
+ * type: string
+ * path:
+ * type: string
+ * location:
+ * type: string
+ * 404:
+ * description: Appointment not found
+ */
+\App::$slim->get(
+ '/appointment/',
+ '\BO\Zmscitizenapi\Controllers\Appointment\AppointmentByIdController'
+)
+ ->setName("AppointmentByIdController");
+
+/**
+ * @swagger
+ * /captcha-details/:
+ * get:
+ * summary: Get CAPTCHA details
+ * tags:
+ * - captcha
+ * responses:
+ * 200:
+ * description: CAPTCHA details
+ * schema:
+ * type: object
+ * properties:
+ * meta:
+ * $ref: "schema/metaresult.json"
+ * data:
+ * $ref: "schema/citizenapi/captcha/friendlyCaptcha.json"
+ */
+\App::$slim->get(
+ '/captcha-details/',
+ '\BO\Zmscitizenapi\Controllers\Security\CaptchaController'
+)
+ ->setName("CaptchaController");
+
+/**
+ * @swagger
+ * /reserve-appointment/:
+ * post:
+ * summary: Reserve an appointment
+ * tags:
+ * - appointments
+ * parameters:
+ * - name: appointment
+ * description: Appointment reservation data
+ * in: body
+ * required: true
+ * schema:
+ * $ref: "schema/citizenapi/appointmentReserve.json"
+ * responses:
+ * 200:
+ * description: Reservation successful
+ * schema:
+ * type: object
+ * properties:
+ * meta:
+ * $ref: "schema/metaresult.json"
+ * data:
+ * $ref: "schema/citizenapi/thinnedProcess.json"
+ * 400:
+ * description: Invalid input
+ * schema:
+ * type: object
+ * properties:
+ * errors:
+ * type: array
+ * items:
+ * type: object
+ * properties:
+ * type:
+ * type: string
+ * msg:
+ * type: string
+ * path:
+ * type: string
+ * location:
+ * type: string
+ * 404:
+ * description: Appointment not found
+ */
+\App::$slim->post(
+ '/reserve-appointment/',
+ '\BO\Zmscitizenapi\Controllers\Appointment\AppointmentReserveController'
+)
+ ->setName("AppointmentReserveController");
+
+/**
+ * @swagger
+ * /update-appointment/:
+ * post:
+ * summary: Update an appointment
+ * tags:
+ * - appointments
+ * parameters:
+ * - name: appointment
+ * description: Appointment update data
+ * in: body
+ * required: true
+ * schema:
+ * $ref: "schema/citizenapi/appointmentUpdate.json"
+ * responses:
+ * 200:
+ * description: Update successful
+ * schema:
+ * type: object
+ * properties:
+ * meta:
+ * $ref: "schema/metaresult.json"
+ * data:
+ * $ref: "schema/citizenapi/thinnedProcess.json"
+ * 400:
+ * description: Invalid input
+ * schema:
+ * type: object
+ * properties:
+ * errors:
+ * type: array
+ * items:
+ * type: object
+ * properties:
+ * type:
+ * type: string
+ * msg:
+ * type: string
+ * path:
+ * type: string
+ * location:
+ * type: string
+ * 404:
+ * description: Appointment not found
+ */
+\App::$slim->post(
+ '/update-appointment/',
+ '\BO\Zmscitizenapi\Controllers\Appointment\AppointmentUpdateController'
+)
+ ->setName("AppointmentUpdateController");
+
+/**
+ * @swagger
+ * /confirm-appointment/:
+ * post:
+ * summary: Confirm an appointment
+ * tags:
+ * - appointments
+ * parameters:
+ * - name: appointment
+ * description: Appointment confirmation data
+ * in: body
+ * required: true
+ * schema:
+ * $ref: "schema/citizenapi/appointmentConfirm.json"
+ * responses:
+ * 200:
+ * description: Confirmation successful
+ * schema:
+ * type: object
+ * properties:
+ * meta:
+ * $ref: "schema/metaresult.json"
+ * data:
+ * $ref: "schema/citizenapi/thinnedProcess.json"
+ * 400:
+ * description: Invalid input
+ * schema:
+ * type: object
+ * properties:
+ * errors:
+ * type: array
+ * items:
+ * type: object
+ * properties:
+ * type:
+ * type: string
+ * msg:
+ * type: string
+ * path:
+ * type: string
+ * location:
+ * type: string
+ * 404:
+ * description: Appointment not found
+ */
+\App::$slim->post(
+ '/confirm-appointment/',
+ '\BO\Zmscitizenapi\Controllers\Appointment\AppointmentConfirmController'
+)
+ ->setName("AppointmentConfirmController");
+
+/**
+ * @swagger
+ * /preconfirm-appointment/:
+ * post:
+ * summary: Preconfirm an appointment
+ * tags:
+ * - appointments
+ * parameters:
+ * - name: appointment
+ * description: Appointment preconfirmation data
+ * in: body
+ * required: true
+ * schema:
+ * $ref: "schema/citizenapi/appointmentPreconfirm.json"
+ * responses:
+ * 200:
+ * description: Preconfirmation successful
+ * schema:
+ * type: object
+ * properties:
+ * meta:
+ * $ref: "schema/metaresult.json"
+ * data:
+ * $ref: "schema/citizenapi/thinnedProcess.json"
+ * 400:
+ * description: Invalid input
+ * schema:
+ * type: object
+ * properties:
+ * errors:
+ * type: array
+ * items:
+ * type: object
+ * properties:
+ * type:
+ * type: string
+ * msg:
+ * type: string
+ * path:
+ * type: string
+ * location:
+ * type: string
+ * 404:
+ * description: Appointment not found
+ */
+\App::$slim->post(
+ '/preconfirm-appointment/',
+ '\BO\Zmscitizenapi\Controllers\Appointment\AppointmentPreconfirmController'
+)
+ ->setName("AppointmentPreconfirmController");
+
+/**
+ * @swagger
+ * /cancel-appointment/:
+ * post:
+ * summary: Cancel an appointment
+ * tags:
+ * - appointments
+ * parameters:
+ * - name: appointment
+ * description: Appointment cancellation data
+ * in: body
+ * required: true
+ * schema:
+ * $ref: "schema/citizenapi/appointmentCancel.json"
+ * responses:
+ * 200:
+ * description: Cancellation successful
+ * schema:
+ * type: object
+ * properties:
+ * meta:
+ * $ref: "schema/metaresult.json"
+ * data:
+ * $ref: "schema/citizenapi/thinnedProcess.json"
+ * 400:
+ * description: Invalid input
+ * schema:
+ * type: object
+ * properties:
+ * errors:
+ * type: array
+ * items:
+ * type: object
+ * properties:
+ * type:
+ * type: string
+ * msg:
+ * type: string
+ * path:
+ * type: string
+ * location:
+ * type: string
+ * 404:
+ * description: Appointment not found
+ */
+\App::$slim->post(
+ '/cancel-appointment/',
+ '\BO\Zmscitizenapi\Controllers\Appointment\AppointmentCancelController'
+)
+ ->setName("AppointmentCancelController");
diff --git a/zmscitizenapi/src/Zmscitizenapi/Application.php b/zmscitizenapi/src/Zmscitizenapi/Application.php
new file mode 100644
index 000000000..dbba2d495
--- /dev/null
+++ b/zmscitizenapi/src/Zmscitizenapi/Application.php
@@ -0,0 +1,115 @@
+ (bool)self::$cache,
+ 'maintenance_mode' => self::$MAINTENANCE_MODE_ENABLED,
+ 'captcha_enabled' => self::$CAPTCHA_ENABLED
+ ]);*/
+ }
+
+ private static function initializeMaintenanceMode(): void
+ {
+ self::$MAINTENANCE_MODE_ENABLED = filter_var(
+ getenv('MAINTENANCE_ENABLED'),
+ FILTER_VALIDATE_BOOLEAN
+ );
+ }
+
+ private static function initializeCaptcha(): void
+ {
+ self::$CAPTCHA_ENABLED = filter_var(
+ getenv('CAPTCHA_ENABLED'),
+ FILTER_VALIDATE_BOOLEAN
+ );
+
+ self::$FRIENDLY_CAPTCHA_SECRET_KEY = getenv('FRIENDLY_CAPTCHA_SECRET_KEY') ?: '';
+ self::$FRIENDLY_CAPTCHA_SITE_KEY = getenv('FRIENDLY_CAPTCHA_SITE_KEY') ?: '';
+ self::$FRIENDLY_CAPTCHA_ENDPOINT = getenv('FRIENDLY_CAPTCHA_ENDPOINT')
+ ?: 'https://eu-api.friendlycaptcha.eu/api/v1/siteverify';
+ self::$FRIENDLY_CAPTCHA_ENDPOINT_PUZZLE = getenv('FRIENDLY_CAPTCHA_ENDPOINT_PUZZLE')
+ ?: 'https://eu-api.friendlycaptcha.eu/api/v1/puzzle';
+
+ self::$ALTCHA_CAPTCHA_SECRET_KEY = getenv('ALTCHA_CAPTCHA_SECRET_KEY') ?: '';
+ self::$ALTCHA_CAPTCHA_SITE_KEY = getenv('ALTCHA_CAPTCHA_SITE_KEY') ?: '';
+ self::$ALTCHA_CAPTCHA_ENDPOINT = getenv('ALTCHA_CAPTCHA_ENDPOINT')
+ ?: 'https://eu.altcha.org/form/';
+ self::$ALTCHA_CAPTCHA_ENDPOINT_PUZZLE = getenv('ALTCHA_CAPTCHA_ENDPOINT_PUZZLE')
+ ?: 'https://eu.altcha.org/';
+ }
+
+ private static function initializeCache(): void
+ {
+ self::$CACHE_DIR = getenv('CACHE_DIR') ?: __DIR__ . '/cache';
+ self::$CACHE_LIFETIME = (int)(getenv('CACHE_LIFETIME') ?: 3600);
+
+ self::validateCacheDirectory();
+ self::setupCache();
+ }
+
+ private static function validateCacheDirectory(): void
+ {
+ if (!is_dir(self::$CACHE_DIR) && !mkdir(self::$CACHE_DIR, 0750, true)) {
+ throw new \RuntimeException(
+ sprintf('Cache directory "%s" could not be created', self::$CACHE_DIR)
+ );
+ }
+
+ if (!is_writable(self::$CACHE_DIR)) {
+ throw new \RuntimeException(
+ sprintf('Cache directory "%s" is not writable', self::$CACHE_DIR)
+ );
+ }
+ }
+
+ private static function setupCache(): void
+ {
+ $psr6 = new FilesystemAdapter(
+ namespace: '',
+ defaultLifetime: self::$CACHE_LIFETIME,
+ directory: self::$CACHE_DIR
+ );
+
+ self::$cache = new Psr16Cache($psr6);
+ }
+}
+
+Application::initialize();
\ No newline at end of file
diff --git a/zmscitizenapi/src/Zmscitizenapi/BaseController.php b/zmscitizenapi/src/Zmscitizenapi/BaseController.php
new file mode 100644
index 000000000..c493012bb
--- /dev/null
+++ b/zmscitizenapi/src/Zmscitizenapi/BaseController.php
@@ -0,0 +1,53 @@
+initRequest($request);
+ $noCacheResponse = \BO\Slim\Render::withLastModified($response, time(), '0');
+ return $this->readResponse($request, $noCacheResponse, $args);
+ }
+
+ /**
+ * Hook method for handling responses in child controllers.
+ * Child classes should override this method to implement their specific response logic.
+ *
+ * @param RequestInterface $request The HTTP request
+ * @param ResponseInterface $response The HTTP response
+ * @param array $args Route parameters
+ * @return ResponseInterface The modified response
+ */
+ public function readResponse(RequestInterface $request, ResponseInterface $response, array $args): ResponseInterface
+ {
+ return parent::__invoke($request, $response, $args);
+ }
+
+ protected function createJsonResponse(ResponseInterface $response, array $content, int $statusCode): ResponseInterface
+ {
+ if ($statusCode < 100 || $statusCode > 599) {
+ throw new \InvalidArgumentException('Invalid HTTP status code');
+ }
+
+ $response = $response->withStatus($statusCode)
+ ->withHeader('Content-Type', 'application/json; charset=utf-8');
+
+ try {
+ // Add JSON_UNESCAPED_SLASHES to ensure slashes in HTML are not escaped
+ $json = json_encode($content, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
+ } catch (\JsonException $e) {
+ throw new \RuntimeException('Failed to encode JSON response: ' . $e->getMessage(), 0, $e);
+ }
+
+ $response->getBody()->write($json);
+
+ return $response;
+ }
+
+}
\ No newline at end of file
diff --git a/zmscitizenapi/src/Zmscitizenapi/Controllers/Appointment/AppointmentByIdController.php b/zmscitizenapi/src/Zmscitizenapi/Controllers/Appointment/AppointmentByIdController.php
new file mode 100644
index 000000000..b3b2913ff
--- /dev/null
+++ b/zmscitizenapi/src/Zmscitizenapi/Controllers/Appointment/AppointmentByIdController.php
@@ -0,0 +1,52 @@
+service = new AppointmentByIdService();
+ }
+
+ public function readResponse(RequestInterface $request, ResponseInterface $response, array $args): ResponseInterface
+ {
+ try {
+ $requestErrors = ValidationService::validateServerGetRequest($request);
+ if (!empty($requestErrors['errors'])) {
+ return $this->createJsonResponse(
+ $response,
+ $requestErrors,
+ ErrorMessages::get('invalidRequest')['statusCode']
+ );
+ }
+
+ $result = $this->service->getAppointmentById($request->getQueryParams());
+
+ return is_array($result) && isset($result['errors'])
+ ? $this->createJsonResponse(
+ $response,
+ $result,
+ ErrorMessages::getHighestStatusCode($result['errors'])
+ )
+ : $this->createJsonResponse($response, $result->toArray(), 200);
+
+ } catch (\Exception $e) {
+ return $this->createJsonResponse(
+ $response,
+ ErrorMessages::get('internalError'),
+ ErrorMessages::get('internalError')['statusCode']
+ );
+ }
+ }
+}
\ No newline at end of file
diff --git a/zmscitizenapi/src/Zmscitizenapi/Controllers/Appointment/AppointmentCancelController.php b/zmscitizenapi/src/Zmscitizenapi/Controllers/Appointment/AppointmentCancelController.php
new file mode 100644
index 000000000..ffaddc4c6
--- /dev/null
+++ b/zmscitizenapi/src/Zmscitizenapi/Controllers/Appointment/AppointmentCancelController.php
@@ -0,0 +1,52 @@
+service = new AppointmentCancelService();
+ }
+
+ public function readResponse(RequestInterface $request, ResponseInterface $response, array $args): ResponseInterface
+ {
+ try {
+ $requestErrors = ValidationService::validateServerPostRequest($request);
+ if (!empty($requestErrors['errors'])) {
+ return $this->createJsonResponse(
+ $response,
+ $requestErrors,
+ ErrorMessages::get('invalidRequest')['statusCode']
+ );
+ }
+
+ $result = $this->service->processCancel($request->getParsedBody());
+
+ return is_array($result) && isset($result['errors'])
+ ? $this->createJsonResponse(
+ $response,
+ $result,
+ ErrorMessages::getHighestStatusCode($result['errors'])
+ )
+ : $this->createJsonResponse($response, $result->toArray(), 200);
+
+ } catch (\Exception $e) {
+ return $this->createJsonResponse(
+ $response,
+ ErrorMessages::get('internalError'),
+ ErrorMessages::get('internalError')['statusCode']
+ );
+ }
+ }
+}
\ No newline at end of file
diff --git a/zmscitizenapi/src/Zmscitizenapi/Controllers/Appointment/AppointmentConfirmController.php b/zmscitizenapi/src/Zmscitizenapi/Controllers/Appointment/AppointmentConfirmController.php
new file mode 100644
index 000000000..1846a118e
--- /dev/null
+++ b/zmscitizenapi/src/Zmscitizenapi/Controllers/Appointment/AppointmentConfirmController.php
@@ -0,0 +1,52 @@
+service = new AppointmentConfirmService();
+ }
+
+ public function readResponse(RequestInterface $request, ResponseInterface $response, array $args): ResponseInterface
+ {
+ try {
+ $requestErrors = ValidationService::validateServerPostRequest($request);
+ if (!empty($requestErrors['errors'])) {
+ return $this->createJsonResponse(
+ $response,
+ $requestErrors,
+ ErrorMessages::get('invalidRequest')['statusCode']
+ );
+ }
+
+ $result = $this->service->processConfirm($request->getParsedBody());
+
+ return is_array($result) && isset($result['errors'])
+ ? $this->createJsonResponse(
+ $response,
+ $result,
+ ErrorMessages::getHighestStatusCode($result['errors'])
+ )
+ : $this->createJsonResponse($response, $result->toArray(), 200);
+
+ } catch (\Exception $e) {
+ return $this->createJsonResponse(
+ $response,
+ ErrorMessages::get('internalError'),
+ ErrorMessages::get('internalError')['statusCode']
+ );
+ }
+ }
+}
\ No newline at end of file
diff --git a/zmscitizenapi/src/Zmscitizenapi/Controllers/Appointment/AppointmentPreconfirmController.php b/zmscitizenapi/src/Zmscitizenapi/Controllers/Appointment/AppointmentPreconfirmController.php
new file mode 100644
index 000000000..a2bf77241
--- /dev/null
+++ b/zmscitizenapi/src/Zmscitizenapi/Controllers/Appointment/AppointmentPreconfirmController.php
@@ -0,0 +1,52 @@
+service = new AppointmentPreconfirmService();
+ }
+
+ public function readResponse(RequestInterface $request, ResponseInterface $response, array $args): ResponseInterface
+ {
+ try {
+ $requestErrors = ValidationService::validateServerPostRequest($request);
+ if (!empty($requestErrors['errors'])) {
+ return $this->createJsonResponse(
+ $response,
+ $requestErrors,
+ ErrorMessages::get('invalidRequest')['statusCode']
+ );
+ }
+
+ $result = $this->service->processPreconfirm($request->getParsedBody());
+
+ return is_array($result) && isset($result['errors'])
+ ? $this->createJsonResponse(
+ $response,
+ $result,
+ ErrorMessages::getHighestStatusCode($result['errors'])
+ )
+ : $this->createJsonResponse($response, $result->toArray(), 200);
+
+ } catch (\Exception $e) {
+ return $this->createJsonResponse(
+ $response,
+ ErrorMessages::get('internalError'),
+ ErrorMessages::get('internalError')['statusCode']
+ );
+ }
+ }
+}
\ No newline at end of file
diff --git a/zmscitizenapi/src/Zmscitizenapi/Controllers/Appointment/AppointmentReserveController.php b/zmscitizenapi/src/Zmscitizenapi/Controllers/Appointment/AppointmentReserveController.php
new file mode 100644
index 000000000..fc12e038f
--- /dev/null
+++ b/zmscitizenapi/src/Zmscitizenapi/Controllers/Appointment/AppointmentReserveController.php
@@ -0,0 +1,52 @@
+service = new AppointmentReserveService();
+ }
+
+ public function readResponse(RequestInterface $request, ResponseInterface $response, array $args): ResponseInterface
+ {
+ try {
+ $requestErrors = ValidationService::validateServerPostRequest($request);
+ if (!empty($requestErrors['errors'])) {
+ return $this->createJsonResponse(
+ $response,
+ $requestErrors,
+ ErrorMessages::get('invalidRequest')['statusCode']
+ );
+ }
+
+ $result = $this->service->processReservation($request->getParsedBody());
+
+ return is_array($result) && isset($result['errors'])
+ ? $this->createJsonResponse(
+ $response,
+ $result,
+ ErrorMessages::getHighestStatusCode($result['errors'])
+ )
+ : $this->createJsonResponse($response, $result->toArray(), 200);
+
+ } catch (\Exception $e) {
+ return $this->createJsonResponse(
+ $response,
+ ErrorMessages::get('internalError'),
+ ErrorMessages::get('internalError')['statusCode']
+ );
+ }
+ }
+}
\ No newline at end of file
diff --git a/zmscitizenapi/src/Zmscitizenapi/Controllers/Appointment/AppointmentUpdateController.php b/zmscitizenapi/src/Zmscitizenapi/Controllers/Appointment/AppointmentUpdateController.php
new file mode 100644
index 000000000..8eec5785c
--- /dev/null
+++ b/zmscitizenapi/src/Zmscitizenapi/Controllers/Appointment/AppointmentUpdateController.php
@@ -0,0 +1,52 @@
+service = new AppointmentUpdateService();
+ }
+
+ public function readResponse(RequestInterface $request, ResponseInterface $response, array $args): ResponseInterface
+ {
+ try {
+ $requestErrors = ValidationService::validateServerPostRequest($request);
+ if (!empty($requestErrors['errors'])) {
+ return $this->createJsonResponse(
+ $response,
+ $requestErrors,
+ ErrorMessages::get('invalidRequest')['statusCode']
+ );
+ }
+
+ $result = $this->service->processUpdate($request->getParsedBody());
+
+ return is_array($result) && isset($result['errors'])
+ ? $this->createJsonResponse(
+ $response,
+ $result,
+ ErrorMessages::getHighestStatusCode($result['errors'])
+ )
+ : $this->createJsonResponse($response, $result->toArray(), 200);
+
+ } catch (\Exception $e) {
+ return $this->createJsonResponse(
+ $response,
+ ErrorMessages::get('internalError'),
+ ErrorMessages::get('internalError')['statusCode']
+ );
+ }
+ }
+}
\ No newline at end of file
diff --git a/zmscitizenapi/src/Zmscitizenapi/Controllers/Availability/AvailableAppointmentsListController.php b/zmscitizenapi/src/Zmscitizenapi/Controllers/Availability/AvailableAppointmentsListController.php
new file mode 100644
index 000000000..272783529
--- /dev/null
+++ b/zmscitizenapi/src/Zmscitizenapi/Controllers/Availability/AvailableAppointmentsListController.php
@@ -0,0 +1,52 @@
+service = new AvailableAppointmentsListService();
+ }
+
+ public function readResponse(RequestInterface $request, ResponseInterface $response, array $args): ResponseInterface
+ {
+ try {
+ $requestErrors = ValidationService::validateServerGetRequest($request);
+ if (!empty($requestErrors['errors'])) {
+ return $this->createJsonResponse(
+ $response,
+ $requestErrors,
+ ErrorMessages::get('invalidRequest')['statusCode']
+ );
+ }
+
+ $result = $this->service->getAvailableAppointmentsList($request->getQueryParams());
+
+ return is_array($result) && isset($result['errors'])
+ ? $this->createJsonResponse(
+ $response,
+ $result,
+ ErrorMessages::getHighestStatusCode($result['errors'])
+ )
+ : $this->createJsonResponse($response, $result->toArray(), 200);
+
+ } catch (\Exception $e) {
+ return $this->createJsonResponse(
+ $response,
+ ErrorMessages::get('internalError'),
+ ErrorMessages::get('internalError')['statusCode']
+ );
+ }
+ }
+}
\ No newline at end of file
diff --git a/zmscitizenapi/src/Zmscitizenapi/Controllers/Availability/AvailableDaysListController.php b/zmscitizenapi/src/Zmscitizenapi/Controllers/Availability/AvailableDaysListController.php
new file mode 100644
index 000000000..cf691fc2d
--- /dev/null
+++ b/zmscitizenapi/src/Zmscitizenapi/Controllers/Availability/AvailableDaysListController.php
@@ -0,0 +1,52 @@
+service = new AvailableDaysListService();
+ }
+
+ public function readResponse(RequestInterface $request, ResponseInterface $response, array $args): ResponseInterface
+ {
+ try {
+ $requestErrors = ValidationService::validateServerGetRequest($request);
+ if (!empty($requestErrors['errors'])) {
+ return $this->createJsonResponse(
+ $response,
+ $requestErrors,
+ ErrorMessages::get('invalidRequest')['statusCode']
+ );
+ }
+
+ $result = $this->service->getAvailableDaysList($request->getQueryParams());
+
+ return is_array($result) && isset($result['errors'])
+ ? $this->createJsonResponse(
+ $response,
+ $result,
+ ErrorMessages::getHighestStatusCode($result['errors'])
+ )
+ : $this->createJsonResponse($response, $result->toArray(), 200);
+
+ } catch (\Exception $e) {
+ return $this->createJsonResponse(
+ $response,
+ ErrorMessages::get('internalError'),
+ ErrorMessages::get('internalError')['statusCode']
+ );
+ }
+ }
+}
diff --git a/zmscitizenapi/src/Zmscitizenapi/Controllers/Office/OfficeListByServiceController.php b/zmscitizenapi/src/Zmscitizenapi/Controllers/Office/OfficeListByServiceController.php
new file mode 100644
index 000000000..9b021650a
--- /dev/null
+++ b/zmscitizenapi/src/Zmscitizenapi/Controllers/Office/OfficeListByServiceController.php
@@ -0,0 +1,52 @@
+service = new OfficeListByServiceService();
+ }
+
+ public function readResponse(RequestInterface $request, ResponseInterface $response, array $args): ResponseInterface
+ {
+ try {
+ $requestErrors = ValidationService::validateServerGetRequest($request);
+ if (!empty($requestErrors['errors'])) {
+ return $this->createJsonResponse(
+ $response,
+ $requestErrors,
+ ErrorMessages::get('invalidRequest')['statusCode']
+ );
+ }
+
+ $result = $this->service->getOfficeList($request->getQueryParams());
+
+ return is_array($result) && isset($result['errors'])
+ ? $this->createJsonResponse(
+ $response,
+ $result,
+ ErrorMessages::getHighestStatusCode($result['errors'])
+ )
+ : $this->createJsonResponse($response, $result->toArray(), 200);
+
+ } catch (\Exception $e) {
+ return $this->createJsonResponse(
+ $response,
+ ErrorMessages::get('internalError'),
+ ErrorMessages::get('internalError')['statusCode']
+ );
+ }
+ }
+}
diff --git a/zmscitizenapi/src/Zmscitizenapi/Controllers/Office/OfficesListController.php b/zmscitizenapi/src/Zmscitizenapi/Controllers/Office/OfficesListController.php
new file mode 100644
index 000000000..7b91a975d
--- /dev/null
+++ b/zmscitizenapi/src/Zmscitizenapi/Controllers/Office/OfficesListController.php
@@ -0,0 +1,52 @@
+service = new OfficesListService();
+ }
+
+ public function readResponse(RequestInterface $request, ResponseInterface $response, array $args): ResponseInterface
+ {
+ try {
+ $requestErrors = ValidationService::validateServerGetRequest($request);
+ if (!empty($requestErrors['errors'])) {
+ return $this->createJsonResponse(
+ $response,
+ $requestErrors,
+ ErrorMessages::get('invalidRequest')['statusCode']
+ );
+ }
+
+ $result = $this->service->getOfficesList();
+
+ return is_array($result) && isset($result['errors'])
+ ? $this->createJsonResponse(
+ $response,
+ $result,
+ ErrorMessages::getHighestStatusCode($result['errors'])
+ )
+ : $this->createJsonResponse($response, $result->toArray(), 200);
+
+ } catch (\Exception $e) {
+ return $this->createJsonResponse(
+ $response,
+ ErrorMessages::get('internalError'),
+ ErrorMessages::get('internalError')['statusCode']
+ );
+ }
+ }
+}
\ No newline at end of file
diff --git a/zmscitizenapi/src/Zmscitizenapi/Controllers/Office/OfficesServicesRelationsController.php b/zmscitizenapi/src/Zmscitizenapi/Controllers/Office/OfficesServicesRelationsController.php
new file mode 100644
index 000000000..eda9413a4
--- /dev/null
+++ b/zmscitizenapi/src/Zmscitizenapi/Controllers/Office/OfficesServicesRelationsController.php
@@ -0,0 +1,52 @@
+service = new OfficesServicesRelationsService();
+ }
+
+ public function readResponse(RequestInterface $request, ResponseInterface $response, array $args): ResponseInterface
+ {
+ try {
+ $requestErrors = ValidationService::validateServerGetRequest($request);
+ if (!empty($requestErrors['errors'])) {
+ return $this->createJsonResponse(
+ $response,
+ $requestErrors,
+ ErrorMessages::get('invalidRequest')['statusCode']
+ );
+ }
+
+ $result = $this->service->getServicesAndOfficesList();
+
+ return is_array($result) && isset($result['errors'])
+ ? $this->createJsonResponse(
+ $response,
+ $result,
+ ErrorMessages::getHighestStatusCode($result['errors'])
+ )
+ : $this->createJsonResponse($response, $result->toArray(), 200);
+
+ } catch (\Exception $e) {
+ return $this->createJsonResponse(
+ $response,
+ ErrorMessages::get('internalError'),
+ ErrorMessages::get('internalError')['statusCode']
+ );
+ }
+ }
+}
\ No newline at end of file
diff --git a/zmscitizenapi/src/Zmscitizenapi/Controllers/Scope/ScopeByIdController.php b/zmscitizenapi/src/Zmscitizenapi/Controllers/Scope/ScopeByIdController.php
new file mode 100644
index 000000000..bc0b35bb3
--- /dev/null
+++ b/zmscitizenapi/src/Zmscitizenapi/Controllers/Scope/ScopeByIdController.php
@@ -0,0 +1,52 @@
+service = new ScopeByIdService();
+ }
+
+ public function readResponse(RequestInterface $request, ResponseInterface $response, array $args): ResponseInterface
+ {
+ try {
+ $requestErrors = ValidationService::validateServerGetRequest($request);
+ if (!empty($requestErrors['errors'])) {
+ return $this->createJsonResponse(
+ $response,
+ $requestErrors,
+ ErrorMessages::get('invalidRequest')['statusCode']
+ );
+ }
+
+ $result = $this->service->getScope($request->getQueryParams());
+
+ return is_array($result) && isset($result['errors'])
+ ? $this->createJsonResponse(
+ $response,
+ $result,
+ ErrorMessages::getHighestStatusCode($result['errors'])
+ )
+ : $this->createJsonResponse($response, $result->toArray(), 200);
+
+ } catch (\Exception $e) {
+ return $this->createJsonResponse(
+ $response,
+ ErrorMessages::get('internalError'),
+ ErrorMessages::get('internalError')['statusCode']
+ );
+ }
+ }
+}
\ No newline at end of file
diff --git a/zmscitizenapi/src/Zmscitizenapi/Controllers/Scope/ScopesListController.php b/zmscitizenapi/src/Zmscitizenapi/Controllers/Scope/ScopesListController.php
new file mode 100644
index 000000000..d7e027dd3
--- /dev/null
+++ b/zmscitizenapi/src/Zmscitizenapi/Controllers/Scope/ScopesListController.php
@@ -0,0 +1,52 @@
+service = new ScopesListService();
+ }
+
+ public function readResponse(RequestInterface $request, ResponseInterface $response, array $args): ResponseInterface
+ {
+ try {
+ $requestErrors = ValidationService::validateServerGetRequest($request);
+ if (!empty($requestErrors['errors'])) {
+ return $this->createJsonResponse(
+ $response,
+ $requestErrors,
+ ErrorMessages::get('invalidRequest')['statusCode']
+ );
+ }
+
+ $result = $this->service->getScopesList();
+
+ return is_array($result) && isset($result['errors'])
+ ? $this->createJsonResponse(
+ $response,
+ $result,
+ ErrorMessages::getHighestStatusCode($result['errors'])
+ )
+ : $this->createJsonResponse($response, $result->toArray(), 200);
+
+ } catch (\Exception $e) {
+ return $this->createJsonResponse(
+ $response,
+ ErrorMessages::get('internalError'),
+ ErrorMessages::get('internalError')['statusCode']
+ );
+ }
+ }
+}
\ No newline at end of file
diff --git a/zmscitizenapi/src/Zmscitizenapi/Controllers/Security/CaptchaController.php b/zmscitizenapi/src/Zmscitizenapi/Controllers/Security/CaptchaController.php
new file mode 100644
index 000000000..75cb8615d
--- /dev/null
+++ b/zmscitizenapi/src/Zmscitizenapi/Controllers/Security/CaptchaController.php
@@ -0,0 +1,52 @@
+service = new CaptchaService();
+ }
+
+ public function readResponse(RequestInterface $request, ResponseInterface $response, array $args): ResponseInterface
+ {
+ try {
+ $requestErrors = ValidationService::validateServerGetRequest($request);
+ if (!empty($requestErrors['errors'])) {
+ return $this->createJsonResponse(
+ $response,
+ $requestErrors,
+ ErrorMessages::get('invalidRequest')['statusCode']
+ );
+ }
+
+ $result = $this->service->getCaptcha();
+
+ return is_array($result) && isset($result['errors'])
+ ? $this->createJsonResponse(
+ $response,
+ $result,
+ ErrorMessages::getHighestStatusCode($result['errors'])
+ )
+ : $this->createJsonResponse($response, $result, 200);
+
+ } catch (\Exception $e) {
+ return $this->createJsonResponse(
+ $response,
+ ['errors' => [ErrorMessages::get('captchaVerificationError')]],
+ 500
+ );
+ }
+ }
+}
\ No newline at end of file
diff --git a/zmscitizenapi/src/Zmscitizenapi/Controllers/Service/ServiceListByOfficeController.php b/zmscitizenapi/src/Zmscitizenapi/Controllers/Service/ServiceListByOfficeController.php
new file mode 100644
index 000000000..e3cd9e9ce
--- /dev/null
+++ b/zmscitizenapi/src/Zmscitizenapi/Controllers/Service/ServiceListByOfficeController.php
@@ -0,0 +1,52 @@
+service = new ServiceListByOfficeService();
+ }
+
+ public function readResponse(RequestInterface $request, ResponseInterface $response, array $args): ResponseInterface
+ {
+ try {
+ $requestErrors = ValidationService::validateServerGetRequest($request);
+ if (!empty($requestErrors['errors'])) {
+ return $this->createJsonResponse(
+ $response,
+ $requestErrors,
+ ErrorMessages::get('invalidRequest')['statusCode']
+ );
+ }
+
+ $result = $this->service->getServiceList($request->getQueryParams());
+
+ return is_array($result) && isset($result['errors'])
+ ? $this->createJsonResponse(
+ $response,
+ $result,
+ ErrorMessages::getHighestStatusCode($result['errors'])
+ )
+ : $this->createJsonResponse($response, $result->toArray(), 200);
+
+ } catch (\Exception $e) {
+ return $this->createJsonResponse(
+ $response,
+ ErrorMessages::get('internalError'),
+ ErrorMessages::get('internalError')['statusCode']
+ );
+ }
+ }
+}
\ No newline at end of file
diff --git a/zmscitizenapi/src/Zmscitizenapi/Controllers/Service/ServicesListController.php b/zmscitizenapi/src/Zmscitizenapi/Controllers/Service/ServicesListController.php
new file mode 100644
index 000000000..7f69df90d
--- /dev/null
+++ b/zmscitizenapi/src/Zmscitizenapi/Controllers/Service/ServicesListController.php
@@ -0,0 +1,52 @@
+service = new ServicesListService();
+ }
+
+ public function readResponse(RequestInterface $request, ResponseInterface $response, array $args): ResponseInterface
+ {
+ try {
+ $requestErrors = ValidationService::validateServerGetRequest($request);
+ if (!empty($requestErrors['errors'])) {
+ return $this->createJsonResponse(
+ $response,
+ $requestErrors,
+ ErrorMessages::get('invalidRequest')['statusCode']
+ );
+ }
+
+ $result = $this->service->getServicesList();
+
+ return is_array($result) && isset($result['errors'])
+ ? $this->createJsonResponse(
+ $response,
+ $result,
+ ErrorMessages::getHighestStatusCode($result['errors'])
+ )
+ : $this->createJsonResponse($response, $result->toArray(), 200);
+
+ } catch (\Exception $e) {
+ return $this->createJsonResponse(
+ $response,
+ ErrorMessages::get('internalError'),
+ ErrorMessages::get('internalError')['statusCode']
+ );
+ }
+ }
+}
diff --git a/zmscitizenapi/src/Zmscitizenapi/Helper/ClientIpHelper.php b/zmscitizenapi/src/Zmscitizenapi/Helper/ClientIpHelper.php
new file mode 100644
index 000000000..ef43ff71a
--- /dev/null
+++ b/zmscitizenapi/src/Zmscitizenapi/Helper/ClientIpHelper.php
@@ -0,0 +1,18 @@
+ (int) $date->format('d'),
+ 'month' => (int) $date->format('m'),
+ 'year' => (int) $date->format('Y'),
+ ];
+ }
+
+ public static function getInternalDateFromISO(string $dateString): array
+ {
+ try {
+ if (!is_string($dateString)) {
+ throw new \InvalidArgumentException('Date string must be a string');
+ }
+ $date = new \DateTime($dateString);
+ return self::formatDateArray($date);
+ } catch (\Exception $e) {
+ throw new \InvalidArgumentException('Invalid ISO date format: ' . $e->getMessage());
+ }
+ }
+
+ public static function getInternalDateFromTimestamp(int $timestamp): array
+ {
+ try {
+ $date = (new \DateTime())->setTimestamp($timestamp);
+ return self::formatDateArray($date);
+ } catch (\Exception $e) {
+ throw new \InvalidArgumentException('Invalid timestamp: ' . $e->getMessage());
+ }
+ }
+
+}
diff --git a/zmscitizenapi/src/Zmscitizenapi/Helper/ErrorHandler.php b/zmscitizenapi/src/Zmscitizenapi/Helper/ErrorHandler.php
new file mode 100644
index 000000000..af7838317
--- /dev/null
+++ b/zmscitizenapi/src/Zmscitizenapi/Helper/ErrorHandler.php
@@ -0,0 +1,92 @@
+getStatusCode($exception);
+
+ if ($logErrors) {
+ $this->logError($exception, $request, $displayErrorDetails, $logErrorDetails);
+ }
+
+ $response = new \Slim\Psr7\Response();
+ $payload = $this->formatErrorPayload($exception, $displayErrorDetails);
+
+ $response->getBody()->write(json_encode($payload));
+
+ return $response
+ ->withHeader('Content-Type', 'application/json')
+ ->withStatus($statusCode);
+ }
+
+ private function getStatusCode(\Throwable $exception): int
+ {
+ if ($exception instanceof HttpException) {
+ return $exception->getCode();
+ }
+
+ return 500;
+ }
+
+ private function formatErrorPayload(\Throwable $exception, bool $displayErrorDetails): array
+ {
+ $error = [
+ 'message' => $this->getErrorMessage($exception, $displayErrorDetails),
+ 'code' => $exception->getCode()
+ ];
+
+ if ($displayErrorDetails) {
+ $error['type'] = get_class($exception);
+ $error['file'] = $exception->getFile();
+ $error['line'] = $exception->getLine();
+ $error['trace'] = $exception->getTrace();
+ }
+
+ return ['error' => $error];
+ }
+
+ private function getErrorMessage(\Throwable $exception, bool $displayErrorDetails): string
+ {
+ if ($displayErrorDetails) {
+ return $exception->getMessage();
+ }
+
+ if ($exception instanceof HttpException) {
+ return $exception->getMessage();
+ }
+
+ return 'An internal error has occurred.';
+ }
+
+ private function logError(
+ \Throwable $exception,
+ ServerRequestInterface $request,
+ bool $displayErrorDetails,
+ bool $logErrorDetails
+ ): void {
+ LoggerService::logError($exception, $request, null, [
+ 'displayErrorDetails' => $displayErrorDetails,
+ 'logErrorDetails' => $logErrorDetails,
+ 'uri' => (string)$request->getUri(),
+ 'method' => $request->getMethod(),
+ 'ip' => ClientIpHelper::getClientIp()
+ ]);
+ }
+}
\ No newline at end of file
diff --git a/zmscitizenapi/src/Zmscitizenapi/Localization/ErrorMessages.php b/zmscitizenapi/src/Zmscitizenapi/Localization/ErrorMessages.php
new file mode 100644
index 000000000..641657c34
--- /dev/null
+++ b/zmscitizenapi/src/Zmscitizenapi/Localization/ErrorMessages.php
@@ -0,0 +1,697 @@
+ [
+ 'errorCode' => 'notImplemented',
+ 'statusCode' => self::HTTP_NOT_IMPLEMENTED,
+ 'errorMessage' => 'Feature not implemented yet.',
+ ],
+ 'invalidRequest' => [
+ 'errorCode' => 'invalidRequest',
+ 'statusCode' => self::HTTP_BAD_REQUEST,
+ 'errorMessage' => 'Invalid request.'
+ ],
+ 'requestMethodNotAllowed' => [
+ 'errorCode' => 'requestMethodNotAllowed',
+ 'statusCode' => self::HTTP_INVALID_REQUEST_METHOD,
+ 'errorMessage' => 'Request method not allowed.',
+ ],
+ 'captchaVerificationFailed' => [
+ 'errorCode' => 'captchaVerificationFailed',
+ 'statusCode' => self::HTTP_BAD_REQUEST,
+ 'errorMessage' => 'Captcha verification failed.'
+ ],
+ 'invalidLocationAndServiceCombination' => [
+ 'errorCode' => 'invalidLocationAndServiceCombination',
+ 'statusCode' => self::HTTP_BAD_REQUEST,
+ 'errorMessage' => 'The provided service(s) do not exist at the given location.'
+ ],
+ 'invalidStartDate' => [
+ 'errorCode' => 'invalidStartDate',
+ 'statusCode' => self::HTTP_BAD_REQUEST,
+ 'errorMessage' => 'startDate is required and must be a valid date.'
+ ],
+ 'invalidEndDate' => [
+ 'errorCode' => 'invalidEndDate',
+ 'statusCode' => self::HTTP_BAD_REQUEST,
+ 'errorMessage' => 'endDate is required and must be a valid date.'
+ ],
+ 'invalidOfficeId' => [
+ 'errorCode' => 'invalidOfficeId',
+ 'statusCode' => self::HTTP_BAD_REQUEST,
+ 'errorMessage' => 'officeId should be a 32-bit integer.'
+ ],
+ 'invalidServiceId' => [
+ 'errorCode' => 'invalidServiceId',
+ 'statusCode' => self::HTTP_BAD_REQUEST,
+ 'errorMessage' => 'serviceId should be a 32-bit integer.'
+ ],
+ 'emptyServiceArrays' => [
+ 'errorCode' => 'EMPTY_SERVICE_ARRAYS',
+ 'statusCode' => self::HTTP_BAD_REQUEST,
+ 'errorMessage' => 'Service IDs and counts cannot be empty'
+ ],
+ 'mismatchedArrays' => [
+ 'errorCode' => 'MISMATCHED_ARRAYS',
+ 'statusCode' => self::HTTP_BAD_REQUEST,
+ 'errorMessage' => 'Service IDs and counts must have same length'
+ ],
+ 'invalidServiceCount' => [
+ 'errorCode' => 'invalidServiceCount',
+ 'statusCode' => self::HTTP_BAD_REQUEST,
+ 'errorMessage' => 'serviceCounts should be an array of numeric values.'
+ ],
+ 'invalidProcessId' => [
+ 'errorCode' => 'invalidProcessId',
+ 'statusCode' => self::HTTP_BAD_REQUEST,
+ 'errorMessage' => 'processId should be a positive 32-bit integer.'
+ ],
+ 'invalidScopeId' => [
+ 'errorCode' => 'invalidScopeId',
+ 'statusCode' => self::HTTP_BAD_REQUEST,
+ 'errorMessage' => 'scopeId should be a positive 32-bit integer.'
+ ],
+ 'invalidAuthKey' => [
+ 'errorCode' => 'invalidAuthKey',
+ 'statusCode' => self::HTTP_BAD_REQUEST,
+ 'errorMessage' => 'authKey should be a string.'
+ ],
+ 'invalidDate' => [
+ 'errorCode' => 'invalidDate',
+ 'statusCode' => self::HTTP_BAD_REQUEST,
+ 'errorMessage' => 'date is required and must be a valid date.'
+ ],
+ 'invalidTimestamp' => [
+ 'errorCode' => 'invalidTimestamp',
+ 'statusCode' => self::HTTP_BAD_REQUEST,
+ 'errorMessage' => 'Missing timestamp or invalid timestamp format. It should be a positive numeric value.'
+ ],
+ 'invalidFamilyName' => [
+ 'errorCode' => 'invalidFamilyName',
+ 'statusCode' => self::HTTP_BAD_REQUEST,
+ 'errorMessage' => 'familyName should be a non-empty string.'
+ ],
+ 'invalidEmail' => [
+ 'errorCode' => 'invalidEmail',
+ 'statusCode' => self::HTTP_BAD_REQUEST,
+ 'errorMessage' => 'email should be a valid email address.'
+ ],
+ 'invalidTelephone' => [
+ 'errorCode' => 'invalidTelephone',
+ 'statusCode' => self::HTTP_BAD_REQUEST,
+ 'errorMessage' => 'telephone should be a numeric string between 7 and 15 digits.'
+ ],
+ 'invalidCustomTextfield' => [
+ 'errorCode' => 'invalidCustomTextfield',
+ 'statusCode' => self::HTTP_BAD_REQUEST,
+ 'errorMessage' => 'customTextfield should be a string.'
+ ],
+ 'appointmentCanNotBeCanceled' => [
+ 'errorCode' => 'appointmentCanNotBeCanceled',
+ 'statusCode' => self::HTTP_NOT_ACCEPTABLE,
+ 'errorMessage' => 'The selected appointment cannot be canceled.'
+ ],
+ 'appointmentNotAvailable' => [
+ 'errorCode' => 'appointmentNotAvailable',
+ 'statusCode' => self::HTTP_NOT_FOUND,
+ 'errorMessage' => 'The selected appointment is unfortunately no longer available.'
+ ],
+ 'noAppointmentForThisDay' => [
+ 'errorCode' => 'noAppointmentForThisDay',
+ 'statusCode' => self::HTTP_NOT_FOUND,
+ 'errorMessage' => 'No available days found for the given criteria.'
+ ],
+ 'captchaVerificationError' => [
+ 'errorCode' => 'captchaVerificationError',
+ 'statusCode' => self::HTTP_BAD_REQUEST,
+ 'errorMessage' => 'An error occurred during captcha verification.'
+ ],
+ 'serviceUnavailable' => [
+ 'errorCode' => 'serviceUnavailable',
+ 'statusCode' => self::HTTP_UNAVAILABLE,
+ 'errorMessage' => 'Service Unavailable: The application is under maintenance.'
+ ],
+
+ //Zmsapi exceptions
+ 'internalError' => [
+ 'errorCode' => 'internalError',
+ 'errorMessage' => 'An internal error occurred. Please try again later.',
+ 'statusCode' => self::HTTP_INTERNAL_SERVER_ERROR
+ ],
+ 'invalidApiClient' => [
+ 'errorCode' => 'invalidApiClient',
+ 'errorMessage' => 'Invalid API client.',
+ 'statusCode' => self::HTTP_BAD_REQUEST
+ ],
+ 'departmentNotFound' => [
+ 'errorCode' => 'departmentNotFound',
+ 'errorMessage' => 'Department not found.',
+ 'statusCode' => self::HTTP_NOT_FOUND
+ ],
+ 'mailNotFound' => [
+ 'errorCode' => 'mailNotFound',
+ 'errorMessage' => 'Mail template not found.',
+ 'statusCode' => self::HTTP_NOT_FOUND
+ ],
+ 'organisationNotFound' => [
+ 'errorCode' => 'organisationNotFound',
+ 'errorMessage' => 'Organisation not found.',
+ 'statusCode' => self::HTTP_NOT_FOUND
+ ],
+ 'providerNotFound' => [
+ 'errorCode' => 'providerNotFound',
+ 'errorMessage' => 'Provider not found.',
+ 'statusCode' => self::HTTP_NOT_FOUND
+ ],
+ 'requestNotFound' => [
+ 'errorCode' => 'requestNotFound',
+ 'errorMessage' => 'Request not found.',
+ 'statusCode' => self::HTTP_NOT_FOUND
+ ],
+ 'scopeNotFound' => [
+ 'errorCode' => 'scopeNotFound',
+ 'errorMessage' => 'Scope not found.',
+ 'statusCode' => self::HTTP_NOT_FOUND
+ ],
+ 'processInvalid' => [
+ 'errorCode' => 'processInvalid',
+ 'errorMessage' => 'The process data is invalid.',
+ 'statusCode' => self::HTTP_BAD_REQUEST
+ ],
+ 'processAlreadyExists' => [
+ 'errorCode' => 'processAlreadyExists',
+ 'errorMessage' => 'An appointment process already exists.',
+ 'statusCode' => self::HTTP_CONFLICT
+ ],
+ 'processDeleteFailed' => [
+ 'errorCode' => 'processDeleteFailed',
+ 'errorMessage' => 'Failed to delete the appointment.',
+ 'statusCode' => self::HTTP_INTERNAL_SERVER_ERROR
+ ],
+ 'processAlreadyCalled' => [
+ 'errorCode' => 'processAlreadyCalled',
+ 'errorMessage' => 'The appointment has already been called.',
+ 'statusCode' => self::HTTP_CONFLICT
+ ],
+ 'processNotReservedAnymore' => [
+ 'errorCode' => 'processNotReservedAnymore',
+ 'errorMessage' => 'The appointment is no longer reserved.',
+ 'statusCode' => self::HTTP_CONFLICT
+ ],
+ 'processNotPreconfirmedAnymore' => [
+ 'errorCode' => 'processNotPreconfirmedAnymore',
+ 'errorMessage' => 'The appointment is no longer preconfirmed.',
+ 'statusCode' => self::HTTP_CONFLICT
+ ],
+ 'emailIsRequired' => [
+ 'errorCode' => 'emailIsRequired',
+ 'errorMessage' => 'Email address is required.',
+ 'statusCode' => self::HTTP_NOT_ACCEPTABLE
+ ],
+ 'telephoneIsRequired' => [
+ 'errorCode' => 'telephoneIsRequired',
+ 'errorMessage' => 'Telephone number is required.',
+ 'statusCode' => self::HTTP_NOT_ACCEPTABLE
+ ],
+ 'appointmentNotFound' => [
+ 'errorCode' => 'appointmentNotFound',
+ 'errorMessage' => 'The requested appointment could not be found.',
+ 'statusCode' => self::HTTP_NOT_FOUND
+ ],
+ 'authKeyMismatch' => [
+ 'errorCode' => 'authKeyMismatch',
+ 'errorMessage' => 'Invalid authentication key.',
+ 'statusCode' => self::HTTP_NOT_ACCEPTABLE
+ ],
+ 'noAppointmentsAtLocation' => [
+ 'errorCode' => 'noAppointmentsAtLocation',
+ 'errorMessage' => 'No appointments available at the specified location.',
+ 'statusCode' => self::HTTP_NOT_FOUND
+ ],
+ 'tooManyAppointmentsWithSameMail' => [
+ 'errorCode' => 'tooManyAppointmentsWithSameMail',
+ 'errorMessage' => 'Too many appointments with the same email address.',
+ 'statusCode' => self::HTTP_NOT_ACCEPTABLE
+ ],
+ 'officesNotFound' => [
+ 'errorCode' => 'officesNotFound',
+ 'errorMessage' => 'No offices found.',
+ 'statusCode' => self::HTTP_NOT_FOUND
+ ],
+ 'servicesNotFound' => [
+ 'errorCode' => 'servicesNotFound',
+ 'errorMessage' => 'No services found.',
+ 'statusCode' => self::HTTP_NOT_FOUND
+ ],
+ 'scopesNotFound' => [
+ 'errorCode' => 'scopesNotFound',
+ 'errorMessage' => 'No scopes found.',
+ 'statusCode' => self::HTTP_NOT_FOUND
+ ],
+ 'preconfirmationExpired' => [
+ 'errorCode' => 'preconfirmationExpired',
+ 'statusCode' => self::HTTP_BAD_REQUEST,
+ 'errorMessage' => 'The preconfirmation has expired. Please make a new appointment.'
+ ],
+
+ //Middleware exceptions
+ 'ipBlacklisted' => [
+ 'errorCode' => 'IP_BLACKLISTED',
+ 'statusCode' => self::HTTP_FORBIDDEN,
+ 'errorMessage' => 'Access denied - IP address is blacklisted.'
+ ],
+ 'corsOriginNotAllowed' => [
+ 'errorCode' => 'corsOriginNotAllowed',
+ 'statusCode' => self::HTTP_FORBIDDEN,
+ 'errorMessage' => 'Origin not allowed by CORS policy.' // DE: 'Ursprung durch CORS-Richtlinie nicht erlaubt.'
+ ],
+ 'csrfTokenMissing' => [
+ 'errorCode' => 'csrfTokenMissing',
+ 'statusCode' => self::HTTP_FORBIDDEN,
+ 'errorMessage' => 'CSRF token is missing.' // DE: 'CSRF-Token fehlt.'
+ ],
+ 'csrfTokenInvalid' => [
+ 'errorCode' => 'csrfTokenInvalid',
+ 'statusCode' => self::HTTP_FORBIDDEN,
+ 'errorMessage' => 'Invalid CSRF token.' // DE: 'Ungültiger CSRF-Token.'
+ ],
+ 'rateLimitExceeded' => [
+ 'errorCode' => 'rateLimitExceeded',
+ 'statusCode' => self::HTTP_TOO_MANY_REQUESTS,
+ 'errorMessage' => 'Rate limit exceeded. Please try again later.' // DE: 'Anfragelimit überschritten. Bitte versuchen Sie es später erneut.'
+ ],
+ 'requestEntityTooLarge' => [
+ 'errorCode' => 'requestEntityTooLarge',
+ 'statusCode' => self::HTTP_REQUEST_ENTITY_TOO_LARGE,
+ 'errorMessage' => 'Request entity too large.' // DE: 'Anfrage zu groß.'
+ ],
+ 'securityHeaderViolation' => [
+ 'errorCode' => 'securityHeaderViolation',
+ 'statusCode' => self::HTTP_FORBIDDEN,
+ 'errorMessage' => 'Security policy violation.' // DE: 'Verstoß gegen Sicherheitsrichtlinien.'
+ ]
+
+ ];
+
+ // German messages
+ public const DE = [
+ 'notImplemented' => [
+ 'errorCode' => 'notImplemented',
+ 'statusCode' => self::HTTP_NOT_IMPLEMENTED,
+ 'errorMessage' => 'Funktion ist noch nicht implementiert.',
+ ],
+ 'invalidRequest' => [
+ 'errorCode' => 'invalidRequest',
+ 'statusCode' => self::HTTP_BAD_REQUEST,
+ 'errorMessage' => 'Ungültige Anfrage.'
+ ],
+ 'requestMethodNotAllowed' => [
+ 'errorCode' => 'requestMethodNotAllowed',
+ 'statusCode' => self::HTTP_INVALID_REQUEST_METHOD,
+ 'errorMessage' => 'Anfragemethode nicht zulässig.',
+ ],
+ 'captchaVerificationFailed' => [
+ 'errorCode' => 'captchaVerificationFailed',
+ 'statusCode' => self::HTTP_BAD_REQUEST,
+ 'errorMessage' => 'Captcha-Prüfung fehlgeschlagen.'
+ ],
+ 'invalidLocationAndServiceCombination' => [
+ 'errorCode' => 'invalidLocationAndServiceCombination',
+ 'statusCode' => self::HTTP_BAD_REQUEST,
+ 'errorMessage' => 'Die angegebene Dienstleistung ist an diesem Standort nicht verfügbar.'
+ ],
+ 'invalidStartDate' => [
+ 'errorCode' => 'invalidStartDate',
+ 'statusCode' => self::HTTP_BAD_REQUEST,
+ 'errorMessage' => 'startDate ist erforderlich und muss ein gültiges Datum sein.'
+ ],
+ 'invalidEndDate' => [
+ 'errorCode' => 'invalidEndDate',
+ 'statusCode' => self::HTTP_BAD_REQUEST,
+ 'errorMessage' => 'endDate ist erforderlich und muss ein gültiges Datum sein.'
+ ],
+ 'invalidOfficeId' => [
+ 'errorCode' => 'invalidOfficeId',
+ 'statusCode' => self::HTTP_BAD_REQUEST,
+ 'errorMessage' => 'officeId muss eine 32-Bit-Ganzzahl sein.'
+ ],
+ 'invalidServiceId' => [
+ 'errorCode' => 'invalidServiceId',
+ 'statusCode' => self::HTTP_BAD_REQUEST,
+ 'errorMessage' => 'serviceId muss eine 32-Bit-Ganzzahl sein.'
+ ],
+ 'invalidServiceCount' => [
+ 'errorCode' => 'invalidServiceCount',
+ 'statusCode' => self::HTTP_BAD_REQUEST,
+ 'errorMessage' => 'serviceCounts muss ein Array aus numerischen Werten sein.'
+ ],
+ 'emptyServiceArrays' => [
+ 'errorCode' => 'EMPTY_SERVICE_ARRAYS',
+ 'statusCode' => self::HTTP_BAD_REQUEST,
+ 'errorMessage' => 'Service-IDs und Anzahl dürfen nicht leer sein'
+ ],
+ 'mismatchedArrays' => [
+ 'errorCode' => 'MISMATCHED_ARRAYS',
+ 'statusCode' => self::HTTP_BAD_REQUEST,
+ 'errorMessage' => 'Service-IDs und Anzahl müssen gleiche Länge haben'
+ ],
+ 'invalidProcessId' => [
+ 'errorCode' => 'invalidProcessId',
+ 'statusCode' => self::HTTP_BAD_REQUEST,
+ 'errorMessage' => 'processId muss eine positive 32-Bit-Ganzzahl sein.'
+ ],
+ 'invalidScopeId' => [
+ 'errorCode' => 'invalidScopeId',
+ 'statusCode' => self::HTTP_BAD_REQUEST,
+ 'errorMessage' => 'scopeId muss eine positive 32-Bit-Ganzzahl sein.'
+ ],
+ 'invalidAuthKey' => [
+ 'errorCode' => 'invalidAuthKey',
+ 'statusCode' => self::HTTP_BAD_REQUEST,
+ 'errorMessage' => 'authKey muss eine Zeichenkette sein.'
+ ],
+ 'invalidDate' => [
+ 'errorCode' => 'invalidDate',
+ 'statusCode' => self::HTTP_BAD_REQUEST,
+ 'errorMessage' => 'date ist erforderlich und muss ein gültiges Datum sein.'
+ ],
+ 'invalidTimestamp' => [
+ 'errorCode' => 'invalidTimestamp',
+ 'statusCode' => self::HTTP_BAD_REQUEST,
+ 'errorMessage' => 'Fehlender oder ungültiger Zeitstempel. Der Wert muss eine positive Zahl sein.'
+ ],
+ 'invalidFamilyName' => [
+ 'errorCode' => 'invalidFamilyName',
+ 'statusCode' => self::HTTP_BAD_REQUEST,
+ 'errorMessage' => 'familyName muss eine nicht-leere Zeichenkette sein.'
+ ],
+ 'invalidEmail' => [
+ 'errorCode' => 'invalidEmail',
+ 'statusCode' => self::HTTP_BAD_REQUEST,
+ 'errorMessage' => 'email muss eine gültige E-Mail-Adresse sein.'
+ ],
+ 'invalidTelephone' => [
+ 'errorCode' => 'invalidTelephone',
+ 'statusCode' => self::HTTP_BAD_REQUEST,
+ 'errorMessage' => 'telephone muss eine Zahlenkette zwischen 7 und 15 Stellen sein.'
+ ],
+ 'invalidCustomTextfield' => [
+ 'errorCode' => 'invalidCustomTextfield',
+ 'statusCode' => self::HTTP_BAD_REQUEST,
+ 'errorMessage' => 'customTextfield muss eine Zeichenkette sein.'
+ ],
+ 'appointmentNotAvailable' => [
+ 'errorCode' => 'appointmentNotAvailable',
+ 'statusCode' => self::HTTP_NOT_FOUND,
+ 'errorMessage' => 'Der von Ihnen gewählte Termin ist leider nicht mehr verfügbar.'
+ ],
+ 'appointmentCanNotBeCanceled' => [
+ 'errorCode' => 'appointmentCanNotBeCanceled',
+ 'statusCode' => self::HTTP_NOT_ACCEPTABLE,
+ 'errorMessage' => 'Der von Ihnen gewählte Termin ist leider nicht mehr gelöscht werden.'
+ ],
+ 'noAppointmentForThisDay' => [
+ 'errorCode' => 'noAppointmentForThisDay',
+ 'statusCode' => self::HTTP_NOT_FOUND,
+ 'errorMessage' => 'Keine verfügbaren Termine für dieses Datum.'
+ ],
+ 'captchaVerificationError' => [
+ 'errorCode' => 'captchaVerificationError',
+ 'statusCode' => self::HTTP_BAD_REQUEST,
+ 'errorMessage' => 'Bei der Captcha-Prüfung ist ein Fehler aufgetreten.'
+ ],
+ 'serviceUnavailable' => [
+ 'errorCode' => 'serviceUnavailable',
+ 'statusCode' => self::HTTP_UNAVAILABLE,
+ 'errorMessage' => 'Der Dienst ist nicht verfügbar: Die Anwendung wird gerade gewartet.'
+ ],
+
+ //Zmsapi exceptions
+ 'internalError' => [
+ 'errorCode' => 'internalError',
+ 'errorMessage' => 'Ein interner Fehler ist aufgetreten. Bitte versuchen Sie es später erneut.',
+ 'statusCode' => self::HTTP_INTERNAL_SERVER_ERROR
+ ],
+ 'invalidApiClient' => [
+ 'errorCode' => 'invalidApiClient',
+ 'errorMessage' => 'Ungültiger API-Client.',
+ 'statusCode' => self::HTTP_BAD_REQUEST
+ ],
+ 'departmentNotFound' => [
+ 'errorCode' => 'departmentNotFound',
+ 'errorMessage' => 'Abteilung nicht gefunden.',
+ 'statusCode' => self::HTTP_NOT_FOUND
+ ],
+ 'mailNotFound' => [
+ 'errorCode' => 'mailNotFound',
+ 'errorMessage' => 'E-Mail-Vorlage nicht gefunden.',
+ 'statusCode' => self::HTTP_NOT_FOUND
+ ],
+ 'organisationNotFound' => [
+ 'errorCode' => 'organisationNotFound',
+ 'errorMessage' => 'Organisation nicht gefunden.',
+ 'statusCode' => self::HTTP_NOT_FOUND
+ ],
+ 'providerNotFound' => [
+ 'errorCode' => 'providerNotFound',
+ 'errorMessage' => 'Anbieter nicht gefunden.',
+ 'statusCode' => self::HTTP_NOT_FOUND
+ ],
+ 'requestNotFound' => [
+ 'errorCode' => 'requestNotFound',
+ 'errorMessage' => 'Anfrage nicht gefunden.',
+ 'statusCode' => self::HTTP_NOT_FOUND
+ ],
+ 'scopeNotFound' => [
+ 'errorCode' => 'scopeNotFound',
+ 'errorMessage' => 'Bereich nicht gefunden.',
+ 'statusCode' => self::HTTP_NOT_FOUND
+ ],
+ 'processInvalid' => [
+ 'errorCode' => 'processInvalid',
+ 'errorMessage' => 'Die Prozessdaten sind ungültig.',
+ 'statusCode' => self::HTTP_BAD_REQUEST
+ ],
+ 'processAlreadyExists' => [
+ 'errorCode' => 'processAlreadyExists',
+ 'errorMessage' => 'Ein Terminprozess existiert bereits.',
+ 'statusCode' => self::HTTP_CONFLICT
+ ],
+ 'processDeleteFailed' => [
+ 'errorCode' => 'processDeleteFailed',
+ 'errorMessage' => 'Der Termin konnte nicht gelöscht werden.',
+ 'statusCode' => self::HTTP_INTERNAL_SERVER_ERROR
+ ],
+ 'processAlreadyCalled' => [
+ 'errorCode' => 'processAlreadyCalled',
+ 'errorMessage' => 'Der Termin wurde bereits aufgerufen.',
+ 'statusCode' => self::HTTP_CONFLICT
+ ],
+ 'processNotReservedAnymore' => [
+ 'errorCode' => 'processNotReservedAnymore',
+ 'errorMessage' => 'Der Termin ist nicht mehr reserviert.',
+ 'statusCode' => self::HTTP_CONFLICT
+ ],
+ 'processNotPreconfirmedAnymore' => [
+ 'errorCode' => 'processNotPreconfirmedAnymore',
+ 'errorMessage' => 'Der Termin ist nicht mehr vorbestätigt.',
+ 'statusCode' => self::HTTP_CONFLICT
+ ],
+ 'emailIsRequired' => [
+ 'errorCode' => 'emailIsRequired',
+ 'errorMessage' => 'E-Mail-Adresse ist erforderlich.',
+ 'statusCode' => self::HTTP_NOT_ACCEPTABLE
+ ],
+ 'telephoneIsRequired' => [
+ 'errorCode' => 'telephoneIsRequired',
+ 'errorMessage' => 'Telefonnummer ist erforderlich.',
+ 'statusCode' => self::HTTP_NOT_ACCEPTABLE
+ ],
+ 'appointmentNotFound' => [
+ 'errorCode' => 'appointmentNotFound',
+ 'errorMessage' => 'Der angeforderte Termin wurde nicht gefunden.',
+ 'statusCode' => self::HTTP_NOT_FOUND
+ ],
+ 'authKeyMismatch' => [
+ 'errorCode' => 'authKeyMismatch',
+ 'errorMessage' => 'Ungültiger Authentifizierungsschlüssel.',
+ 'statusCode' => self::HTTP_NOT_ACCEPTABLE
+ ],
+ 'noAppointmentsAtLocation' => [
+ 'errorCode' => 'noAppointmentsAtLocation',
+ 'errorMessage' => 'Keine Termine am angegebenen Standort verfügbar.',
+ 'statusCode' => self::HTTP_NOT_FOUND
+ ],
+ 'tooManyAppointmentsWithSameMail' => [
+ 'errorCode' => 'tooManyAppointmentsWithSameMail',
+ 'errorMessage' => 'Zu viele Termine mit derselben E-Mail-Adresse.',
+ 'statusCode' => self::HTTP_NOT_ACCEPTABLE
+ ],
+ 'officesNotFound' => [
+ 'errorCode' => 'officesNotFound',
+ 'errorMessage' => 'Keine Standorte gefunden.',
+ 'statusCode' => self::HTTP_NOT_FOUND
+ ],
+ 'servicesNotFound' => [
+ 'errorCode' => 'servicesNotFound',
+ 'errorMessage' => 'Keine Dienstleistungen gefunden.',
+ 'statusCode' => self::HTTP_NOT_FOUND
+ ],
+ 'scopesNotFound' => [
+ 'errorCode' => 'scopesNotFound',
+ 'errorMessage' => 'Keine Bereiche gefunden.',
+ 'statusCode' => self::HTTP_NOT_FOUND
+ ],
+ 'preconfirmationExpired' => [
+ 'errorCode' => 'preconfirmationExpired',
+ 'statusCode' => self::HTTP_BAD_REQUEST,
+ 'errorMessage' => 'Die Vorbestätigung ist abgelaufen. Bitte vereinbaren Sie einen neuen Termin.'
+ ],
+
+ //Middleware exceptions
+ 'ipBlacklisted' => [
+ 'errorCode' => 'IP_BLACKLISTED',
+ 'statusCode' => self::HTTP_FORBIDDEN,
+ 'errorMessage' => 'Zugriff verweigert - IP-Adresse ist auf der schwarzen Liste.'
+ ],
+ 'corsOriginNotAllowed' => [
+ 'errorCode' => 'corsOriginNotAllowed',
+ 'statusCode' => self::HTTP_FORBIDDEN,
+ 'errorMessage' => 'Ursprung durch CORS-Richtlinie nicht erlaubt.' // DE: 'Ursprung durch CORS-Richtlinie nicht erlaubt.'
+ ],
+ 'csrfTokenMissing' => [
+ 'errorCode' => 'csrfTokenMissing',
+ 'statusCode' => self::HTTP_FORBIDDEN,
+ 'errorMessage' => 'CSRF-Token fehlt.' // DE: 'CSRF-Token fehlt.'
+ ],
+ 'csrfTokenInvalid' => [
+ 'errorCode' => 'csrfTokenInvalid',
+ 'statusCode' => self::HTTP_FORBIDDEN,
+ 'errorMessage' => 'Ungültiger CSRF-Token.' // DE: 'Ungültiger CSRF-Token.'
+ ],
+ 'rateLimitExceeded' => [
+ 'errorCode' => 'rateLimitExceeded',
+ 'statusCode' => self::HTTP_TOO_MANY_REQUESTS,
+ 'errorMessage' => 'Anfragelimit überschritten. Bitte versuchen Sie es später erneut.' // DE: 'Anfragelimit überschritten. Bitte versuchen Sie es später erneut.'
+ ],
+ 'requestEntityTooLarge' => [
+ 'errorCode' => 'requestEntityTooLarge',
+ 'statusCode' => self::HTTP_REQUEST_ENTITY_TOO_LARGE,
+ 'errorMessage' => 'Anfrage zu groß.' // DE: 'Anfrage zu groß.'
+ ],
+ 'securityHeaderViolation' => [
+ 'errorCode' => 'securityHeaderViolation',
+ 'statusCode' => self::HTTP_FORBIDDEN,
+ 'errorMessage' => 'Verstoß gegen Sicherheitsrichtlinien.' // DE: 'Verstoß gegen Sicherheitsrichtlinien.'
+ ]
+
+ ];
+
+ /**
+ * Get an error message by key with fallback logic.
+ *
+ * @param string $key The error message key.
+ * @param string|null $language Optional language (default is centralized DEFAULT_LANGUAGE).
+ * @return array The error message array.
+ */
+ public static function get(string $key, ?string $language = null): array
+ {
+ $language = $language ?? self::DEFAULT_LANGUAGE;
+
+ // Attempt to get messages for the specified language
+ $messages = match ($language) {
+ 'DE' => self::DE,
+ 'EN' => self::EN,
+ default => self::EN,
+ };
+
+ if (isset($messages[$key])) {
+ return $messages[$key];
+ }
+
+ $fallbackMessages = match (self::FALLBACK_LANGUAGE) {
+ 'DE' => self::DE,
+ 'EN' => self::EN,
+ default => self::EN,
+ };
+
+ if (isset($fallbackMessages[$key])) {
+ return $fallbackMessages[$key];
+ }
+
+ $genericErrorMessage = match ($language) {
+ 'DE' => [
+ 'errorCode' => 'unknownError',
+ 'statusCode' => self::HTTP_UNKNOWN,
+ 'errorMessage' => 'Ein unbekannter Fehler ist aufgetreten.'
+ ],
+ default => [
+ 'errorCode' => 'unknownError',
+ 'statusCode' => self::HTTP_UNKNOWN,
+ 'errorMessage' => 'An unknown error occurred.'
+ ],
+ };
+
+ return $genericErrorMessage;
+ }
+
+ /**
+ * Get the highest status code from an array of errors.
+ *
+ * @param array $errors Array of error messages
+ * @return int The highest status code found, or HTTP_OK (200) if no errors
+ * @throws \InvalidArgumentException If any error has an invalid structure
+ */
+ public static function getHighestStatusCode(array $errors): int
+ {
+ if (empty($errors)) {
+ return self::HTTP_OK;
+ }
+
+ $errorCodes = [];
+ foreach ($errors as $error) {
+ if (!is_array($error) || !isset($error['statusCode']) || !is_int($error['statusCode'])) {
+ throw new \InvalidArgumentException('Invalid error structure. Each error must have a statusCode.');
+ }
+ $errorCodes[] = $error['statusCode'];
+ }
+
+ return max($errorCodes);
+ }
+
+}
diff --git a/zmscitizenapi/src/Zmscitizenapi/Middleware/CorsMiddleware.php b/zmscitizenapi/src/Zmscitizenapi/Middleware/CorsMiddleware.php
new file mode 100644
index 000000000..573cd6438
--- /dev/null
+++ b/zmscitizenapi/src/Zmscitizenapi/Middleware/CorsMiddleware.php
@@ -0,0 +1,91 @@
+logger = $logger;
+ $corsEnv = getenv('CORS');
+ if ($corsEnv) {
+ $this->whitelist = array_map('trim', explode(',', $corsEnv));
+ }
+ }
+
+ public function process(
+ ServerRequestInterface $request,
+ RequestHandlerInterface $handler
+ ): ResponseInterface {
+ try {
+ $origin = $request->getHeaderLine('Origin');
+
+ // Allow requests without Origin header (direct browser access)
+ if (empty($origin)) {
+ /*$this->logger->logInfo('Direct browser request - no Origin header', [
+ 'uri' => (string)$request->getUri(),
+ 'headers' => $request->getHeaders()
+ ]);*/
+ return $handler->handle($request);
+ }
+
+ if (!$this->isOriginAllowed($origin)) {
+ $this->logger->logInfo(sprintf(
+ 'CORS blocked - Origin %s not allowed. URI: %s',
+ $origin,
+ $request->getUri()
+ ));
+
+ $response = \App::$slim->getResponseFactory()->createResponse();
+ $response = $response->withStatus(ErrorMessages::get(self::ERROR_CORS)['statusCode'])
+ ->withHeader('Content-Type', 'application/json');
+
+ $response->getBody()->write(json_encode([
+ 'errors' => [ErrorMessages::get(self::ERROR_CORS)]
+ ]));
+
+ return $response;
+ }
+
+ // Handle preflight OPTIONS requests
+ if ($request->getMethod() === 'OPTIONS') {
+ $response = \App::$slim->getResponseFactory()->createResponse(200);
+ return $response
+ ->withHeader('Access-Control-Allow-Origin', $origin)
+ ->withHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS')
+ ->withHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-CSRF-Token')
+ ->withHeader('Access-Control-Allow-Credentials', 'true')
+ ->withHeader('Access-Control-Max-Age', '86400');
+ }
+
+ $response = $handler->handle($request);
+ return $response
+ ->withHeader('Access-Control-Allow-Origin', $origin)
+ ->withHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS')
+ ->withHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-CSRF-Token')
+ ->withHeader('Access-Control-Allow-Credentials', 'true')
+ ->withHeader('Access-Control-Max-Age', '86400');
+ } catch (\Throwable $e) {
+ $this->logger->logError($e, $request);
+ throw $e;
+ }
+ }
+
+ private function isOriginAllowed(string $origin): bool
+ {
+ return in_array($origin, $this->whitelist, true);
+ }
+}
\ No newline at end of file
diff --git a/zmscitizenapi/src/Zmscitizenapi/Middleware/CsrfMiddleware.php b/zmscitizenapi/src/Zmscitizenapi/Middleware/CsrfMiddleware.php
new file mode 100644
index 000000000..1ef8761e9
--- /dev/null
+++ b/zmscitizenapi/src/Zmscitizenapi/Middleware/CsrfMiddleware.php
@@ -0,0 +1,120 @@
+logger = $logger;
+ if (session_status() === PHP_SESSION_NONE) {
+ session_start();
+ }
+ }
+
+ public function process(
+ ServerRequestInterface $request,
+ RequestHandlerInterface $handler
+ ): ResponseInterface {
+ try {
+ if (in_array($request->getMethod(), self::SAFE_METHODS, true)) {
+ $this->ensureTokenExists();
+ return $handler->handle($request);
+ }
+
+ $token = $request->getHeaderLine('X-CSRF-Token');
+ if (empty($token)) {
+ $this->logger->logInfo('CSRF token missing', [
+ 'uri' => (string)$request->getUri()
+ ]);
+
+ $response = \App::$slim->getResponseFactory()->createResponse();
+ $response = $response->withStatus(ErrorMessages::get(self::ERROR_TOKEN_MISSING)['statusCode'])
+ ->withHeader('Content-Type', 'application/json');
+
+ $response->getBody()->write(json_encode([
+ 'errors' => [ErrorMessages::get(self::ERROR_TOKEN_MISSING)]
+ ]));
+
+ return $response;
+ }
+
+ if (!$this->validateToken($token)) {
+ $this->logger->logInfo('Invalid CSRF token', [
+ 'uri' => (string)$request->getUri()
+ ]);
+
+ $response = \App::$slim->getResponseFactory()->createResponse();
+ $response = $response->withStatus(ErrorMessages::get(self::ERROR_TOKEN_INVALID)['statusCode'])
+ ->withHeader('Content-Type', 'application/json');
+
+ $response->getBody()->write(json_encode([
+ 'errors' => [ErrorMessages::get(self::ERROR_TOKEN_INVALID)]
+ ]));
+
+ return $response;
+ }
+
+ return $handler->handle($request);
+ } catch (\Throwable $e) {
+ $this->logger->logError($e, $request);
+ throw $e;
+ }
+ }
+
+ private function validateToken(string $token): bool
+ {
+ if (strlen($token) !== self::TOKEN_LENGTH || !ctype_xdigit($token)) {
+ return false;
+ }
+
+ $storedToken = $this->getStoredToken();
+ if (empty($storedToken)) {
+ return false;
+ }
+
+ return hash_equals($storedToken, $token);
+ }
+
+ private function ensureTokenExists(): void
+ {
+ if (empty($this->getStoredToken())) {
+ $this->generateNewToken();
+ }
+ }
+
+ private function generateNewToken(): string
+ {
+ $token = bin2hex(random_bytes(self::TOKEN_LENGTH / 2));
+ $_SESSION[self::SESSION_TOKEN_KEY] = $token;
+ return $token;
+ }
+
+ private function getStoredToken(): string
+ {
+ return $_SESSION[self::SESSION_TOKEN_KEY] ?? '';
+ }
+
+ public function getToken(): string
+ {
+ $this->ensureTokenExists();
+ return $this->getStoredToken();
+ }
+}
\ No newline at end of file
diff --git a/zmscitizenapi/src/Zmscitizenapi/Middleware/IpFilterMiddleware.php b/zmscitizenapi/src/Zmscitizenapi/Middleware/IpFilterMiddleware.php
new file mode 100644
index 000000000..2c9bb7ac6
--- /dev/null
+++ b/zmscitizenapi/src/Zmscitizenapi/Middleware/IpFilterMiddleware.php
@@ -0,0 +1,149 @@
+logger = $logger;
+ }
+
+ public function process(
+ ServerRequestInterface $request,
+ RequestHandlerInterface $handler
+ ): ResponseInterface {
+ try {
+ $ip = ClientIpHelper::getClientIp();
+ $uri = (string)$request->getUri();
+ if ($ip === null || !filter_var($ip, FILTER_VALIDATE_IP)) {
+ $this->logger->logInfo('Invalid IP address detected', [
+ 'ip' => $ip,
+ 'uri' => $uri
+ ]);
+ return $handler->handle($request);
+ }
+
+ $blacklist = $this->parseIpList(getenv('IP_BLACKLIST') ?: null);
+ if ($this->isIpInList($ip, $blacklist)) {
+ $this->logger->logInfo('Access denied - IP blacklisted', [
+ 'ip' => $ip,
+ 'uri' => $uri
+ ]);
+
+ $response = \App::$slim->getResponseFactory()->createResponse();
+ $error = ErrorMessages::get(self::ERROR_BLACKLISTED);
+ $response = $response->withStatus($error['statusCode'])
+ ->withHeader('Content-Type', 'application/json');
+
+ $response->getBody()->write(json_encode([
+ 'errors' => [$error]
+ ]));
+
+ return $response;
+ }
+
+ /*$this->logger->logInfo('Request processed successfully', [
+ 'uri' => $uri
+ ]);*/
+ return $handler->handle($request);
+
+ } catch (\Throwable $e) {
+ $this->logger->logError($e, $request);
+ throw $e;
+ }
+ }
+
+ private function parseIpList(?string $ipList): array
+ {
+ if (empty($ipList)) {
+ return [];
+ }
+
+ $list = array_map('trim', explode(',', $ipList));
+ return array_filter($list, function ($entry) {
+ if (strpos($entry, '/') !== false) {
+ list($ip, $bits) = explode('/', $entry);
+ return filter_var($ip, FILTER_VALIDATE_IP) &&
+ is_numeric($bits) &&
+ (int)$bits >= 0 &&
+ (int)$bits <= (strpos($ip, ':') !== false ? self::IPV6_BITS : self::IPV4_BITS);
+ }
+ return filter_var($entry, FILTER_VALIDATE_IP);
+ });
+ }
+
+ private function isIpInList(string $ip, array $list): bool
+ {
+ if (empty($list)) {
+ return false;
+ }
+
+ foreach ($list as $range) {
+ if ($this->isIpInRange($ip, $range)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private function isIpInRange(string $ip, string $range): bool
+ {
+ $flags = FILTER_FLAG_IPV4 | FILTER_FLAG_IPV6;
+ if (!filter_var($ip, FILTER_VALIDATE_IP, $flags)) {
+ return false;
+ }
+
+ if (strpos($range, '/') !== false) {
+ list($subnet, $bits) = explode('/', $range);
+
+ if (!filter_var($subnet, FILTER_VALIDATE_IP, $flags)) {
+ return false;
+ }
+
+ $ipBin = @inet_pton($ip);
+ $subnetBin = @inet_pton($subnet);
+
+ if ($ipBin === false || $subnetBin === false ||
+ strlen($ipBin) !== strlen($subnetBin)) {
+ return false;
+ }
+
+ $bits = (int)$bits;
+ $maxBits = strlen($ipBin) === 4 ? self::IPV4_BITS : self::IPV6_BITS;
+
+ if ($bits < 0 || $bits > $maxBits) {
+ return false;
+ }
+
+ $bytes = strlen($ipBin);
+ $mask = str_repeat("\xFF", (int)($bits / 8));
+
+ if ($bits % 8) {
+ $mask .= chr(0xFF << (8 - ($bits % 8)));
+ }
+
+ $mask = str_pad($mask, $bytes, "\x00");
+ return ($ipBin & $mask) === ($subnetBin & $mask);
+ }
+
+ return $ip === $range;
+ }
+}
\ No newline at end of file
diff --git a/zmscitizenapi/src/Zmscitizenapi/Middleware/MaintenanceMiddleware.php b/zmscitizenapi/src/Zmscitizenapi/Middleware/MaintenanceMiddleware.php
new file mode 100644
index 000000000..997d01a8d
--- /dev/null
+++ b/zmscitizenapi/src/Zmscitizenapi/Middleware/MaintenanceMiddleware.php
@@ -0,0 +1,26 @@
+ $errors, 'statusCode' => self::HTTP_UNAVAILABLE];
+ }
+ return $next->handle($request);
+ }
+}
\ No newline at end of file
diff --git a/zmscitizenapi/src/Zmscitizenapi/Middleware/RateLimitingMiddleware.php b/zmscitizenapi/src/Zmscitizenapi/Middleware/RateLimitingMiddleware.php
new file mode 100644
index 000000000..f02a4ff8e
--- /dev/null
+++ b/zmscitizenapi/src/Zmscitizenapi/Middleware/RateLimitingMiddleware.php
@@ -0,0 +1,186 @@
+cache = $cache;
+ $this->logger = $logger;
+ }
+
+ public function process(
+ ServerRequestInterface $request,
+ RequestHandlerInterface $handler
+ ): ResponseInterface {
+ try {
+ $ip = ClientIpHelper::getClientIp();
+ $key = 'rate_limit_' . md5($ip);
+ $lockKey = $key . '_lock';
+
+ // Try to acquire rate limit with retries and exponential backoff
+ $attempt = 0;
+ $limited = false;
+
+ while ($attempt < self::MAX_RETRIES) {
+ try {
+ if ($this->acquireLock($lockKey)) {
+ try {
+ $limited = $this->checkAndIncrementLimit($key);
+ break;
+ } finally {
+ $this->releaseLock($lockKey);
+ }
+ }
+ } catch (\Throwable $e) {
+ $this->logger->logError($e, $request);
+ }
+
+ $attempt++;
+ if ($attempt < self::MAX_RETRIES) {
+ // Exponential backoff with jitter
+ $backoffMs = min(
+ self::BACKOFF_MAX,
+ (int)(self::BACKOFF_MIN * min(pow(2, $attempt), PHP_INT_MAX / self::BACKOFF_MIN))
+ );
+ $jitterMs = random_int(0, (int)($backoffMs * 0.1));
+ $sleepMs = $backoffMs + $jitterMs;
+ usleep($sleepMs * 1000); // Convert to microseconds
+ }
+ }
+
+ if ($limited) {
+ $this->logger->logInfo(sprintf(
+ 'Rate limit exceeded for IP %s. URI: %s',
+ $ip,
+ $request->getUri()
+ ));
+
+ return $this->createRateLimitResponse();
+ }
+
+ $response = $handler->handle($request);
+ // Subtract one extra to account for the current request
+ $remaining = max(0, self::MAX_REQUESTS - $this->getCurrentRequestCount($key) - 1);
+
+ return $response
+ ->withHeader('X-RateLimit-Limit', (string)self::MAX_REQUESTS)
+ ->withHeader('X-RateLimit-Remaining', (string)max(0, $remaining))
+ ->withHeader('X-RateLimit-Reset', (string)$this->getResetTime($key));
+
+ } catch (\Throwable $e) {
+ $this->logger->logError($e, $request);
+ throw $e;
+ }
+ }
+
+ private function checkAndIncrementLimit(string $key): bool
+ {
+ $requestData = $this->cache->get($key);
+
+ if ($requestData === null) {
+ // First request
+ $this->cache->set($key, [
+ 'count' => 1,
+ 'timestamp' => time()
+ ], self::TIME_WINDOW);
+ return false;
+ }
+
+ if (!is_array($requestData)) {
+ // Handle corrupted data
+ $this->cache->delete($key);
+ return false;
+ }
+
+ $count = (int)($requestData['count'] ?? 0);
+
+ if ($count >= self::MAX_REQUESTS) {
+ return true;
+ }
+
+ // Update the counter atomically
+ $requestData['count'] = $count + 1;
+ $requestData['timestamp'] = time();
+ $this->cache->set($key, $requestData, self::TIME_WINDOW);
+
+ return false;
+ }
+
+ private function getCurrentRequestCount(string $key): int
+ {
+ $requestData = $this->cache->get($key);
+ if (!is_array($requestData)) {
+ return 0;
+ }
+ return (int)($requestData['count'] ?? 0);
+ }
+
+ private function getResetTime(string $key): int
+ {
+ $requestData = $this->cache->get($key);
+ if (!is_array($requestData)) {
+ return time() + self::TIME_WINDOW;
+ }
+ return (int)($requestData['timestamp'] ?? time()) + self::TIME_WINDOW;
+ }
+
+ private function createRateLimitResponse(): ResponseInterface
+ {
+ $response = \App::$slim->getResponseFactory()->createResponse();
+ $response = $response->withStatus(ErrorMessages::get(self::ERROR_RATE_LIMIT)['statusCode'])
+ ->withHeader('Content-Type', 'application/json');
+
+ $response->getBody()->write(json_encode([
+ 'errors' => [ErrorMessages::get(self::ERROR_RATE_LIMIT)]
+ ]));
+
+ return $response;
+ }
+
+ private function acquireLock(string $lockKey): bool
+ {
+ // Try to acquire lock by setting it only if it doesn't exist
+ if (!$this->cache->has($lockKey)) {
+ return $this->cache->set($lockKey, true, self::LOCK_TIMEOUT);
+ }
+ return false;
+ }
+
+ private function releaseLock(string $lockKey): void
+ {
+ $this->cache->delete($lockKey);
+ }
+
+ /**
+ * For testing purposes - allows checking if a lock exists
+ */
+ public function isLocked(string $ip): bool
+ {
+ $lockKey = 'rate_limit_' . md5($ip) . '_lock';
+ return $this->cache->has($lockKey);
+ }
+}
\ No newline at end of file
diff --git a/zmscitizenapi/src/Zmscitizenapi/Middleware/RequestLoggingMiddleware.php b/zmscitizenapi/src/Zmscitizenapi/Middleware/RequestLoggingMiddleware.php
new file mode 100644
index 000000000..404a7b42d
--- /dev/null
+++ b/zmscitizenapi/src/Zmscitizenapi/Middleware/RequestLoggingMiddleware.php
@@ -0,0 +1,54 @@
+logger = $logger;
+ }
+
+ /**
+ * Process an incoming server request and log its details
+ *
+ * @param ServerRequestInterface $request The request to process
+ * @param RequestHandlerInterface $handler The handler to process the request
+ * @return ResponseInterface The resulting response
+ * @throws \Throwable If an error occurs during request handling
+ */
+ public function process(
+ ServerRequestInterface $request,
+ RequestHandlerInterface $handler
+ ): ResponseInterface {
+ try {
+ $response = $handler->handle($request);
+
+ $responseToLog = clone $response;
+ $responseToLog->getBody()->rewind();
+
+ $this->logger->logRequest($request, $responseToLog);
+
+ return $response;
+ } catch (\Throwable $e) {
+ $this->logger->logError($e, $request);
+ throw $e;
+ }
+ }
+}
\ No newline at end of file
diff --git a/zmscitizenapi/src/Zmscitizenapi/Middleware/RequestSanitizerMiddleware.php b/zmscitizenapi/src/Zmscitizenapi/Middleware/RequestSanitizerMiddleware.php
new file mode 100644
index 000000000..8e2fb3813
--- /dev/null
+++ b/zmscitizenapi/src/Zmscitizenapi/Middleware/RequestSanitizerMiddleware.php
@@ -0,0 +1,125 @@
+logger = $logger;
+ }
+
+ public function process(
+ ServerRequestInterface $request,
+ RequestHandlerInterface $handler
+ ): ResponseInterface {
+ try {
+ $request = $this->sanitizeRequest($request);
+
+ /*$this->logger->logInfo('Request sanitized', [
+ 'uri' => (string) $request->getUri()
+ ]);*/
+
+ return $handler->handle($request);
+ } catch (\Throwable $e) {
+ $this->logger->logError($e, $request);
+ throw $e;
+ }
+ }
+
+ private function sanitizeRequest(ServerRequestInterface $request): ServerRequestInterface
+ {
+ // Sanitize query parameters
+ $queryParams = $request->getQueryParams();
+ $sanitizedQueryParams = $this->sanitizeData($queryParams);
+ $request = $request->withQueryParams($sanitizedQueryParams);
+
+ // Sanitize parsed body
+ $parsedBody = $request->getParsedBody();
+ if (is_array($parsedBody)) {
+ $sanitizedParsedBody = $this->sanitizeData($parsedBody);
+ $request = $request->withParsedBody($sanitizedParsedBody);
+ } elseif (is_object($parsedBody)) {
+ $sanitizedParsedBody = $this->sanitizeObject($parsedBody);
+ $request = $request->withParsedBody($sanitizedParsedBody);
+ }
+
+ return $request;
+ }
+
+ private function sanitizeData(array $data): array
+ {
+ return $this->sanitizeDataWithDepth($data, 0);
+ }
+
+ private function sanitizeDataWithDepth(array $data, int $depth): array
+ {
+ if ($depth >= self::MAX_RECURSION_DEPTH) {
+ throw new \RuntimeException('Maximum recursion depth exceeded');
+ }
+
+ $sanitized = [];
+ foreach ($data as $key => $value) {
+ if (is_array($value)) {
+ $sanitized[$key] = $this->sanitizeDataWithDepth($value, $depth + 1);
+ } elseif (is_string($value)) {
+ $sanitized[$key] = $this->sanitizeString($value);
+ } else {
+ $sanitized[$key] = $value;
+ }
+ }
+ return $sanitized;
+ }
+
+ private function sanitizeObject(object $data): object
+ {
+ return $this->sanitizeObjectWithDepth($data, 0);
+ }
+
+ private function sanitizeObjectWithDepth(object $data, int $depth): object
+ {
+ if ($depth >= self::MAX_RECURSION_DEPTH) {
+ throw new \RuntimeException('Maximum recursion depth exceeded');
+ }
+
+ foreach ($data as $key => $value) {
+ if (is_array($value)) {
+ $data->$key = $this->sanitizeDataWithDepth($value, $depth + 1);
+ } elseif (is_object($value)) {
+ $data->$key = $this->sanitizeObjectWithDepth($value, $depth + 1);
+ } elseif (is_string($value)) {
+ $data->$key = $this->sanitizeString($value);
+ }
+ }
+ return $data;
+ }
+
+ private function sanitizeString(string $value): string
+ {
+ if (strlen($value) > self::MAX_STRING_LENGTH) {
+ throw new \RuntimeException('String exceeds maximum length');
+ }
+
+ $value = preg_replace('/[\x00-\x1F\x7F]/u', '', $value);
+
+ $value = trim($value);
+ if (!mb_check_encoding($value, 'UTF-8')) {
+ $this->logger->logWarning('Invalid string encoding detected.', ['value' => $value]);
+ $value = mb_convert_encoding($value, 'UTF-8', 'auto');
+ }
+ return $value;
+ }
+}
\ No newline at end of file
diff --git a/zmscitizenapi/src/Zmscitizenapi/Middleware/RequestSizeLimitMiddleware.php b/zmscitizenapi/src/Zmscitizenapi/Middleware/RequestSizeLimitMiddleware.php
new file mode 100644
index 000000000..f50761836
--- /dev/null
+++ b/zmscitizenapi/src/Zmscitizenapi/Middleware/RequestSizeLimitMiddleware.php
@@ -0,0 +1,61 @@
+logger = $logger;
+ }
+
+ public function process(
+ ServerRequestInterface $request,
+ RequestHandlerInterface $handler
+ ): ResponseInterface {
+ try {
+ $contentLength = $request->getHeaderLine('Content-Length');
+ if ($contentLength === '') {
+ return $handler->handle($request);
+ }
+ $contentLength = (int)$contentLength;
+
+ if ($contentLength > self::MAX_SIZE) {
+ $this->logger->logInfo(sprintf(
+ 'Request too large: %d bytes. URI: %s',
+ $contentLength,
+ $request->getUri()
+ ));
+
+ $response = \App::$slim->getResponseFactory()->createResponse();
+ $response = $response->withStatus(ErrorMessages::get(self::ERROR_TOO_LARGE)['statusCode'])
+ ->withHeader('Content-Type', 'application/json');
+
+ $response->getBody()->write(json_encode([
+ 'errors' => [ErrorMessages::get(self::ERROR_TOO_LARGE)]
+ ]));
+
+ return $response;
+ }
+
+ return $handler->handle($request);
+ } catch (\Throwable $e) {
+ $this->logger->logError($e, $request);
+ throw $e;
+ }
+ }
+}
\ No newline at end of file
diff --git a/zmscitizenapi/src/Zmscitizenapi/Middleware/SecurityHeadersMiddleware.php b/zmscitizenapi/src/Zmscitizenapi/Middleware/SecurityHeadersMiddleware.php
new file mode 100644
index 000000000..bcfa39eb0
--- /dev/null
+++ b/zmscitizenapi/src/Zmscitizenapi/Middleware/SecurityHeadersMiddleware.php
@@ -0,0 +1,65 @@
+ 'DENY',
+ 'X-Content-Type-Options' => 'nosniff',
+ 'X-XSS-Protection' => '1; mode=block',
+ 'Strict-Transport-Security' => 'max-age=31536000; includeSubDomains',
+ 'Content-Security-Policy' => "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; connect-src 'self'",
+ 'Referrer-Policy' => 'strict-origin-when-cross-origin',
+ 'Permissions-Policy' => 'geolocation=(), microphone=(), camera=()',
+ 'X-Permitted-Cross-Domain-Policies' => 'none'
+ ];
+
+ private LoggerService $logger;
+
+ public function __construct(LoggerService $logger)
+ {
+ $this->logger = $logger;
+ }
+
+ public function process(
+ ServerRequestInterface $request,
+ RequestHandlerInterface $handler
+ ): ResponseInterface {
+ try {
+ $response = $handler->handle($request);
+
+ foreach ($this->securityHeaders as $header => $value) {
+ $response = $response->withHeader($header, $value);
+ }
+
+ /*$this->logger->logInfo('Security headers added', [
+ 'uri' => (string)$request->getUri()
+ ]);*/
+
+ return $response;
+ } catch (\Throwable $e) {
+ $this->logger->logError($e, $request);
+
+ $response = \App::$slim->getResponseFactory()->createResponse();
+ $response = $response->withStatus(ErrorMessages::get('securityHeaderViolation')['statusCode'])
+ ->withHeader('Content-Type', 'application/json');
+
+ $response->getBody()->write(json_encode([
+ 'errors' => [ErrorMessages::get('securityHeaderViolation')]
+ ]));
+
+ return $response;
+ }
+ }
+}
\ No newline at end of file
diff --git a/zmscitizenapi/src/Zmscitizenapi/Models/AvailableAppointments.php b/zmscitizenapi/src/Zmscitizenapi/Models/AvailableAppointments.php
new file mode 100644
index 000000000..eff4654cc
--- /dev/null
+++ b/zmscitizenapi/src/Zmscitizenapi/Models/AvailableAppointments.php
@@ -0,0 +1,47 @@
+appointmentTimestamps = array_map('intval', $appointmentTimestamps);
+
+ $this->ensureValid();
+ }
+
+ private function ensureValid()
+ {
+ if (!$this->testValid()) {
+ throw new InvalidArgumentException("The provided data is invalid according to the schema.");
+ }
+ }
+
+ /**
+ * Converts the model data back into an array for serialization.
+ *
+ * @return array
+ */
+ public function toArray(): array
+ {
+ return [
+ 'appointmentTimestamps' => $this->appointmentTimestamps,
+ ];
+ }
+
+ public function jsonSerialize(): mixed
+ {
+ return $this->toArray();
+ }
+}
diff --git a/zmscitizenapi/src/Zmscitizenapi/Models/AvailableDays.php b/zmscitizenapi/src/Zmscitizenapi/Models/AvailableDays.php
new file mode 100644
index 000000000..fa0877dbb
--- /dev/null
+++ b/zmscitizenapi/src/Zmscitizenapi/Models/AvailableDays.php
@@ -0,0 +1,47 @@
+availableDays = $availableDays;
+
+ $this->ensureValid();
+ }
+
+ private function ensureValid()
+ {
+ if (!$this->testValid()) {
+ throw new InvalidArgumentException("The provided data is invalid according to the schema.");
+ }
+ }
+
+ /**
+ * Converts the model data back into an array for serialization.
+ *
+ * @return array
+ */
+ public function toArray(): array
+ {
+ return [
+ 'availableDays' => $this->availableDays,
+ ];
+ }
+
+ public function jsonSerialize(): mixed
+ {
+ return $this->toArray();
+ }
+}
diff --git a/zmscitizenapi/src/Zmscitizenapi/Models/Captcha/AltchaCaptcha.php b/zmscitizenapi/src/Zmscitizenapi/Models/Captcha/AltchaCaptcha.php
new file mode 100644
index 000000000..0c0f5afd9
--- /dev/null
+++ b/zmscitizenapi/src/Zmscitizenapi/Models/Captcha/AltchaCaptcha.php
@@ -0,0 +1,93 @@
+service = 'AltchaCaptcha';
+ $this->siteKey = \App::$ALTCHA_CAPTCHA_SITE_KEY;
+ $this->apiUrl = \App::$ALTCHA_CAPTCHA_ENDPOINT;
+ $this->secretKey = \App::$ALTCHA_CAPTCHA_SECRET_KEY;
+ $this->puzzle = \App::$ALTCHA_CAPTCHA_ENDPOINT_PUZZLE;
+
+ $this->ensureValid();
+ }
+
+ private function ensureValid()
+ {
+ if (!$this->testValid()) {
+ throw new \InvalidArgumentException("The provided data is invalid according to the schema.");
+ }
+ }
+
+ /**
+ * Gibt die Captcha-Konfigurationsdetails zurück.
+ *
+ * @return array
+ */
+ public function getCaptchaDetails(): array
+ {
+ return [
+ 'siteKey' => $this->siteKey,
+ 'captchaEndpoint' => $this->apiUrl,
+ 'puzzle' => $this->puzzle,
+ 'captchaEnabled' => \App::$CAPTCHA_ENABLED
+ ];
+ }
+
+ /**
+ * Überprüft die Captcha-Lösung.
+ *
+ * @param string $solution
+ * @return bool
+ * @throws \Exception
+ */
+ public function verifyCaptcha(string $solution): bool
+ {
+ try {
+ $response = \App::$http->post($this->apiUrl, [
+ 'form_params' => [
+ 'secret' => $this->secretKey,
+ 'solution' => $solution
+ ]
+ ]);
+
+ $responseBody = json_decode((string)$response->getBody(), true);
+
+ if (json_last_error() !== JSON_ERROR_NONE || !isset($responseBody['valid'])) {
+ return false;
+ }
+
+ return $responseBody['valid'] === true;
+ } catch (RequestException $e) {
+ return false;
+ }
+ }
+}
\ No newline at end of file
diff --git a/zmscitizenapi/src/Zmscitizenapi/Models/Captcha/FriendlyCaptcha.php b/zmscitizenapi/src/Zmscitizenapi/Models/Captcha/FriendlyCaptcha.php
new file mode 100644
index 000000000..9a91570be
--- /dev/null
+++ b/zmscitizenapi/src/Zmscitizenapi/Models/Captcha/FriendlyCaptcha.php
@@ -0,0 +1,93 @@
+service = 'FriendlyCaptcha';
+ $this->siteKey = \App::$FRIENDLY_CAPTCHA_SITE_KEY;
+ $this->apiUrl = \App::$FRIENDLY_CAPTCHA_ENDPOINT;
+ $this->secretKey = \App::$FRIENDLY_CAPTCHA_SECRET_KEY;
+ $this->puzzle = \App::$FRIENDLY_CAPTCHA_ENDPOINT_PUZZLE;
+
+ $this->ensureValid();
+ }
+
+ private function ensureValid()
+ {
+ if (!$this->testValid()) {
+ throw new \InvalidArgumentException("The provided data is invalid according to the schema.");
+ }
+ }
+
+ /**
+ * Gibt die Captcha-Konfigurationsdetails zurück.
+ *
+ * @return array
+ */
+ public function getCaptchaDetails(): array
+ {
+ return [
+ 'siteKey' => $this->siteKey,
+ 'captchaEndpoint' => $this->apiUrl,
+ 'puzzle' => $this->puzzle,
+ 'captchaEnabled' => \App::$CAPTCHA_ENABLED
+ ];
+ }
+
+ /**
+ * Überprüft die Captcha-Lösung.
+ *
+ * @param string $solution
+ * @return bool
+ * @throws \Exception
+ */
+ public function verifyCaptcha(string $solution): bool
+ {
+ try {
+ $response = \App::$http->post($this->apiUrl, [
+ 'form_params' => [
+ 'secret' => $this->secretKey,
+ 'solution' => $solution
+ ]
+ ]);
+
+ $responseBody = json_decode((string)$response->getBody(), true);
+
+ if (json_last_error() !== JSON_ERROR_NONE || !isset($responseBody['success'])) {
+ return false;
+ }
+
+ return $responseBody['success'] === true;
+ } catch (RequestException $e) {
+ return false;
+ }
+ }
+}
\ No newline at end of file
diff --git a/zmscitizenapi/src/Zmscitizenapi/Models/CaptchaInterface.php b/zmscitizenapi/src/Zmscitizenapi/Models/CaptchaInterface.php
new file mode 100644
index 000000000..3a6a17268
--- /dev/null
+++ b/zmscitizenapi/src/Zmscitizenapi/Models/CaptchaInterface.php
@@ -0,0 +1,23 @@
+offices[] = $office;
+ } catch (\Exception $e) {
+ error_log("Invalid Office encountered: " . $e->getMessage()); //Gracefully handle
+ }
+ }
+
+ $this->ensureValid();
+ }
+
+ private function ensureValid()
+ {
+ if (!$this->testValid()) {
+ throw new InvalidArgumentException("The provided data is invalid according to the schema.");
+ }
+ }
+
+ public function toArray(): array
+ {
+ return [
+ 'offices' => array_map(fn(Office $office) => $office->toArray(), $this->offices),
+ ];
+ }
+
+ public function jsonSerialize(): mixed
+ {
+ return $this->toArray();
+ }
+}
diff --git a/zmscitizenapi/src/Zmscitizenapi/Models/Collections/OfficeServiceAndRelationList.php b/zmscitizenapi/src/Zmscitizenapi/Models/Collections/OfficeServiceAndRelationList.php
new file mode 100644
index 000000000..2c78f21f5
--- /dev/null
+++ b/zmscitizenapi/src/Zmscitizenapi/Models/Collections/OfficeServiceAndRelationList.php
@@ -0,0 +1,55 @@
+offices = $offices;
+ $this->services = $services;
+ $this->relations = $relations;
+
+ $this->ensureValid();
+ }
+
+ public function toArray(): array
+ {
+ return [
+ 'offices' => $this->offices->toArray()['offices'],
+ 'services' => $this->services->toArray()['services'],
+ 'relations' => $this->relations->toArray()['relations']
+ ];
+ }
+
+ private function ensureValid()
+ {
+ if (!$this->testValid()) {
+ throw new InvalidArgumentException("The provided data is invalid according to the schema.");
+ }
+ }
+
+ public function jsonSerialize(): mixed
+ {
+ return $this->toArray();
+ }
+}
diff --git a/zmscitizenapi/src/Zmscitizenapi/Models/Collections/OfficeServiceRelationList.php b/zmscitizenapi/src/Zmscitizenapi/Models/Collections/OfficeServiceRelationList.php
new file mode 100644
index 000000000..520e83203
--- /dev/null
+++ b/zmscitizenapi/src/Zmscitizenapi/Models/Collections/OfficeServiceRelationList.php
@@ -0,0 +1,53 @@
+relations[] = $relation;
+ } catch (\Exception $e) {
+ error_log("Invalid OfficeServiceRelation encountered: " . $e->getMessage()); //Gracefully handle
+ }
+ }
+
+ $this->ensureValid();
+ }
+
+ private function ensureValid()
+ {
+ if (!$this->testValid()) {
+ throw new InvalidArgumentException("The provided data is invalid according to the schema.");
+ }
+ }
+
+ public function toArray(): array
+ {
+ return [
+ 'relations' => array_map(fn(OfficeServiceRelation $relation) => $relation->toArray(), $this->relations),
+ ];
+ }
+
+ public function jsonSerialize(): mixed
+ {
+ return $this->toArray();
+ }
+}
diff --git a/zmscitizenapi/src/Zmscitizenapi/Models/Collections/ServiceList.php b/zmscitizenapi/src/Zmscitizenapi/Models/Collections/ServiceList.php
new file mode 100644
index 000000000..0e854d787
--- /dev/null
+++ b/zmscitizenapi/src/Zmscitizenapi/Models/Collections/ServiceList.php
@@ -0,0 +1,58 @@
+services[] = $service;
+ } catch (\Exception $e) {
+ error_log("Invalid Service encountered: " . $e->getMessage()); //Gracefully handle
+ }
+
+ }
+
+ $this->ensureValid();
+ }
+
+ private function ensureValid()
+ {
+ if (!$this->testValid()) {
+ throw new InvalidArgumentException("The provided data is invalid according to the schema.");
+ }
+ }
+
+ /**
+ * Converts the service list to an array for serialization.
+ *
+ * @return array
+ */
+ public function toArray(): array
+ {
+ return [
+ "services" => array_map(fn(Service $service) => $service->toArray(), $this->services)
+ ];
+ }
+
+ public function jsonSerialize(): mixed
+ {
+ return $this->toArray();
+ }
+}
diff --git a/zmscitizenapi/src/Zmscitizenapi/Models/Collections/ThinnedScopeList.php b/zmscitizenapi/src/Zmscitizenapi/Models/Collections/ThinnedScopeList.php
new file mode 100644
index 000000000..949e0e579
--- /dev/null
+++ b/zmscitizenapi/src/Zmscitizenapi/Models/Collections/ThinnedScopeList.php
@@ -0,0 +1,58 @@
+scopes[] = $scope;
+ } catch (\Exception $e) {
+ error_log("Invalid ThinnedScope encountered: " . $e->getMessage()); //Gracefully handle
+ }
+ }
+
+ $this->ensureValid();
+ }
+
+ private function ensureValid()
+ {
+ if (!$this->testValid()) {
+ throw new InvalidArgumentException("The provided data is invalid according to the schema.");
+ }
+ }
+
+ public function toArray(): array
+ {
+ return [
+ 'scopes' => array_map(fn(ThinnedScope $scope) => $scope->toArray(), $this->scopes),
+ ];
+ }
+
+ public function jsonSerialize(): mixed
+ {
+ return $this->toArray();
+ }
+
+ public function getScopes(): array
+ {
+ return $this->scopes;
+ }
+
+}
diff --git a/zmscitizenapi/src/Zmscitizenapi/Models/Combinable.php b/zmscitizenapi/src/Zmscitizenapi/Models/Combinable.php
new file mode 100644
index 000000000..223fdf094
--- /dev/null
+++ b/zmscitizenapi/src/Zmscitizenapi/Models/Combinable.php
@@ -0,0 +1,63 @@
+ */
+ private array $combinations = [];
+
+ /**
+ * Constructor.
+ *
+ * @param array $combinations An associative array of combinations (serviceId => providerIds).
+ */
+ public function __construct(array $combinations = [])
+ {
+ foreach ($combinations as $id => $providerIds) {
+
+ $this->combinations[(int)$id] = array_map('intval', $providerIds);
+
+ $this->ensureValid();
+ }
+ }
+
+ private function ensureValid()
+ {
+ if (!$this->testValid()) {
+ throw new InvalidArgumentException("The provided data is invalid according to the schema.");
+ }
+ }
+
+ /**
+ * Get the combinations array.
+ *
+ * @return array The combinations as an associative array of integers.
+ */
+ public function getCombinations(): array
+ {
+ return $this->combinations;
+ }
+
+ /**
+ * Converts the model data back into an array for serialization.
+ *
+ * @return array The combinations as an associative array of integers.
+ */
+ public function toArray(): array
+ {
+ return $this->combinations;
+ }
+
+ public function jsonSerialize(): mixed
+ {
+ return $this->toArray();
+ }
+}
diff --git a/zmscitizenapi/src/Zmscitizenapi/Models/Office.php b/zmscitizenapi/src/Zmscitizenapi/Models/Office.php
new file mode 100644
index 000000000..8290febc6
--- /dev/null
+++ b/zmscitizenapi/src/Zmscitizenapi/Models/Office.php
@@ -0,0 +1,77 @@
+id = $id;
+ $this->name = $name;
+ $this->address = $address;
+ $this->geo = $geo;
+ $this->scope = $scope;
+
+ $this->ensureValid();
+ }
+
+ private function ensureValid()
+ {
+ if (!$this->testValid()) {
+ throw new InvalidArgumentException("The provided data is invalid according to the schema.");
+ }
+ }
+
+ /**
+ * Converts the model data back into an array for serialization.
+ *
+ * @return array
+ */
+ public function toArray(): array
+ {
+ return [
+ 'id' => $this->id,
+ 'name' => $this->name,
+ 'address' => $this->address,
+ 'geo' => $this->geo,
+ 'scope' => $this->scope?->toArray(),
+ ];
+ }
+
+ public function jsonSerialize(): mixed
+ {
+ return $this->toArray();
+ }
+}
diff --git a/zmscitizenapi/src/Zmscitizenapi/Models/OfficeServiceRelation.php b/zmscitizenapi/src/Zmscitizenapi/Models/OfficeServiceRelation.php
new file mode 100644
index 000000000..a43b02e4c
--- /dev/null
+++ b/zmscitizenapi/src/Zmscitizenapi/Models/OfficeServiceRelation.php
@@ -0,0 +1,69 @@
+officeId = $officeId;
+ $this->serviceId = $serviceId;
+ $this->slots = $slots;
+
+ $this->ensureValid();
+ }
+
+ private function ensureValid()
+ {
+ if (!$this->testValid()) {
+ throw new InvalidArgumentException("The provided data is invalid according to the schema.");
+ }
+ }
+
+ /**
+ * Converts the model data back into an array for serialization.
+ *
+ * @return array
+ */
+ public function toArray(): array
+ {
+ return [
+ 'officeId' => $this->officeId,
+ 'serviceId' => $this->serviceId,
+ 'slots' => $this->slots,
+ ];
+ }
+
+ /**
+ * Implements JSON serialization.
+ *
+ * @return mixed
+ */
+ public function jsonSerialize(): mixed
+ {
+ return $this->toArray();
+ }
+}
diff --git a/zmscitizenapi/src/Zmscitizenapi/Models/ProcessFreeSlots.php b/zmscitizenapi/src/Zmscitizenapi/Models/ProcessFreeSlots.php
new file mode 100644
index 000000000..0095d13c4
--- /dev/null
+++ b/zmscitizenapi/src/Zmscitizenapi/Models/ProcessFreeSlots.php
@@ -0,0 +1,55 @@
+appointmentTimestamps = array_map('intval', $appointmentTimestamps);
+
+ $this->ensureValid();
+ }
+
+ private function ensureValid(): void
+ {
+ if (!$this->testValid()) {
+ throw new InvalidArgumentException('The provided data is invalid according to the schema.');
+ }
+ }
+
+ /**
+ * Converts the model data back into an array for serialization.
+ *
+ * @return array
+ */
+ public function toArray(): array
+ {
+ return [
+ 'appointmentTimestamps' => $this->appointmentTimestamps,
+ ];
+ }
+
+ /**
+ * Implementation of JsonSerializable.
+ */
+ public function jsonSerialize(): mixed
+ {
+ return $this->toArray();
+ }
+}
\ No newline at end of file
diff --git a/zmscitizenapi/src/Zmscitizenapi/Models/Service.php b/zmscitizenapi/src/Zmscitizenapi/Models/Service.php
new file mode 100644
index 000000000..dd2b550a0
--- /dev/null
+++ b/zmscitizenapi/src/Zmscitizenapi/Models/Service.php
@@ -0,0 +1,76 @@
+id = $id;
+ $this->name = $name;
+ $this->maxQuantity = $maxQuantity;
+ $this->combinable = $combinable;
+
+ $this->ensureValid();
+ }
+
+ private function ensureValid()
+ {
+ if (!$this->testValid()) {
+ throw new InvalidArgumentException("The provided data is invalid according to the schema.");
+ }
+ }
+
+ /**
+ * Converts the model data back into an array for serialization.
+ *
+ * @return array
+ */
+ public function toArray(): array
+ {
+ return [
+ 'id' => $this->id,
+ 'name' => $this->name,
+ 'maxQuantity' => $this->maxQuantity,
+ 'combinable' => $this->combinable
+ ];
+ }
+
+ public function jsonSerialize(): mixed
+ {
+ return $this->toArray();
+ }
+}
\ No newline at end of file
diff --git a/zmscitizenapi/src/Zmscitizenapi/Models/ThinnedContact.php b/zmscitizenapi/src/Zmscitizenapi/Models/ThinnedContact.php
new file mode 100644
index 000000000..a5c8cfa43
--- /dev/null
+++ b/zmscitizenapi/src/Zmscitizenapi/Models/ThinnedContact.php
@@ -0,0 +1,70 @@
+city = $city ?? '';
+ $this->country = $country ?? '';
+ $this->name = $name ?? '';
+ $this->postalCode = $postalCode ?? '';
+ $this->region = $region ?? '';
+ $this->street = $street ?? '';
+ $this->streetNumber = $streetNumber ?? '';
+
+ $this->ensureValid();
+ }
+
+ /**
+ * Validates the model against the JSON schema.
+ *
+ * @throws InvalidArgumentException if validation fails.
+ */
+ private function ensureValid(): void
+ {
+ // testValid() is inherited from Entity; it checks $this against self::$schema.
+ $this->testValid();
+ }
+
+ public function toArray(): array
+ {
+ return [
+ 'city' => $this->city,
+ 'country' => $this->country,
+ 'name' => $this->name,
+ 'postalCode' => $this->postalCode,
+ 'region' => $this->region,
+ 'street' => $this->street,
+ 'streetNumber' => $this->streetNumber,
+ ];
+ }
+
+ public function jsonSerialize(): mixed
+ {
+ return $this->toArray();
+ }
+}
\ No newline at end of file
diff --git a/zmscitizenapi/src/Zmscitizenapi/Models/ThinnedProcess.php b/zmscitizenapi/src/Zmscitizenapi/Models/ThinnedProcess.php
new file mode 100644
index 000000000..2ce6d560c
--- /dev/null
+++ b/zmscitizenapi/src/Zmscitizenapi/Models/ThinnedProcess.php
@@ -0,0 +1,127 @@
+processId = $processId;
+ $this->timestamp = $timestamp;
+ $this->authKey = $authKey;
+ $this->familyName = $familyName;
+ $this->customTextfield = $customTextfield;
+ $this->email = $email;
+ $this->telephone = $telephone;
+ $this->officeName = $officeName;
+ $this->officeId = $officeId;
+ $this->scope = $scope;
+ $this->subRequestCounts = $subRequestCounts;
+ $this->serviceId = $serviceId;
+ $this->serviceCount = $serviceCount;
+ $this->status = $status;
+
+ $this->ensureValid();
+ }
+
+ /**
+ * Convert the ThinnedProcess object to an array.
+ *
+ * @return array
+ */
+ public function toArray(): array
+ {
+ return [
+ 'processId' => $this->processId ?? null,
+ 'timestamp' => $this->timestamp ?? null,
+ 'authKey' => $this->authKey ?? null,
+ 'familyName' => $this->familyName ?? null,
+ 'customTextfield' => $this->customTextfield ?? null,
+ 'email' => $this->email ?? null,
+ 'telephone' => $this->telephone ?? null,
+ 'officeName' => $this->officeName ?? null,
+ 'officeId' => $this->officeId ?? null,
+ 'scope' => $this->scope ?? null,
+ 'subRequestCounts' => $this->subRequestCounts,
+ 'serviceId' => $this->serviceId ?? null,
+ 'serviceCount' => $this->serviceCount,
+ 'status' => $this->status ?? null,
+ ];
+ }
+
+ private function ensureValid()
+ {
+ if (!$this->testValid()) {
+ throw new InvalidArgumentException("The provided data is invalid according to the schema.");
+ }
+ }
+
+ public function jsonSerialize(): mixed
+ {
+ return $this->toArray();
+ }
+}
\ No newline at end of file
diff --git a/zmscitizenapi/src/Zmscitizenapi/Models/ThinnedProvider.php b/zmscitizenapi/src/Zmscitizenapi/Models/ThinnedProvider.php
new file mode 100644
index 000000000..506724f50
--- /dev/null
+++ b/zmscitizenapi/src/Zmscitizenapi/Models/ThinnedProvider.php
@@ -0,0 +1,78 @@
+id = $id;
+ $this->name = $name;
+ $this->lat = $lat;
+ $this->lon = $lon;
+ $this->source = $source;
+ $this->contact = $contact;
+
+ $this->ensureValid();
+ }
+
+ private function ensureValid()
+ {
+ if (!$this->testValid()) {
+ throw new InvalidArgumentException("The provided data is invalid according to the schema.");
+ }
+ }
+
+ /**
+ * Convert the ThinnedProvider object to an array.
+ *
+ * @return array
+ */
+ public function toArray(): array
+ {
+ return [
+ 'id' => $this->id ?? null,
+ 'name' => $this->name ?? null,
+ 'lat' => $this->lat ?? null,
+ 'lon' => $this->lon ?? null,
+ 'source' => $this->source ?? null,
+ 'contact' => $this->contact ?? null,
+ ];
+ }
+
+ public function jsonSerialize(): mixed
+ {
+ return $this->toArray();
+ }
+}
diff --git a/zmscitizenapi/src/Zmscitizenapi/Models/ThinnedScope.php b/zmscitizenapi/src/Zmscitizenapi/Models/ThinnedScope.php
new file mode 100644
index 000000000..b8ac1c7ec
--- /dev/null
+++ b/zmscitizenapi/src/Zmscitizenapi/Models/ThinnedScope.php
@@ -0,0 +1,143 @@
+id = $id;
+ $this->provider = $provider;
+ $this->shortName = $shortName;
+ $this->telephoneActivated = $telephoneActivated;
+ $this->telephoneRequired = $telephoneRequired;
+ $this->customTextfieldActivated = $customTextfieldActivated;
+ $this->customTextfieldRequired = $customTextfieldRequired;
+ $this->customTextfieldLabel = $customTextfieldLabel;
+ $this->captchaActivatedRequired = $captchaActivatedRequired;
+ $this->displayInfo = $displayInfo;
+
+ $this->ensureValid();
+ }
+
+ private function ensureValid()
+ {
+ if (!$this->testValid()) {
+ throw new InvalidArgumentException("The provided data is invalid according to the schema.");
+ }
+ }
+
+ public function getProvider(): ?ThinnedProvider
+ {
+ return $this->provider;
+ }
+
+ public function getShortName(): ?string
+ {
+ return $this->shortName;
+ }
+
+ public function getTelephoneActivated(): ?bool
+ {
+ return $this->telephoneActivated;
+ }
+
+ public function getTelephoneRequired(): ?bool
+ {
+ return $this->telephoneRequired;
+ }
+
+ public function getCustomTextfieldActivated(): ?bool
+ {
+ return $this->customTextfieldActivated;
+ }
+
+ public function getCustomTextfieldRequired(): ?bool
+ {
+ return $this->customTextfieldRequired;
+ }
+
+ public function getCustomTextfieldLabel(): ?string
+ {
+ return $this->customTextfieldLabel;
+ }
+
+ public function getCaptchaActivatedRequired(): ?bool
+ {
+ return $this->captchaActivatedRequired;
+ }
+
+ public function getDisplayInfo(): ?string
+ {
+ return $this->displayInfo;
+ }
+
+ public function toArray(): array
+ {
+ return [
+ 'id' => $this->id,
+ 'provider' => $this->provider,
+ 'shortName' => $this->shortName,
+ 'telephoneActivated' => $this->telephoneActivated,
+ 'telephoneRequired' => $this->telephoneRequired,
+ 'customTextfieldActivated' => $this->customTextfieldActivated,
+ 'customTextfieldRequired' => $this->customTextfieldRequired,
+ 'customTextfieldLabel' => $this->customTextfieldLabel,
+ 'captchaActivatedRequired' => $this->captchaActivatedRequired,
+ 'displayInfo' => $this->displayInfo,
+ ];
+ }
+
+ public function jsonSerialize(): mixed
+ {
+ return $this->toArray();
+ }
+}
diff --git a/zmscitizenapi/src/Zmscitizenapi/Services/Appointment/AppointmentByIdService.php b/zmscitizenapi/src/Zmscitizenapi/Services/Appointment/AppointmentByIdService.php
new file mode 100644
index 000000000..ff8ee7b25
--- /dev/null
+++ b/zmscitizenapi/src/Zmscitizenapi/Services/Appointment/AppointmentByIdService.php
@@ -0,0 +1,45 @@
+extractClientData($queryParams);
+
+ $errors = $this->validateClientData($clientData);
+ if (!empty($errors['errors'])) {
+ return $errors;
+ }
+
+ return $this->getAppointment($clientData->processId, $clientData->authKey);
+ }
+
+ private function extractClientData(array $queryParams): object
+ {
+ return (object) [
+ 'processId' => isset($queryParams['processId']) && is_numeric($queryParams['processId'])
+ ? (int) $queryParams['processId']
+ : null,
+ 'authKey' => isset($queryParams['authKey']) && is_string($queryParams['authKey']) && trim($queryParams['authKey']) !== ''
+ ? htmlspecialchars(trim($queryParams['authKey']), ENT_QUOTES, 'UTF-8')
+ : null
+ ];
+ }
+
+ private function validateClientData(object $data): array
+ {
+ return ValidationService::validateGetProcessById($data->processId, $data->authKey);
+ }
+
+ private function getAppointment(?int $processId, ?string $authKey): ThinnedProcess|array
+ {
+ return ZmsApiFacadeService::getThinnedProcessById($processId, $authKey);
+ }
+}
\ No newline at end of file
diff --git a/zmscitizenapi/src/Zmscitizenapi/Services/Appointment/AppointmentCancelService.php b/zmscitizenapi/src/Zmscitizenapi/Services/Appointment/AppointmentCancelService.php
new file mode 100644
index 000000000..240716d0f
--- /dev/null
+++ b/zmscitizenapi/src/Zmscitizenapi/Services/Appointment/AppointmentCancelService.php
@@ -0,0 +1,91 @@
+extractClientData($body);
+
+ $errors = $this->validateClientData($clientData);
+ if (!empty($errors['errors'])) {
+ return $errors;
+ }
+
+ $process = $this->getProcess(
+ $clientData->processId,
+ $clientData->authKey
+ );
+
+ if (is_array($process) && !empty($process['errors'])) {
+ return $process;
+ }
+
+ if (!$this->canBeCancelled($process)) {
+ return ['errors' => [ErrorMessages::get('appointmentCanNotBeCanceled')]];
+ }
+
+ // Send cancellation email before cancelling the appointment
+ $this->sendCancellationEmail($process);
+
+ return $this->cancelProcess($process);
+ }
+
+ private function extractClientData(array $body): object
+ {
+ return (object) [
+ 'processId' => isset($body['processId']) && is_numeric($body['processId'])
+ ? (int) $body['processId']
+ : null,
+ 'authKey' => isset($body['authKey']) && is_string($body['authKey']) && trim($body['authKey']) !== ''
+ ? htmlspecialchars(trim($body['authKey']), ENT_QUOTES, 'UTF-8')
+ : null
+ ];
+ }
+
+ private function validateClientData(object $data): array
+ {
+ return ValidationService::validateGetProcessById(
+ $data->processId,
+ $data->authKey
+ );
+ }
+
+ private function getProcess(int $processId, string $authKey): ThinnedProcess|array
+ {
+ return ZmsApiFacadeService::getThinnedProcessById($processId, $authKey);
+ }
+
+ private function canBeCancelled(ThinnedProcess $process): bool
+ {
+ $appointmentTime = new \DateTimeImmutable("@{$process->timestamp}");
+ $now = new \DateTimeImmutable('now', new \DateTimeZone('Europe/Berlin'));
+ return $appointmentTime > $now;
+ }
+
+ private function cancelProcess(ThinnedProcess $process): ThinnedProcess|array
+ {
+ $processEntity = MapperService::thinnedProcessToProcess($process);
+ $result = ZmsApiFacadeService::cancelAppointment($processEntity);
+
+ if (is_array($result) && !empty($result['errors'])) {
+ return $result;
+ }
+
+ return MapperService::processToThinnedProcess($result);
+ }
+
+ private function sendCancellationEmail(ThinnedProcess $process): void
+ {
+ $processEntity = MapperService::thinnedProcessToProcess($process);
+ ZmsApiFacadeService::sendCancelationEmail($processEntity);
+ }
+}
\ No newline at end of file
diff --git a/zmscitizenapi/src/Zmscitizenapi/Services/Appointment/AppointmentConfirmService.php b/zmscitizenapi/src/Zmscitizenapi/Services/Appointment/AppointmentConfirmService.php
new file mode 100644
index 000000000..a4094f795
--- /dev/null
+++ b/zmscitizenapi/src/Zmscitizenapi/Services/Appointment/AppointmentConfirmService.php
@@ -0,0 +1,86 @@
+extractClientData($body);
+
+ $errors = $this->validateClientData($clientData);
+ if (!empty($errors['errors'])) {
+ return $errors;
+ }
+
+ $reservedProcess = $this->getReservedProcess(
+ $clientData->processId,
+ $clientData->authKey
+ );
+
+ if (is_array($reservedProcess) && !empty($reservedProcess['errors'])) {
+ return $reservedProcess;
+ }
+
+ $result = $this->confirmProcess($reservedProcess);
+ if (is_array($result) && !empty($result['errors'])) {
+ return $result;
+ }
+
+ if ($result->status === 'confirmed') {
+ $this->sendConfirmationEmail($result);
+ }
+
+ return $result;
+ }
+
+
+ private function extractClientData(array $body): object
+ {
+ return (object) [
+ 'processId' => isset($body['processId']) && is_numeric($body['processId'])
+ ? (int) $body['processId']
+ : null,
+ 'authKey' => isset($body['authKey']) && is_string($body['authKey']) && trim($body['authKey']) !== ''
+ ? htmlspecialchars(trim($body['authKey']), ENT_QUOTES, 'UTF-8')
+ : null
+ ];
+ }
+
+ private function validateClientData(object $data): array
+ {
+ return ValidationService::validateGetProcessById(
+ $data->processId,
+ $data->authKey
+ );
+ }
+
+ private function getReservedProcess(int $processId, string $authKey): ThinnedProcess|array
+ {
+ return ZmsApiFacadeService::getThinnedProcessById($processId, $authKey);
+ }
+
+ private function confirmProcess(ThinnedProcess $process): ThinnedProcess|array
+ {
+ $processEntity = MapperService::thinnedProcessToProcess($process);
+ $result = ZmsApiFacadeService::confirmAppointment($processEntity);
+
+ if (is_array($result) && !empty($result['errors'])) {
+ return $result;
+ }
+
+ return MapperService::processToThinnedProcess($result);
+ }
+
+ private function sendConfirmationEmail(ThinnedProcess $process): void
+ {
+ $processEntity = MapperService::thinnedProcessToProcess($process);
+ ZmsApiFacadeService::sendConfirmationEmail($processEntity);
+ }
+}
\ No newline at end of file
diff --git a/zmscitizenapi/src/Zmscitizenapi/Services/Appointment/AppointmentPreconfirmService.php b/zmscitizenapi/src/Zmscitizenapi/Services/Appointment/AppointmentPreconfirmService.php
new file mode 100644
index 000000000..1a4109f84
--- /dev/null
+++ b/zmscitizenapi/src/Zmscitizenapi/Services/Appointment/AppointmentPreconfirmService.php
@@ -0,0 +1,87 @@
+extractClientData($body);
+
+ $errors = $this->validateClientData($clientData);
+ if (!empty($errors['errors'])) {
+ return $errors;
+ }
+
+ $reservedProcess = $this->getReservedProcess(
+ $clientData->processId,
+ $clientData->authKey
+ );
+
+ if (is_array($reservedProcess) && !empty($reservedProcess['errors'])) {
+ return $reservedProcess;
+ }
+
+ $result = $this->preconfirmProcess($reservedProcess);
+ if (is_array($result) && !empty($result['errors'])) {
+ return $result;
+ }
+
+ if ($result->status === 'preconfirmed') {
+ $this->sendPreconfirmationEmail($result);
+ }
+
+ return $result;
+ }
+
+
+
+ private function extractClientData(array $body): object
+ {
+ return (object) [
+ 'processId' => isset($body['processId']) && is_numeric($body['processId'])
+ ? (int) $body['processId']
+ : null,
+ 'authKey' => isset($body['authKey']) && is_string($body['authKey']) && trim($body['authKey']) !== ''
+ ? htmlspecialchars(trim($body['authKey']), ENT_QUOTES, 'UTF-8')
+ : null
+ ];
+ }
+
+ private function validateClientData(object $data): array
+ {
+ return ValidationService::validateGetProcessById(
+ $data->processId,
+ $data->authKey
+ );
+ }
+
+ private function getReservedProcess(int $processId, string $authKey): ThinnedProcess|array
+ {
+ return ZmsApiFacadeService::getThinnedProcessById($processId, $authKey);
+ }
+
+ private function preconfirmProcess(ThinnedProcess $process): ThinnedProcess|array
+ {
+ $processEntity = MapperService::thinnedProcessToProcess($process);
+ $result = ZmsApiFacadeService::preconfirmAppointment($processEntity);
+
+ if (is_array($result) && !empty($result['errors'])) {
+ return $result;
+ }
+
+ return MapperService::processToThinnedProcess($result);
+ }
+
+ private function sendPreconfirmationEmail(ThinnedProcess $process): void
+ {
+ $processEntity = MapperService::thinnedProcessToProcess($process);
+ ZmsApiFacadeService::sendPreconfirmationEmail($processEntity);
+ }
+}
\ No newline at end of file
diff --git a/zmscitizenapi/src/Zmscitizenapi/Services/Appointment/AppointmentReserveService.php b/zmscitizenapi/src/Zmscitizenapi/Services/Appointment/AppointmentReserveService.php
new file mode 100644
index 000000000..ca37eacdd
--- /dev/null
+++ b/zmscitizenapi/src/Zmscitizenapi/Services/Appointment/AppointmentReserveService.php
@@ -0,0 +1,168 @@
+extractClientData($body);
+
+ $errors = $this->validateClientData($clientData);
+ if (!empty($errors['errors'])) {
+ return $errors;
+ }
+
+ if (!$this->verifyCaptcha($clientData->officeId, $clientData->captchaSolution)) {
+ return ErrorMessages::get('captchaVerificationFailed');
+ }
+
+ $errors = ValidationService::validateServiceLocationCombination(
+ $clientData->officeId,
+ $clientData->serviceIds
+ );
+
+ if (!empty($errors['errors'])) {
+ return $errors;
+ }
+
+ $selectedProcess = $this->findMatchingProcess(
+ $clientData->officeId,
+ $clientData->serviceIds,
+ $clientData->serviceCounts,
+ $clientData->timestamp
+ );
+
+ $errors = ValidationService::validateGetProcessNotFound($selectedProcess);
+ if (!empty($errors['errors'])) {
+ return $errors;
+ }
+
+ return $this->reserveAppointment(
+ $selectedProcess,
+ $clientData->serviceIds,
+ $clientData->serviceCounts,
+ $clientData->officeId
+ );
+ }
+
+ private function extractClientData(array $body): object
+ {
+ return (object) [
+ 'officeId' => isset($body['officeId']) && is_numeric($body['officeId']) ? (int) $body['officeId'] : null,
+ 'serviceIds' => $body['serviceId'] ?? null,
+ 'serviceCounts' => $body['serviceCount'] ?? [1],
+ 'captchaSolution' => $body['captchaSolution'] ?? null,
+ 'timestamp' => isset($body['timestamp']) && is_numeric($body['timestamp']) ? (int) $body['timestamp'] : null,
+ ];
+ }
+
+ private function validateClientData(object $data): array
+ {
+ return ValidationService::validatePostAppointmentReserve(
+ $data->officeId,
+ $data->serviceIds,
+ $data->serviceCounts,
+ $data->timestamp
+ );
+ }
+
+ private function verifyCaptcha(?int $officeId, ?string $captchaSolution): bool|array
+ {
+ $providerScope = ZmsApiFacadeService::getScopeByOfficeId($officeId);
+ $captchaRequired = \App::$CAPTCHA_ENABLED === true &&
+ isset($providerScope->captchaActivatedRequired) &&
+ $providerScope->captchaActivatedRequired === "1";
+
+ if (!$captchaRequired) {
+ return true;
+ }
+
+ try {
+ $captcha = new FriendlyCaptcha();
+ return $captcha->verifyCaptcha($captchaSolution);
+ } catch (\Exception $e) {
+ return ErrorMessages::get('captchaVerificationError');
+ }
+ }
+
+ private function findMatchingProcess(
+ int $officeId,
+ array $serviceIds,
+ array $serviceCounts,
+ int $timestamp
+ ): ?Process {
+ $freeAppointments = ZmsApiFacadeService::getFreeAppointments(
+ $officeId,
+ $serviceIds,
+ $serviceCounts,
+ DateTimeFormatHelper::getInternalDateFromTimestamp($timestamp)
+ );
+
+ foreach ($freeAppointments as $process) {
+ if (!isset($process->appointments) || empty($process->appointments)) {
+ continue;
+ }
+
+ foreach ($process->appointments as $appointment) {
+ if ((int) $appointment->date === $timestamp) {
+ $requestIds = [];
+ if ($process->requests) {
+ foreach ($process->requests as $request) {
+ $requestIds[] = $request->getId();
+ }
+ }
+
+ $processData = [
+ 'requests' => $requestIds,
+ 'appointments' => [$appointment]
+ ];
+
+ $process->withUpdatedData(
+ $processData,
+ new \DateTime("@$timestamp"),
+ $process->scope
+ );
+ return $process;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ private function reserveAppointment(
+ Process $process,
+ array $serviceIds,
+ array $serviceCounts,
+ int $officeId
+ ): ThinnedProcess {
+ $process->clients = [
+ [
+ 'email' => 'test@muenchen.de'
+ ]
+ ];
+ $reservedProcess = ZmsApiFacadeService::reserveTimeslot($process, $serviceIds, $serviceCounts);
+
+ if ($reservedProcess && $reservedProcess->scope && $reservedProcess->scope->id) {
+ $scopeId = $reservedProcess->scope->id;
+ $scope = ZmsApiFacadeService::getScopeById((int) $scopeId);
+
+ if (!isset($scope['errors']) && isset($scope) && !empty($scope)) {
+ $reservedProcess->scope = $scope;
+ $reservedProcess->officeId = $officeId;
+ }
+ }
+
+ return $reservedProcess;
+ }
+}
\ No newline at end of file
diff --git a/zmscitizenapi/src/Zmscitizenapi/Services/Appointment/AppointmentUpdateService.php b/zmscitizenapi/src/Zmscitizenapi/Services/Appointment/AppointmentUpdateService.php
new file mode 100644
index 000000000..2cb6bc7fd
--- /dev/null
+++ b/zmscitizenapi/src/Zmscitizenapi/Services/Appointment/AppointmentUpdateService.php
@@ -0,0 +1,90 @@
+extractClientData($body);
+
+ $errors = $this->validateClientData($clientData);
+ if (!empty($errors['errors'])) {
+ return $errors;
+ }
+
+ $reservedProcess = $this->getReservedProcess(
+ $clientData->processId,
+ $clientData->authKey
+ );
+
+ if (is_array($reservedProcess) && !empty($reservedProcess['errors'])) {
+ return $reservedProcess;
+ }
+
+ $updatedProcess = $this->updateProcessWithClientData($reservedProcess, $clientData);
+
+ return $this->saveProcessUpdate($updatedProcess);
+ }
+
+ private function extractClientData(array $body): object
+ {
+ return (object) [
+ 'processId' => isset($body['processId']) && is_numeric($body['processId'])
+ ? (int) $body['processId']
+ : null,
+ 'authKey' => isset($body['authKey']) && is_string($body['authKey']) && trim($body['authKey']) !== ''
+ ? htmlspecialchars(trim($body['authKey']), ENT_QUOTES, 'UTF-8')
+ : null,
+ 'familyName' => isset($body['familyName']) && is_string($body['familyName']) ? (string) $body['familyName'] : null,
+ 'email' => isset($body['email']) && is_string($body['email']) ? (string) $body['email'] : null,
+ 'telephone' => isset($body['telephone']) && is_string($body['telephone']) ? (string) $body['telephone'] : null,
+ 'customTextfield' => isset($body['customTextfield']) && is_string($body['customTextfield']) ? (string) $body['customTextfield'] : null,
+ ];
+ }
+
+ private function validateClientData(object $data): array
+ {
+ return ValidationService::validateUpdateAppointmentInputs(
+ $data->processId,
+ $data->authKey,
+ $data->familyName,
+ $data->email,
+ $data->telephone,
+ $data->customTextfield
+ );
+ }
+
+ private function getReservedProcess(int $processId, string $authKey): ThinnedProcess|array
+ {
+ return ZmsApiFacadeService::getThinnedProcessById($processId, $authKey);
+ }
+
+ private function updateProcessWithClientData(ThinnedProcess $process, object $data): ThinnedProcess
+ {
+ $process->familyName = $data->familyName ?? $process->familyName ?? null;
+ $process->email = $data->email ?? $process->email ?? null;
+ $process->telephone = $data->telephone ?? $process->telephone ?? null;
+ $process->customTextfield = $data->customTextfield ?? $process->customTextfield ?? null;
+
+ return $process;
+ }
+
+ private function saveProcessUpdate(ThinnedProcess $process): ThinnedProcess|array
+ {
+ $processEntity = MapperService::thinnedProcessToProcess($process);
+ $result = ZmsApiFacadeService::updateClientData($processEntity);
+
+ if (is_array($result) && !empty($result['errors'])) {
+ return $result;
+ }
+
+ return MapperService::processToThinnedProcess($result);
+ }
+}
\ No newline at end of file
diff --git a/zmscitizenapi/src/Zmscitizenapi/Services/Availability/AvailableAppointmentsListService.php b/zmscitizenapi/src/Zmscitizenapi/Services/Availability/AvailableAppointmentsListService.php
new file mode 100644
index 000000000..2642fe899
--- /dev/null
+++ b/zmscitizenapi/src/Zmscitizenapi/Services/Availability/AvailableAppointmentsListService.php
@@ -0,0 +1,57 @@
+extractClientData($queryParams);
+
+ $errors = $this->validateClientData($clientData);
+ if (!empty($errors['errors'])) {
+ return $errors;
+ }
+
+ return $this->getAvailableAppointments($clientData);
+ }
+
+ private function extractClientData(array $queryParams): object
+ {
+ return (object) [
+ 'date' => isset($queryParams['date']) ? (string) $queryParams['date'] : null,
+ 'officeId' => isset($queryParams['officeId']) ? (int) $queryParams['officeId'] : null,
+ 'serviceIds' => isset($queryParams['serviceId'])
+ ? array_map('trim', explode(',', $queryParams['serviceId']))
+ : [],
+ 'serviceCounts' => isset($queryParams['serviceCount'])
+ ? array_map('trim', explode(',', $queryParams['serviceCount']))
+ : []
+ ];
+ }
+
+ private function validateClientData(object $data): array
+ {
+ return ValidationService::validateGetAvailableAppointments(
+ $data->date,
+ $data->officeId,
+ $data->serviceIds,
+ $data->serviceCounts
+ );
+ }
+
+ private function getAvailableAppointments(object $data): array|AvailableAppointments
+ {
+ return ZmsApiFacadeService::getAvailableAppointments(
+ $data->date,
+ $data->officeId,
+ $data->serviceIds,
+ $data->serviceCounts
+ );
+ }
+}
\ No newline at end of file
diff --git a/zmscitizenapi/src/Zmscitizenapi/Services/Availability/AvailableDaysListService.php b/zmscitizenapi/src/Zmscitizenapi/Services/Availability/AvailableDaysListService.php
new file mode 100644
index 000000000..6d0e26870
--- /dev/null
+++ b/zmscitizenapi/src/Zmscitizenapi/Services/Availability/AvailableDaysListService.php
@@ -0,0 +1,66 @@
+extractClientData($queryParams);
+
+ $errors = $this->validateClientData($clientData);
+ if (!empty($errors['errors'])) {
+ return $errors;
+ }
+
+ return $this->getAvailableDays($clientData);
+ }
+
+ private function extractClientData(array $queryParams): object
+ {
+ $serviceCount = $queryParams['serviceCount'] ?? '';
+ $serviceCounts = !empty($serviceCount)
+ ? array_map('trim', explode(',', $serviceCount))
+ : [];
+
+ return (object) [
+ 'officeId' => isset($queryParams['officeId']) && is_numeric($queryParams['officeId'])
+ ? (int)$queryParams['officeId']
+ : null,
+ 'serviceId' => isset($queryParams['serviceId']) && is_numeric($queryParams['serviceId'])
+ ? (int)$queryParams['serviceId']
+ : null,
+ 'serviceCounts' => $serviceCounts,
+ 'startDate' => $queryParams['startDate'] ?? null,
+ 'endDate' => $queryParams['endDate'] ?? null
+ ];
+ }
+
+ private function validateClientData(object $data): array
+ {
+ return ValidationService::validateGetBookableFreeDays(
+ $data->officeId,
+ $data->serviceId,
+ $data->startDate,
+ $data->endDate,
+ $data->serviceCounts
+ );
+ }
+
+ private function getAvailableDays(object $data): array|AvailableDays
+ {
+ return ZmsApiFacadeService::getBookableFreeDays(
+ $data->officeId,
+ $data->serviceId,
+ $data->serviceCounts,
+ $data->startDate,
+ $data->endDate
+ );
+ }
+}
+
diff --git a/zmscitizenapi/src/Zmscitizenapi/Services/Core/ExceptionService.php b/zmscitizenapi/src/Zmscitizenapi/Services/Core/ExceptionService.php
new file mode 100644
index 000000000..af6e3c065
--- /dev/null
+++ b/zmscitizenapi/src/Zmscitizenapi/Services/Core/ExceptionService.php
@@ -0,0 +1,317 @@
+ [ErrorMessages::get('noAppointmentsAtLocation')]];
+ }
+
+ public static function appointmentNotFound(): array
+ {
+ return ['errors' => [ErrorMessages::get('appointmentNotFound')]];
+ }
+
+ public static function authKeyMismatch(): array
+ {
+ return ['errors' => [ErrorMessages::get('authKeyMismatch')]];
+ }
+
+ public static function preconfirmationExpired(): array
+ {
+ return ['errors' => [ErrorMessages::get('preconfirmationExpired')]];
+ }
+
+ public static function tooManyAppointmentsWithSameMail(): array
+ {
+ return ['errors' => [ErrorMessages::get('tooManyAppointmentsWithSameMail')]];
+ }
+
+ public static function officesNotFound(): array
+ {
+ return ['errors' => [ErrorMessages::get('officesNotFound')]];
+ }
+
+ public static function servicesNotFound(): array
+ {
+ return ['errors' => [ErrorMessages::get('servicesNotFound')]];
+ }
+
+ public static function scopesNotFound(): array
+ {
+ return ['errors' => [ErrorMessages::get('scopesNotFound')]];
+ }
+
+ public static function processInvalid(): array
+ {
+ return ['errors' => [ErrorMessages::get('processInvalid')]];
+ }
+
+ public static function processAlreadyExists(): array
+ {
+ return ['errors' => [ErrorMessages::get('processAlreadyExists')]];
+ }
+
+ public static function processDeleteFailed(): array
+ {
+ return ['errors' => [ErrorMessages::get('processDeleteFailed')]];
+ }
+
+ public static function processAlreadyCalled(): array
+ {
+ return ['errors' => [ErrorMessages::get('processAlreadyCalled')]];
+ }
+
+ public static function processNotReservedAnymore(): array
+ {
+ return ['errors' => [ErrorMessages::get('processNotReservedAnymore')]];
+ }
+
+ public static function processNotPreconfirmedAnymore(): array
+ {
+ return ['errors' => [ErrorMessages::get('processNotPreconfirmedAnymore')]];
+ }
+
+ public static function emailIsRequired(): array
+ {
+ return ['errors' => [ErrorMessages::get('emailIsRequired')]];
+ }
+
+ public static function telephoneIsRequired(): array
+ {
+ return ['errors' => [ErrorMessages::get('telephoneIsRequired')]];
+ }
+
+ public static function internalError(): array
+ {
+ return ['errors' => [ErrorMessages::get('internalError')]];
+ }
+
+ public static function invalidApiClient(): array
+ {
+ return ['errors' => [ErrorMessages::get('invalidApiClient')]];
+ }
+
+ public static function departmentNotFound(): array
+ {
+ return ['errors' => [ErrorMessages::get('departmentNotFound')]];
+ }
+
+ public static function mailNotFound(): array
+ {
+ return ['errors' => [ErrorMessages::get('mailNotFound')]];
+ }
+
+ public static function organisationNotFound(): array
+ {
+ return ['errors' => [ErrorMessages::get('organisationNotFound')]];
+ }
+
+ public static function providerNotFound(): array
+ {
+ return ['errors' => [ErrorMessages::get('providerNotFound')]];
+ }
+
+ public static function requestNotFound(): array
+ {
+ return ['errors' => [ErrorMessages::get('requestNotFound')]];
+ }
+
+ public static function scopeNotFound(): array
+ {
+ return ['errors' => [ErrorMessages::get('scopeNotFound')]];
+ }
+
+ public static function handleException(\Exception $e, string $method): never
+ {
+ $exceptionName = json_decode(json_encode($e), true)['template'] ?? null;
+
+ // Common exceptions across all methods
+ switch ($exceptionName) {
+ case 'BO\\Zmsapi\\Exception\\Process\\ApiclientInvalid':
+ $error = ErrorMessages::get('invalidApiClient');
+ break;
+ case 'BO\\Zmsapi\\Exception\\Department\\DepartmentNotFound':
+ $error = ErrorMessages::get('departmentNotFound');
+ break;
+ case 'BO\\Zmsapi\\Exception\\Mail\\MailNotFound':
+ $error = ErrorMessages::get('mailNotFound');
+ break;
+ case 'BO\\Zmsapi\\Exception\\Organisation\\OrganisationNotFound':
+ $error = ErrorMessages::get('organisationNotFound');
+ break;
+ case 'BO\\Zmsapi\\Exception\\Provider\\ProviderNotFound':
+ $error = ErrorMessages::get('providerNotFound');
+ break;
+ case 'BO\\Zmsapi\\Exception\\Request\\RequestNotFound':
+ $error = ErrorMessages::get('requestNotFound');
+ break;
+ case 'BO\\Zmsapi\\Exception\\Scope\\ScopeNotFound':
+ $error = ErrorMessages::get('scopeNotFound');
+ break;
+ case 'BO\\Zmsapi\\Exception\\Process\\ProcessInvalid':
+ $error = ErrorMessages::get('processInvalid');
+ break;
+
+ // Method-specific exceptions
+ default:
+ $error = self::handleMethodSpecificException($exceptionName, $method);
+ break;
+ }
+
+ throw new \RuntimeException(
+ $error['errorCode'] . ': ' . $error['errorMessage'],
+ $error['statusCode'],
+ $e
+ );
+ }
+
+ private static function handleMethodSpecificException(?string $exceptionName, string $method): array
+ {
+ switch ($method) {
+ case 'getOffices':
+ case 'getScopes':
+ case 'getServices':
+ case 'getRequestRelationList':
+ if ($exceptionName === 'BO\\Zmsapi\\Exception\\Source\\SourceNotFound') {
+ return ErrorMessages::get('internalError');
+ }
+ break;
+
+ case 'getFreeDays':
+ case 'getFreeTimeslots':
+ if ($exceptionName === 'BO\\Zmsapi\\Exception\\Calendar\\InvalidFirstDay') {
+ return ErrorMessages::get('invalidDateRange');
+ }
+ if ($exceptionName === 'BO\\Zmsapi\\Exception\\Calendar\\AppointmentsMissed') {
+ return ErrorMessages::get('noAppointmentsAtLocation');
+ }
+ break;
+
+ case 'reserveTimeslot':
+ if ($exceptionName === 'BO\\Zmsapi\\Exception\\Process\\ProcessAlreadyExists') {
+ return ErrorMessages::get('processAlreadyExists');
+ }
+ if ($exceptionName === 'BO\\Zmsapi\\Exception\\Process\\EmailRequired') {
+ return ErrorMessages::get('emailIsRequired');
+ }
+ if ($exceptionName === 'BO\\Zmsapi\\Exception\\Process\\TelephoneRequired') {
+ return ErrorMessages::get('telephoneIsRequired');
+ }
+ break;
+
+ case 'submitClientData':
+ if ($exceptionName === 'BO\\Zmsapi\\Exception\\Process\\MoreThanAllowedAppointmentsPerMail') {
+ return ErrorMessages::get('tooManyAppointmentsWithSameMail');
+ }
+ if ($exceptionName === 'BO\\Zmsapi\\Exception\\Process\\EmailRequired') {
+ return ErrorMessages::get('emailIsRequired');
+ }
+ if ($exceptionName === 'BO\\Zmsapi\\Exception\\Process\\TelephoneRequired') {
+ return ErrorMessages::get('telephoneIsRequired');
+ }
+ if ($exceptionName === 'BO\\Zmsapi\\Exception\\Process\\ProcessNotFound') {
+ return ErrorMessages::get('appointmentNotFound');
+ }
+ if ($exceptionName === 'BO\\Zmsapi\\Exception\\Process\\AuthKeyMatchFailed') {
+ return ErrorMessages::get('authKeyMismatch');
+ }
+ if ($exceptionName === 'BO\\Zmsapi\\Exception\\Process\\ProcessNotReservedAnymore') {
+ return ErrorMessages::get('processNotReservedAnymore');
+ }
+ break;
+
+ case 'preconfirmProcess':
+ if ($exceptionName === 'BO\\Zmsapi\\Exception\\Process\\PreconfirmationExpired') {
+ return ErrorMessages::get('preconfirmationExpired');
+ }
+ if ($exceptionName === 'BO\\Zmsapi\\Exception\\Process\\MoreThanAllowedAppointmentsPerMail') {
+ return ErrorMessages::get('tooManyAppointmentsWithSameMail');
+ }
+ if ($exceptionName === 'BO\\Zmsapi\\Exception\\Process\\ProcessNotFound') {
+ return ErrorMessages::get('appointmentNotFound');
+ }
+ if ($exceptionName === 'BO\\Zmsapi\\Exception\\Process\\AuthKeyMatchFailed') {
+ return ErrorMessages::get('authKeyMismatch');
+ }
+ if ($exceptionName === 'BO\\Zmsapi\\Exception\\Process\\ProcessNotReservedAnymore') {
+ return ErrorMessages::get('processNotReservedAnymore');
+ }
+ if ($exceptionName === 'BO\\Zmsapi\\Exception\\Process\\ProcessAlreadyCalled') {
+ return ErrorMessages::get('processAlreadyCalled');
+ }
+ break;
+
+ case 'confirmProcess':
+ if ($exceptionName === 'BO\\Zmsapi\\Exception\\Process\\MoreThanAllowedAppointmentsPerMail') {
+ return ErrorMessages::get('tooManyAppointmentsWithSameMail');
+ }
+ if ($exceptionName === 'BO\\Zmsapi\\Exception\\Process\\ProcessNotFound') {
+ return ErrorMessages::get('appointmentNotFound');
+ }
+ if ($exceptionName === 'BO\\Zmsapi\\Exception\\Process\\AuthKeyMatchFailed') {
+ return ErrorMessages::get('authKeyMismatch');
+ }
+ if ($exceptionName === 'BO\\Zmsapi\\Exception\\Process\\ProcessNotReservedAnymore') {
+ return ErrorMessages::get('processNotReservedAnymore');
+ }
+ if ($exceptionName === 'BO\\Zmsapi\\Exception\\Process\\ProcessNotPreconfirmedAnymore') {
+ return ErrorMessages::get('processNotPreconfirmedAnymore');
+ }
+ if ($exceptionName === 'BO\\Zmsapi\\Exception\\Process\\ProcessAlreadyCalled') {
+ return ErrorMessages::get('processAlreadyCalled');
+ }
+ break;
+
+ case 'cancelAppointment':
+ if ($exceptionName === 'BO\\Zmsapi\\Exception\\Process\\ProcessNotFound') {
+ return ErrorMessages::get('appointmentNotFound');
+ }
+ if ($exceptionName === 'BO\\Zmsapi\\Exception\\Process\\AuthKeyMatchFailed') {
+ return ErrorMessages::get('authKeyMismatch');
+ }
+ if ($exceptionName === 'BO\\Zmsapi\\Exception\\Process\\ProcessDeleteFailed') {
+ return ErrorMessages::get('processDeleteFailed');
+ }
+ if ($exceptionName === 'BO\\Zmsapi\\Exception\\Process\\ProcessNotReservedAnymore') {
+ return ErrorMessages::get('processNotReservedAnymore');
+ }
+ if ($exceptionName === 'BO\\Zmsapi\\Exception\\Process\\ProcessNotPreconfirmedAnymore') {
+ return ErrorMessages::get('processNotPreconfirmedAnymore');
+ }
+ if ($exceptionName === 'BO\\Zmsapi\\Exception\\Process\\ProcessAlreadyCalled') {
+ return ErrorMessages::get('processAlreadyCalled');
+ }
+ break;
+
+ case 'getProcessById':
+ if ($exceptionName === 'BO\\Zmsapi\\Exception\\Process\\ProcessNotFound') {
+ return ErrorMessages::get('appointmentNotFound');
+ }
+ if ($exceptionName === 'BO\\Zmsapi\\Exception\\Process\\AuthKeyMatchFailed') {
+ return ErrorMessages::get('authKeyMismatch');
+ }
+ case 'sendConfirmationEmail':
+ case 'sendPreconfirmationEmail':
+ case 'sendCancelationEmail':
+ if ($exceptionName === 'BO\\Zmsapi\\Exception\\Process\\ProcessNotFound') {
+ return ErrorMessages::get('appointmentNotFound');
+ }
+ if ($exceptionName === 'BO\\Zmsapi\\Exception\\Process\\AuthKeyMatchFailed') {
+ return ErrorMessages::get('authKeyMismatch');
+ }
+ if ($exceptionName === 'BO\\Zmsapi\\Exception\\Process\\ProcessAlreadyCalled') {
+ return ErrorMessages::get('processAlreadyCalled');
+ }
+ break;
+ }
+
+ return ErrorMessages::get('internalError');
+ }
+}
\ No newline at end of file
diff --git a/zmscitizenapi/src/Zmscitizenapi/Services/Core/LoggerService.php b/zmscitizenapi/src/Zmscitizenapi/Services/Core/LoggerService.php
new file mode 100644
index 000000000..2e56f5ab2
--- /dev/null
+++ b/zmscitizenapi/src/Zmscitizenapi/Services/Core/LoggerService.php
@@ -0,0 +1,442 @@
+getUri();
+ $path = $uri->getPath();
+
+ $queryParams = array_filter(
+ $request->getQueryParams(),
+ function ($key) {
+ return !str_starts_with($key, '/');
+ },
+ ARRAY_FILTER_USE_KEY
+ );
+
+ $queryParts = [];
+ foreach ($queryParams as $key => $value) {
+ $encodedKey = urlencode($key);
+ $encodedValue = in_array(strtolower($key), ['authkey', 'auth_key', 'key'])
+ ? '****'
+ : urlencode($value);
+ $queryParts[] = "$encodedKey=$encodedValue";
+ }
+ $queryString = implode('&', $queryParts);
+
+ $data = [
+ 'timestamp' => date('Y-m-d H:i:s'),
+ 'method' => $request->getMethod(),
+ 'path' => $path . ($queryString ? '?' . $queryString : ''),
+ 'status' => $response->getStatusCode(),
+ 'ip' => ClientIpHelper::getClientIp(),
+ 'headers' => self::filterSensitiveHeaders($request->getHeaders())
+ ];
+
+ if ($response->getStatusCode() >= 400) {
+ $body = '';
+ $stream = $response->getBody();
+
+ if ($stream->isSeekable()) {
+ try {
+ $stream->seek(0, SEEK_END);
+ $size = $stream->tell();
+ $stream->rewind();
+
+ $maxSafeSize = min(
+ self::MAX_RESPONSE_LENGTH,
+ (int) (self::MAX_MESSAGE_SIZE * 0.75)
+ );
+
+ if ($size > $maxSafeSize) {
+ $data['response'] = [
+ 'error' => 'Response body too large to log',
+ 'size' => $size
+ ];
+ } else {
+ $body = (string) $stream;
+ $stream->rewind();
+
+ try {
+ $decodedBody = json_decode($body, true);
+ if (
+ json_last_error() === JSON_ERROR_NONE &&
+ isset($decodedBody['errors']) &&
+ is_array($decodedBody['errors'])
+ ) {
+ // Only log if response contains an errors array
+ $data['response'] = $decodedBody;
+ }
+ } catch (\Throwable $e) {
+ $data['response'] = [
+ 'error' => 'Failed to decode response body',
+ 'message' => $e->getMessage()
+ ];
+ }
+ }
+ } catch (\RuntimeException $e) {
+ $data['response'] = [
+ 'error' => 'Failed to read response body',
+ 'message' => $e->getMessage()
+ ];
+ }
+ } else {
+ $data['response'] = [
+ 'error' => 'Response body not seekable'
+ ];
+ }
+ }
+
+ self::writeLog(
+ $response->getStatusCode() >= 400 ? LOG_ERR : LOG_INFO,
+ self::encodeJson($data)
+ );
+ }
+
+ private static function checkRateLimit(): bool
+ {
+ if (\App::$cache === null) {
+ error_log('Cache not available for rate limiting');
+ return true;
+ }
+
+ $attempt = 0;
+ $key = self::CACHE_COUNTER_KEY;
+ $lockKey = $key . '_lock';
+
+ while ($attempt < self::MAX_RETRIES) {
+ try {
+ if (self::acquireLock($lockKey)) {
+ try {
+ $data = \App::$cache->get($key);
+
+ if ($data === null) {
+ // First log in this window
+ \App::$cache->set($key, [
+ 'count' => 1,
+ 'timestamp' => time()
+ ], 60);
+ return true;
+ }
+
+ if (!is_array($data) || !isset($data['count'], $data['timestamp'])) {
+ // Handle corrupted data
+ \App::$cache->delete($key);
+ return true;
+ }
+
+ $count = (int)$data['count'];
+
+ if ($count >= self::MAX_LOGS_PER_MINUTE) {
+ error_log('Log rate limit exceeded');
+ return false;
+ }
+
+ // Update the counter atomically
+ $data['count'] = $count + 1;
+ \App::$cache->set($key, $data, 60);
+
+ return true;
+ } finally {
+ self::releaseLock($lockKey);
+ }
+ }
+ } catch (\Throwable $e) {
+ error_log('Rate limiting error: ' . $e->getMessage());
+ }
+
+ $attempt++;
+ if ($attempt < self::MAX_RETRIES) {
+ $backoffMs = min(
+ self::BACKOFF_MAX,
+ (int)(self::BACKOFF_MIN * pow(2, $attempt))
+ );
+ $jitterMs = random_int(0, (int)($backoffMs * 0.1));
+ usleep(($backoffMs + $jitterMs) * 1000);
+ }
+ }
+
+ // If we can't acquire the lock after retries, allow logging
+ error_log('Failed to acquire rate limit lock after retries');
+ return true;
+ }
+
+ private static function acquireLock(string $lockKey): bool
+ {
+ if (!\App::$cache->has($lockKey)) {
+ return \App::$cache->set($lockKey, true, self::LOCK_TIMEOUT);
+ }
+ return false;
+ }
+
+ private static function releaseLock(string $lockKey): void
+ {
+ \App::$cache->delete($lockKey);
+ }
+
+ private static function writeLog(int $priority, string $message): void
+ {
+ // Initialize syslog connection if needed
+ self::init();
+
+ if (!self::$logOpened) {
+ error_log('Syslog not available, falling back to error_log');
+ error_log($message);
+ return;
+ }
+
+ // Truncate message if too large
+ if (strlen($message) > self::MAX_MESSAGE_SIZE) {
+ $message = mb_substr($message, 0, self::MAX_MESSAGE_SIZE - 64)
+ . ' ... [truncated, full length: ' . strlen($message) . ']';
+ }
+
+ try {
+ if (@syslog($priority, $message) === false) {
+ error_log('Failed to write to syslog');
+ error_log($message);
+ }
+ } catch (\Throwable $e) {
+ error_log('Logging failed: ' . $e->getMessage());
+ error_log($message);
+ }
+ }
+
+ private static function formatErrorMessage(
+ \Throwable $exception,
+ ?RequestInterface $request,
+ ?ResponseInterface $response,
+ array $context
+ ): string {
+ $data = [
+ 'timestamp' => date('Y-m-d H:i:s'),
+ 'exception' => get_class($exception),
+ 'message' => $exception->getMessage(),
+ 'code' => $exception->getCode(),
+ 'file' => $exception->getFile(),
+ 'line' => $exception->getLine(),
+ 'trace' => implode("\n", array_slice(
+ explode("\n", $exception->getTraceAsString()),
+ 0,
+ self::MAX_STACK_LINES
+ ))
+ ];
+
+ if ($request) {
+ $data['request'] = [
+ 'method' => $request->getMethod(),
+ 'uri' => (string) $request->getUri(),
+ 'headers' => self::filterSensitiveHeaders($request->getHeaders())
+ ];
+ }
+
+ if ($response) {
+ $data['response'] = [
+ 'status' => $response->getStatusCode(),
+ 'headers' => self::filterSensitiveHeaders($response->getHeaders())
+ ];
+
+ // Only include response body for errors
+ if ($response->getStatusCode() >= 400) {
+ $body = '';
+ $stream = $response->getBody();
+
+ if ($stream->isSeekable()) {
+ try {
+ $stream->seek(0, SEEK_END);
+ $size = $stream->tell();
+ $stream->rewind();
+
+ $maxSafeSize = min(
+ self::MAX_RESPONSE_LENGTH,
+ (int) (self::MAX_MESSAGE_SIZE * 0.75)
+ );
+
+ if ($size > $maxSafeSize) {
+ $data['response']['body'] = [
+ 'error' => 'Response body too large to log',
+ 'size' => $size
+ ];
+ } else {
+ $body = (string) $stream;
+ $stream->rewind();
+
+ $decodedBody = json_decode($body, true);
+ if (
+ json_last_error() === JSON_ERROR_NONE &&
+ isset($decodedBody['errors']) &&
+ is_array($decodedBody['errors'])
+ ) {
+ // Only log if response contains an errors array
+ $data['response']['body'] = $decodedBody;
+ }
+ }
+ } catch (\Throwable $e) {
+ $data['response']['body'] = [
+ 'error' => 'Failed to decode response body',
+ 'message' => $e->getMessage()
+ ];
+ }
+ } else {
+ $data['response']['body'] = [
+ 'error' => 'Response body not seekable'
+ ];
+ }
+ }
+ }
+
+ if (!empty($context)) {
+ $data['context'] = $context;
+ }
+
+ return self::encodeJson($data);
+ }
+
+ private static function formatMessage(string $message, array $context): string
+ {
+ $data = [
+ 'timestamp' => date('Y-m-d H:i:s'),
+ 'message' => $message
+ ];
+
+ if ($context) {
+ $data['context'] = $context;
+ }
+
+ return self::encodeJson($data);
+ }
+
+ private static function filterSensitiveHeaders(array $headers): array
+ {
+ $filtered = [];
+ foreach ($headers as $name => $values) {
+ $lower = strtolower($name);
+ if (in_array($lower, self::SENSITIVE_HEADERS, true)) {
+ $filtered[$name] = ['[REDACTED]'];
+ } elseif (in_array($lower, self::IMPORTANT_HEADERS, true)) {
+ $filtered[$name] = $values;
+ }
+ }
+ return $filtered;
+ }
+
+ private static function encodeJson(array $data): string
+ {
+ try {
+ $json = json_encode($data, JSON_UNESCAPED_SLASHES | JSON_INVALID_UTF8_SUBSTITUTE);
+ if ($json === false) {
+ throw new \RuntimeException(json_last_error_msg(), json_last_error());
+ }
+ return $json;
+ } catch (\Throwable $e) {
+ error_log('JSON encoding failed: ' . $e->getMessage());
+ // Return simplified JSON with error
+ return json_encode([
+ 'error' => 'Failed to encode log data',
+ 'message' => $e->getMessage()
+ ], JSON_UNESCAPED_SLASHES);
+ }
+ }
+}
\ No newline at end of file
diff --git a/zmscitizenapi/src/Zmscitizenapi/Services/Core/MapperService.php b/zmscitizenapi/src/Zmscitizenapi/Services/Core/MapperService.php
new file mode 100644
index 000000000..ace21fb0b
--- /dev/null
+++ b/zmscitizenapi/src/Zmscitizenapi/Services/Core/MapperService.php
@@ -0,0 +1,338 @@
+getScopes() as $scope) {
+ if ($scope->provider && $scope->provider->id === $providerId) {
+ $matchingScope = $scope;
+ break;
+ }
+ }
+
+ return $matchingScope;
+ }
+
+ public static function mapOfficesWithScope(ProviderList $providerList): OfficeList
+ {
+ $offices = [];
+ $scopes = ZmsApiFacadeService::getScopes();
+
+ if (!$scopes instanceof ThinnedScopeList) {
+ return new OfficeList();
+ }
+
+ foreach ($providerList as $provider) {
+ $providerScope = self::mapScopeForProvider((int) $provider->id, $scopes);
+
+ $offices[] = new Office(
+ id: isset($provider->id) ? (int) $provider->id : 0,
+ name: isset($provider->displayName) ? $provider->displayName : (isset($provider->name) ? $provider->name : null),
+ address: isset($provider->data['address']) ? $provider->data['address'] : null,
+ geo: isset($provider->data['geo']) ? $provider->data['geo'] : null,
+ scope: isset($providerScope) && !isset($providerScope['errors']) ? new ThinnedScope(
+ id: isset($providerScope->id) ? (int) $providerScope->id : 0,
+ provider: isset($providerScope->provider) ? $providerScope->provider : null,
+ shortName: isset($providerScope->shortName) ? $providerScope->shortName : null,
+ telephoneActivated: isset($providerScope->telephoneActivated) ? (bool) $providerScope->telephoneActivated : null,
+ telephoneRequired: isset($providerScope->telephoneRequired) ? (bool) $providerScope->telephoneRequired : null,
+ customTextfieldActivated: isset($providerScope->customTextfieldActivated) ? (bool) $providerScope->customTextfieldActivated : null,
+ customTextfieldRequired: isset($providerScope->customTextfieldRequired) ? (bool) $providerScope->customTextfieldRequired : null,
+ customTextfieldLabel: isset($providerScope->customTextfieldLabel) ? $providerScope->customTextfieldLabel : null,
+ captchaActivatedRequired: isset($providerScope->captchaActivatedRequired) ? (bool) $providerScope->captchaActivatedRequired : null,
+ displayInfo: isset($providerScope->displayInfo) ? $providerScope->displayInfo : null
+ ) : null
+ );
+ }
+
+ return new OfficeList($offices);
+ }
+
+ public static function mapCombinable(array $serviceCombinations): ?Combinable
+ {
+ return !empty($serviceCombinations) ? new Combinable($serviceCombinations) : null;
+ }
+
+ /**
+ * Map services with combinations based on request and relation lists.
+ *
+ * @param RequestList $requestList
+ * @param RequestRelationList $relationList
+ * @return ServiceList
+ */
+ public static function mapServicesWithCombinations(RequestList $requestList, RequestRelationList $relationList): ServiceList
+ {
+ /** @var array> $servicesProviderIds */
+ $servicesProviderIds = [];
+ foreach ($relationList as $relation) {
+ $serviceId = $relation->request->id;
+ $servicesProviderIds[$serviceId] ??= [];
+ $servicesProviderIds[$serviceId][] = $relation->provider->id;
+ }
+
+ /** @var Service[] $services */
+ $services = [];
+ foreach ($requestList as $service) {
+ /** @var array> $serviceCombinations */
+ $serviceCombinations = [];
+ $combinableData = $service->getAdditionalData()['combinable'] ?? [];
+
+ foreach ($combinableData as $combinationServiceId) {
+ $commonProviders = array_intersect(
+ $servicesProviderIds[$service->getId()] ?? [],
+ $servicesProviderIds[$combinationServiceId] ?? []
+ );
+ $serviceCombinations[$combinationServiceId] = !empty($commonProviders) ? array_values($commonProviders) : [];
+ }
+
+ $combinable = self::mapCombinable($serviceCombinations);
+
+ $services[] = new Service(
+ id: (int) $service->getId(),
+ name: $service->getName(),
+ maxQuantity: $service->getAdditionalData()['maxQuantity'] ?? 1,
+ combinable: $combinable ?? new Combinable()
+ );
+ }
+
+ return new ServiceList($services);
+ }
+
+ public static function mapRelations(RequestRelationList $relationList): OfficeServiceRelationList
+ {
+ $relations = [];
+ foreach ($relationList as $relation) {
+ $relations[] = new OfficeServiceRelation(
+ officeId: (int) $relation->provider->id,
+ serviceId: (int) $relation->request->id,
+ slots: intval($relation->slots)
+ );
+ }
+
+ return new OfficeServiceRelationList($relations);
+ }
+
+ public static function scopeToThinnedScope(Scope $scope): ThinnedScope
+ {
+ if (!$scope || !isset($scope->id)) {
+ return new ThinnedScope();
+ }
+
+ $provider = null;
+
+ try {
+ if ($scope->getProvider()) {
+ $provider = $scope->getProvider();
+ $contact = $provider->getContact();
+
+ $thinnedProvider = new ThinnedProvider(
+ id: (int) $provider->id ?? null,
+ name: $provider->getName() ?? null,
+ source: $provider->getSource() ?? null,
+ contact: self::contactToThinnedContact($contact ) ? new ThinnedContact() : null
+ );
+ }
+ } catch (\BO\Zmsentities\Exception\ScopeMissingProvider $e) {
+ $thinnedProvider = null;
+ }
+
+ return new ThinnedScope(
+ id: (int) ($scope->getId() ?? 0),
+ provider: $thinnedProvider,
+ shortName: $scope->getShortName() ?? null,
+ telephoneActivated: $scope->getTelephoneActivated() !== null ? (bool) $scope->getTelephoneActivated() : null,
+ telephoneRequired: $scope->getTelephoneRequired() !== null ? (bool) $scope->getTelephoneRequired() : null,
+ customTextfieldActivated: $scope->getCustomTextfieldActivated() !== null ? (bool) $scope->getCustomTextfieldActivated() : null,
+ customTextfieldRequired: $scope->getCustomTextfieldRequired() !== null ? (bool) $scope->getCustomTextfieldRequired() : null,
+ customTextfieldLabel: $scope->getCustomTextfieldLabel() ?? null,
+ captchaActivatedRequired: $scope->getCaptchaActivatedRequired() !== null ? (bool) $scope->getCaptchaActivatedRequired() : null,
+ displayInfo: $scope->getDisplayInfo() ?? null
+ );
+ }
+
+ public static function processToThinnedProcess(Process $myProcess): ThinnedProcess
+ {
+ if (!$myProcess || !isset($myProcess->id)) {
+ return new ThinnedProcess();
+ }
+
+ $subRequestCounts = [];
+ $mainServiceId = null;
+ $mainServiceCount = 0;
+
+ $requests = $myProcess->getRequests() ?? [];
+ if ($requests) {
+ $requests = is_array($requests) ? $requests : iterator_to_array($requests);
+ if (count($requests) > 0) {
+ $mainServiceId = $requests[0]->id;
+ foreach ($requests as $request) {
+ if ($request->id === $mainServiceId) {
+ $mainServiceCount++;
+ } else {
+ if (!isset($subRequestCounts[$request->id])) {
+ $subRequestCounts[$request->id] = [
+ 'id' => $request->id,
+ 'count' => 0,
+ ];
+ }
+ $subRequestCounts[$request->id]['count']++;
+ }
+ }
+ }
+ }
+
+ return new ThinnedProcess(
+ processId: isset($myProcess->id) ? (int) $myProcess->id : 0,
+ timestamp: (isset($myProcess->appointments[0]) && isset($myProcess->appointments[0]->date)) ? strval($myProcess->appointments[0]->date) : null,
+ authKey: isset($myProcess->authKey) ? $myProcess->authKey : null,
+ familyName: (isset($myProcess->clients[0]) && isset($myProcess->clients[0]->familyName)) ? $myProcess->clients[0]->familyName : null,
+ customTextfield: isset($myProcess->customTextfield) ? $myProcess->customTextfield : null,
+ email: (isset($myProcess->clients[0]) && isset($myProcess->clients[0]->email)) ? $myProcess->clients[0]->email : null,
+ telephone: (isset($myProcess->clients[0]) && isset($myProcess->clients[0]->telephone)) ? $myProcess->clients[0]->telephone : null,
+ officeName: (isset($myProcess->scope->contact) && isset($myProcess->scope->contact->name)) ? $myProcess->scope->contact->name : null,
+ officeId: (isset($myProcess->scope->provider) && isset($myProcess->scope->provider->id)) ? (int) $myProcess->scope->provider->id : 0,
+ scope: isset($myProcess->scope) ? self::scopeToThinnedScope($myProcess->scope) : null,
+ subRequestCounts: isset($subRequestCounts) ? array_values($subRequestCounts) : [],
+ serviceId: isset($mainServiceId) ? (int) $mainServiceId : 0,
+ serviceCount: isset($mainServiceCount) ? $mainServiceCount : 0,
+ status: (isset($myProcess->queue) && isset($myProcess->queue->status)) ? $myProcess->queue->status : null
+ );
+ }
+
+ public static function thinnedProcessToProcess(ThinnedProcess $thinnedProcess): Process
+ {
+ if (!$thinnedProcess || !isset($thinnedProcess->processId)) {
+ return new Process();
+ }
+
+ $processEntity = new Process();
+ $processEntity->id = $thinnedProcess->processId;
+ $processEntity->authKey = $thinnedProcess->authKey ?? null;
+
+ $client = new Client();
+ $client->familyName = $thinnedProcess->familyName ?? null;
+ $client->email = $thinnedProcess->email ?? null;
+ $client->telephone = $thinnedProcess->telephone ?? null;
+ $client->customTextfield = $thinnedProcess->customTextfield ?? null;
+
+ $processEntity->clients = [$client];
+
+ $appointment = new Appointment();
+ $appointment->date = $thinnedProcess->timestamp ?? null;
+ $processEntity->appointments = [$appointment];
+
+ $scope = new Scope();
+ if (isset($thinnedProcess->officeName)) {
+ $scope->contact = new Contact();
+ $scope->contact->name = $thinnedProcess->officeName;
+ }
+ if (isset($thinnedProcess->officeId)) {
+ $scope->provider = new Provider();
+ $scope->provider->id = $thinnedProcess->officeId;
+ $scope->provider->source = \App::$source_name;
+ }
+ $processEntity->scope = $scope;
+
+ if (isset($thinnedProcess->status)) {
+ $processEntity->queue = new \stdClass();
+ $processEntity->queue->status = $thinnedProcess->status;
+ $processEntity->status = $thinnedProcess->status;
+ }
+
+ $mainServiceId = $thinnedProcess->serviceId ?? null;
+ $mainServiceCount = $thinnedProcess->serviceCount ?? 0;
+ $subRequestCounts = $thinnedProcess->subRequestCounts ?? [];
+
+ $requests = [];
+ for ($i = 0; $i < $mainServiceCount; $i++) {
+ $request = new Request();
+ $request->id = $mainServiceId;
+ $request->source = \App::$source_name;
+ $requests[] = $request;
+ }
+ foreach ($subRequestCounts as $subRequest) {
+ for ($i = 0; $i < ($subRequest['count'] ?? 0); $i++) {
+ $request = new Request();
+ $request->id = $subRequest['id'];
+ $request->source = \App::$source_name;
+ $requests[] = $request;
+ }
+ }
+ $processEntity->requests = $requests;
+
+ $processEntity->lastChange = time();
+ $processEntity->createIP = ClientIpHelper::getClientIp();
+ $processEntity->createTimestamp = time();
+
+ return $processEntity;
+ }
+
+ /**
+ * Converts a raw or existing contact object/array into a ThinnedContact model.
+ *
+ * @param object|array $contact
+ * @return ThinnedContact
+ */
+ public static function contactToThinnedContact($contact): ThinnedContact
+ {
+ return new ThinnedContact(
+ $contact['city'] ?? $contact->city ?? '',
+ $contact['country'] ?? $contact->country ?? '',
+ $contact['name'] ?? $contact->name ?? '',
+ $contact['postalCode'] ?? $contact->postalCode ?? '',
+ $contact['region'] ?? $contact->region ?? '',
+ $contact['street'] ?? $contact->street ?? '',
+ $contact['streetNumber'] ?? $contact->streetNumber ?? ''
+ );
+ }
+
+ /**
+ * Convert a Provider object to a ThinnedProvider.
+ *
+ * @param Provider $provider
+ * @return ThinnedProvider
+ */
+ public static function providerToThinnedProvider(Provider $provider): ThinnedProvider
+ {
+ return new ThinnedProvider(
+ id: isset($provider->id) ? (int) $provider->id : null,
+ name: isset($provider->name) ? $provider->name : null,
+ source: isset($provider->source) ? $provider->source : null,
+ lon: isset($provider->data['geo']['lon']) ? (float)$provider->data['geo']['lon'] : null,
+ lat: isset($provider->data['geo']['lat']) ? (float)$provider->data['geo']['lat'] : null,
+ contact: isset($provider->contact) ? self::contactToThinnedContact($provider->contact) : null,
+ );
+ }
+
+}
diff --git a/zmscitizenapi/src/Zmscitizenapi/Services/Core/ValidationService.php b/zmscitizenapi/src/Zmscitizenapi/Services/Core/ValidationService.php
new file mode 100644
index 000000000..f7055ced4
--- /dev/null
+++ b/zmscitizenapi/src/Zmscitizenapi/Services/Core/ValidationService.php
@@ -0,0 +1,417 @@
+ [ErrorMessages::get('invalidRequest')]];
+ }
+
+ if ($request->getMethod() !== "GET") {
+ return ['errors' => [ErrorMessages::get('invalidRequest')]];
+ }
+
+ return [];
+ }
+
+ public static function validateServerPostRequest(?ServerRequestInterface $request): array
+ {
+ if (!$request instanceof ServerRequestInterface) {
+ return ['errors' => [ErrorMessages::get('invalidRequest')]];
+ }
+
+ if ($request->getMethod() !== "POST") {
+ return ['errors' => [ErrorMessages::get('invalidRequest')]];
+ }
+
+ if ($request->getParsedBody() === null) {
+ return ['errors' => [ErrorMessages::get('invalidRequest')]];
+ }
+
+ return [];
+ }
+
+ public static function validateServiceLocationCombination(int $officeId, array $serviceIds): array
+ {
+ if ($officeId <= 0) {
+ return ['errors' => [ErrorMessages::get('invalidOfficeId')]];
+ }
+
+ if (empty($serviceIds) || !self::isValidNumericArray($serviceIds)) {
+ return ['errors' => [ErrorMessages::get('invalidServiceId')]];
+ }
+
+ $availableServices = ZmsApiFacadeService::getServicesProvidedAtOffice($officeId);
+ $availableServiceIds = [];
+ foreach ($availableServices as $service) {
+ $availableServiceIds[] = $service->id;
+ }
+
+ $invalidServiceIds = array_diff($serviceIds, $availableServiceIds);
+
+ return empty($invalidServiceIds)
+ ? []
+ : ['errors' => [ErrorMessages::get('invalidLocationAndServiceCombination')]];
+ }
+
+ public static function validateGetBookableFreeDays(
+ ?int $officeId,
+ ?int $serviceId,
+ ?string $startDate,
+ ?string $endDate,
+ ?array $serviceCounts
+ ): array {
+ $errors = [];
+
+ if (!self::isValidOfficeId($officeId)) {
+ $errors[] = ErrorMessages::get('invalidOfficeId');
+ }
+
+ if (!self::isValidServiceId($serviceId)) {
+ $errors[] = ErrorMessages::get('invalidServiceId');
+ }
+
+ if (!$startDate || !self::isValidDate($startDate)) {
+ $errors[] = ErrorMessages::get('invalidStartDate');
+ }
+
+ if (!$endDate || !self::isValidDate($endDate)) {
+ $errors[] = ErrorMessages::get('invalidEndDate');
+ }
+
+ if ($startDate && $endDate && self::isValidDate($startDate) && self::isValidDate($endDate)) {
+ if (new DateTime($startDate) > new DateTime($endDate)) {
+ $errors[] = ErrorMessages::get('startDateAfterEndDate');
+ }
+
+ if (!self::isDateRangeValid($startDate, $endDate)) {
+ $errors[] = ErrorMessages::get('dateRangeTooLarge');
+ }
+ }
+
+ if (!self::isValidServiceCounts($serviceCounts)) {
+ $errors[] = ErrorMessages::get('invalidServiceCount');
+ }
+
+ return ['errors' => $errors];
+ }
+
+ public static function validateGetProcessById(?int $processId, ?string $authKey): array
+ {
+ $errors = [];
+
+ if (!self::isValidProcessId($processId)) {
+ $errors[] = ErrorMessages::get('invalidProcessId');
+ }
+
+ if (!self::isValidAuthKey($authKey)) {
+ $errors[] = ErrorMessages::get('invalidAuthKey');
+ }
+
+ return ['errors' => $errors];
+ }
+
+ public static function validateGetAvailableAppointments(
+ ?string $date,
+ ?int $officeId,
+ ?array $serviceIds,
+ ?array $serviceCounts
+ ): array {
+ $errors = [];
+
+ if (!$date || !self::isValidDate($date)) {
+ $errors[] = ErrorMessages::get('invalidDate');
+ }
+
+ if (!self::isValidOfficeId($officeId)) {
+ $errors[] = ErrorMessages::get('invalidOfficeId');
+ }
+
+ if (!self::isValidServiceIds($serviceIds)) {
+ $errors[] = ErrorMessages::get('invalidServiceId');
+ }
+
+ if (!self::isValidServiceCounts($serviceCounts)) {
+ $errors[] = ErrorMessages::get('invalidServiceCount');
+ }
+
+ return ['errors' => $errors];
+ }
+
+ public static function validatePostAppointmentReserve(
+ ?int $officeId,
+ ?array $serviceIds,
+ ?array $serviceCounts,
+ ?int $timestamp
+ ): array {
+ $errors = [];
+
+ if (!self::isValidOfficeId($officeId)) {
+ $errors[] = ErrorMessages::get('invalidOfficeId');
+ }
+
+ if (!self::isValidServiceIds($serviceIds)) {
+ $errors[] = ErrorMessages::get('invalidServiceId');
+ }
+
+ if (!self::isValidTimestamp($timestamp)) {
+ $errors[] = ErrorMessages::get('invalidTimestamp');
+ }
+
+ if (!self::isValidServiceCounts($serviceCounts)) {
+ $errors[] = ErrorMessages::get('invalidServiceCount');
+ }
+
+ return ['errors' => $errors];
+ }
+
+ public static function validateUpdateAppointmentInputs(
+ ?int $processId,
+ ?string $authKey,
+ ?string $familyName,
+ ?string $email,
+ ?string $telephone,
+ ?string $customTextfield
+ ): array {
+ $errors = [];
+
+ if (!self::isValidProcessId($processId)) {
+ $errors[] = ErrorMessages::get('invalidProcessId');
+ }
+
+ if (!self::isValidAuthKey($authKey)) {
+ $errors[] = ErrorMessages::get('invalidAuthKey');
+ }
+
+ if (!self::isValidFamilyName($familyName)) {
+ $errors[] = ErrorMessages::get('invalidFamilyName');
+ }
+
+ if (!self::isValidEmail($email)) {
+ $errors[] = ErrorMessages::get('invalidEmail');
+ }
+
+ if (!self::isValidTelephone($telephone)) {
+ $errors[] = ErrorMessages::get('invalidTelephone');
+ }
+
+ if (!self::isValidCustomTextfield($customTextfield)) {
+ $errors[] = ErrorMessages::get('invalidCustomTextfield');
+ }
+
+ return ['errors' => $errors];
+ }
+
+ public static function validateGetScopeById(?int $scopeId): array
+ {
+ return !self::isValidScopeId($scopeId)
+ ? ['errors' => [ErrorMessages::get('invalidScopeId')]]
+ : [];
+ }
+
+ public static function validateGetServicesByOfficeId(?int $officeId): array
+ {
+ return !self::isValidOfficeId($officeId)
+ ? ['errors' => [ErrorMessages::get('invalidOfficeId')]]
+ : [];
+ }
+
+ public static function validateGetOfficeListByServiceId(?int $serviceId): array
+ {
+ return !self::isValidServiceId($serviceId)
+ ? ['errors' => [ErrorMessages::get('invalidServiceId')]]
+ : [];
+ }
+
+ public static function validateGetProcessFreeSlots(?ProcessList $freeSlots): array
+ {
+ return empty($freeSlots) || !is_iterable($freeSlots)
+ ? ['errors' => [ErrorMessages::get('appointmentNotAvailable')]]
+ : [];
+ }
+
+ public static function validateGetProcessByIdTimestamps(?array $appointmentTimestamps): array
+ {
+ return empty($appointmentTimestamps)
+ ? ['errors' => [ErrorMessages::get('appointmentNotAvailable')]]
+ : [];
+ }
+
+ public static function validateGetProcessNotFound(?Process $process): array
+ {
+ return !$process
+ ? ['errors' => [ErrorMessages::get('appointmentNotAvailable')]]
+ : [];
+ }
+
+ public static function validateScopesNotFound(?ScopeList $scopes): array
+ {
+ return empty($scopes) || $scopes === null || $scopes->count() === 0
+ ? ['errors' => [ErrorMessages::get('scopesNotFound')]]
+ : [];
+ }
+
+ public static function validateServicesNotFound(?array $services): array
+ {
+ return empty($services)
+ ? ['errors' => [ErrorMessages::get('servicesNotFound')]]
+ : [];
+ }
+
+ public static function validateOfficesNotFound(?array $offices): array
+ {
+ return empty($offices)
+ ? ['errors' => [ErrorMessages::get('officesNotFound')]]
+ : [];
+ }
+
+ public static function validateAppointmentDaysNotFound(?array $formattedDays): array
+ {
+ return empty($formattedDays)
+ ? ['errors' => [ErrorMessages::get('noAppointmentForThisDay')]]
+ : [];
+ }
+
+ public static function validateNoAppointmentsAtLocation(): array
+ {
+ return ['errors' => [ErrorMessages::get('noAppointmentsAtLocation')]];
+ }
+
+ public static function validateServiceArrays(array $serviceIds, array $serviceCounts): array
+ {
+ $errors = [];
+
+ if (empty($serviceIds) || empty($serviceCounts)) {
+ $errors[] = ErrorMessages::get('emptyServiceArrays');
+ }
+
+ if (count($serviceIds) !== count($serviceCounts)) {
+ $errors[] = ErrorMessages::get('mismatchedArrays');
+ }
+
+ foreach ($serviceIds as $id) {
+ if (!is_numeric($id)) {
+ $errors[] = ErrorMessages::get('invalidServiceId');
+ break;
+ }
+ }
+
+ foreach ($serviceCounts as $count) {
+ if (!is_numeric($count) || $count < 0) {
+ $errors[] = ErrorMessages::get('invalidServiceCount');
+ break;
+ }
+ }
+
+ return $errors;
+ }
+
+ /* Helper methods for validation */
+ private static function isValidDate(string $date): bool
+ {
+ $dateTime = DateTime::createFromFormat(self::DATE_FORMAT, $date);
+ return $dateTime && $dateTime->format(self::DATE_FORMAT) === $date;
+ }
+
+ private static function isDateRangeValid(string $startDate, string $endDate): bool
+ {
+ $start = new DateTime($startDate);
+ $end = new DateTime($endDate);
+ $diff = $start->diff($end);
+
+ return $diff->days <= self::MAX_FUTURE_DAYS;
+ }
+
+ private static function isValidNumericArray(array $array): bool
+ {
+ return !empty($array) && array_filter($array, 'is_numeric') === $array;
+ }
+
+ private static function isValidOfficeId(?int $officeId): bool
+ {
+ return !empty($officeId) && $officeId > 0;
+ }
+
+ private static function isValidServiceId(?int $serviceId): bool
+ {
+ return !empty($serviceId) && $serviceId > 0;
+ }
+
+ private static function isValidScopeId(?int $scopeId): bool
+ {
+ return !empty($scopeId) && $scopeId > 0;
+ }
+
+ private static function isValidProcessId(?int $processId): bool
+ {
+ return !empty($processId) && $processId >= self::MIN_PROCESS_ID;
+ }
+
+ private static function isValidAuthKey(?string $authKey): bool
+ {
+ return !empty($authKey) && is_string($authKey) && strlen(trim($authKey)) > 0;
+ }
+
+ private static function isValidServiceIds(?array $serviceIds): bool
+ {
+ return !empty($serviceIds) && self::isValidNumericArray($serviceIds);
+ }
+
+ private static function isValidServiceCounts(?array $serviceCounts): bool
+ {
+ if (empty($serviceCounts) || !is_array($serviceCounts)) {
+ return false;
+ }
+
+ foreach ($serviceCounts as $count) {
+ if (!is_numeric($count) || $count < 0 || !preg_match(self::SERVICE_COUNT_PATTERN, (string)$count)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ private static function isValidTimestamp(?int $timestamp): bool
+ {
+ return !empty($timestamp) && is_numeric($timestamp) && $timestamp > time();
+ }
+
+ private static function isValidEmail(?string $email): bool
+ {
+ return !empty($email) && filter_var($email, FILTER_VALIDATE_EMAIL) !== false;
+ }
+
+ private static function isValidTelephone(?string $telephone): bool
+ {
+ return $telephone === null || preg_match(self::PHONE_PATTERN, $telephone);
+ }
+
+ private static function isValidFamilyName(?string $familyName): bool
+ {
+ return !empty($familyName) && is_string($familyName) && strlen(trim($familyName)) > 0;
+ }
+
+ private static function isValidCustomTextfield(?string $customTextfield): bool
+ {
+ return $customTextfield === null || (is_string($customTextfield) && strlen(trim($customTextfield)) > 0);
+ }
+}
\ No newline at end of file
diff --git a/zmscitizenapi/src/Zmscitizenapi/Services/Core/ZmsApiClientService.php b/zmscitizenapi/src/Zmscitizenapi/Services/Core/ZmsApiClientService.php
new file mode 100644
index 000000000..de3dfeaf8
--- /dev/null
+++ b/zmscitizenapi/src/Zmscitizenapi/Services/Core/ZmsApiClientService.php
@@ -0,0 +1,331 @@
+get($cacheKey))) {
+ return $data;
+ }
+
+ try {
+ $result = \App::$http->readGetResult('/source/' . \App::$source_name . '/', [
+ 'resolveReferences' => 2,
+ ]);
+
+ $entity = $result?->getEntity();
+ if (!$entity instanceof Source) {
+ return new Source();
+ }
+
+ if (\App::$cache) {
+ \App::$cache->set($cacheKey, $entity, 3600);
+ LoggerService::logInfo('Cache set', [
+ 'key' => $cacheKey,
+ 'ttl' => 3600,
+ 'entity_type' => get_class($entity)
+ ]);
+ }
+
+ return $entity;
+ } catch (\Exception $e) {
+ ExceptionService::handleException($e, __FUNCTION__);
+ }
+ }
+
+
+ public static function getOffices(): ProviderList
+ {
+ try {
+ $sources = self::fetchSourceData();
+ $list = $sources?->getProviderList();
+ if (!$list instanceof ProviderList) {
+ return new ProviderList();
+ }
+ return $list;
+ } catch (\Exception $e) {
+ ExceptionService::handleException($e, __FUNCTION__);
+ }
+ }
+
+ public static function getScopes(): ScopeList
+ {
+ try {
+ $sources = self::fetchSourceData();
+ $list = $sources?->getScopeList();
+ if (!$list instanceof ScopeList) {
+ return new ScopeList();
+ }
+ return $list;
+ } catch (\Exception $e) {
+ ExceptionService::handleException($e, __FUNCTION__);
+ }
+ }
+
+ public static function getServices(): RequestList
+ {
+ try {
+ $sources = self::fetchSourceData();
+ $list = $sources?->getRequestList();
+ if (!$list instanceof RequestList) {
+ return new RequestList();
+ }
+ return $list;
+ } catch (\Exception $e) {
+ ExceptionService::handleException($e, __FUNCTION__);
+ }
+ }
+
+ public static function getRequestRelationList(): RequestRelationList
+ {
+ try {
+ $sources = self::fetchSourceData();
+ $list = $sources?->getRequestRelationList();
+ if (!$list instanceof RequestRelationList) {
+ return new RequestRelationList();
+ }
+ return $list;
+ } catch (\Exception $e) {
+ ExceptionService::handleException($e, __FUNCTION__);
+ }
+ }
+
+ public static function getFreeDays(ProviderList $providers, RequestList $requests, array $firstDay, array $lastDay): Calendar
+ {
+ try {
+ $calendar = new Calendar();
+ $calendar->firstDay = $firstDay;
+ $calendar->lastDay = $lastDay;
+ $calendar->providers = $providers;
+ $calendar->requests = $requests;
+
+ $result = \App::$http->readPostResult('/calendar/', $calendar);
+ $entity = $result?->getEntity();
+ if (!$entity instanceof Calendar) {
+ return new Calendar();
+ }
+ return $entity;
+ } catch (\Exception $e) {
+ ExceptionService::handleException($e, __FUNCTION__);
+ }
+ }
+
+ public static function getFreeTimeslots(ProviderList $providers, RequestList $requests, array $firstDay, array $lastDay): ProcessList
+ {
+ try {
+ $calendar = new Calendar();
+ $calendar->firstDay = $firstDay;
+ $calendar->lastDay = $lastDay;
+ $calendar->providers = $providers;
+ $calendar->requests = $requests;
+
+ $result = \App::$http->readPostResult('/process/status/free/', $calendar);
+
+ $collection = $result?->getCollection();
+ if (!$collection instanceof ProcessList) {
+ return new ProcessList();
+ }
+
+ return $collection;
+ } catch (\Exception $e) {
+ ExceptionService::handleException($e, __FUNCTION__);
+ }
+ }
+
+ public static function reserveTimeslot(Process $appointmentProcess, array $serviceIds, array $serviceCounts): Process
+ {
+ try {
+ $requests = [];
+ foreach ($serviceIds as $index => $serviceId) {
+ $count = intval($serviceCounts[$index]);
+ for ($i = 0; $i < $count; $i++) {
+ $requests[] = [
+ 'id' => $serviceId,
+ 'source' => \App::$source_name
+ ];
+ }
+ }
+
+ $processEntity = new Process();
+ $processEntity->appointments = $appointmentProcess->appointments ?? [];
+ $processEntity->authKey = $appointmentProcess->authKey ?? null;
+ $processEntity->clients = $appointmentProcess->clients ?? [];
+ $processEntity->scope = $appointmentProcess->scope ?? null;
+ $processEntity->requests = $requests;
+ $processEntity->lastChange = $appointmentProcess->lastChange ?? time();
+ $processEntity->createIP = ClientIpHelper::getClientIp();
+ $processEntity->createTimestamp = time();
+
+ if (isset($appointmentProcess->queue)) {
+ $processEntity->queue = $appointmentProcess->queue;
+ }
+
+ $result = \App::$http->readPostResult('/process/status/reserved/', $processEntity);
+ $entity = $result?->getEntity();
+ if (!$entity instanceof Process) {
+ return new Process();
+ }
+ return $entity;
+ } catch (\Exception $e) {
+ ExceptionService::handleException($e, __FUNCTION__);
+ }
+ }
+
+ public static function submitClientData(Process $process): Process
+ {
+ try {
+ $url = "/process/{$process->id}/{$process->authKey}/";
+ $result = \App::$http->readPostResult($url, $process);
+ $entity = $result?->getEntity();
+ if (!$entity instanceof Process) {
+ return new Process();
+ }
+ return $entity;
+ } catch (\Exception $e) {
+ ExceptionService::handleException($e, __FUNCTION__);
+ }
+ }
+
+ public static function preconfirmProcess(Process $process): Process
+ {
+ try {
+ $url = '/process/status/preconfirmed/';
+ $result = \App::$http->readPostResult($url, $process);
+ $entity = $result?->getEntity();
+ if (!$entity instanceof Process) {
+ return new Process();
+ }
+ return $entity;
+ } catch (\Exception $e) {
+ ExceptionService::handleException($e, __FUNCTION__);
+ }
+ }
+
+ public static function confirmProcess(Process $process): Process
+ {
+ try {
+ $url = '/process/status/confirmed/';
+ $result = \App::$http->readPostResult($url, $process);
+ $entity = $result?->getEntity();
+ if (!$entity instanceof Process) {
+ return new Process();
+ }
+ return $entity;
+ } catch (\Exception $e) {
+ ExceptionService::handleException($e, __FUNCTION__);
+ }
+ }
+
+ public static function cancelAppointment(Process $process): Process
+ {
+ try {
+ $url = "/process/{$process->id}/{$process->authKey}/";
+ $result = \App::$http->readDeleteResult($url, [], null); // Changed to match test expectations
+ $entity = $result?->getEntity();
+ if (!$entity instanceof Process) {
+ return new Process();
+ }
+ return $entity;
+ } catch (\Exception $e) {
+ ExceptionService::handleException($e, __FUNCTION__);
+ }
+ }
+
+ public static function sendConfirmationEmail(Process $process): Process
+ {
+ try {
+ $url = "/process/{$process->id}/{$process->authKey}/confirmation/mail/";
+ $result = \App::$http->readPostResult($url, $process);
+ $entity = $result?->getEntity();
+ if (!$entity instanceof Process) {
+ return new Process();
+ }
+ return $entity;
+ } catch (\Exception $e) {
+ ExceptionService::handleException($e, __FUNCTION__);
+ }
+ }
+
+ public static function sendPreconfirmationEmail(Process $process): Process
+ {
+ try {
+ $url = "/process/{$process->id}/{$process->authKey}/preconfirmation/mail/";
+ $result = \App::$http->readPostResult($url, $process);
+ $entity = $result?->getEntity();
+ if (!$entity instanceof Process) {
+ return new Process();
+ }
+ return $entity;
+ } catch (\Exception $e) {
+ ExceptionService::handleException($e, __FUNCTION__);
+ }
+ }
+
+ public static function sendCancelationEmail(Process $process): Process
+ {
+ try {
+ $url = "/process/{$process->id}/{$process->authKey}/delete/mail/";
+ $result = \App::$http->readPostResult($url, $process);
+ $entity = $result?->getEntity();
+ if (!$entity instanceof Process) {
+ return new Process();
+ }
+ return $entity;
+ } catch (\Exception $e) {
+ ExceptionService::handleException($e, __FUNCTION__);
+ }
+ }
+
+ public static function getProcessById(int $processId, string $authKey): Process
+ {
+ try {
+ $resolveReferences = 2;
+ $result = \App::$http->readGetResult("/process/{$processId}/{$authKey}/", [
+ 'resolveReferences' => $resolveReferences
+ ]);
+ $entity = $result?->getEntity();
+ if (!$entity instanceof Process) {
+ return new Process();
+ }
+ return $entity;
+ } catch (\Exception $e) {
+ ExceptionService::handleException($e, __FUNCTION__);
+ }
+ }
+
+ public static function getScopesByProviderId(string $source, string|int $providerId): ScopeList
+ {
+ try {
+ $scopeList = self::getScopes();
+ if (!$scopeList instanceof ScopeList) {
+ return new ScopeList();
+ }
+ $result = $scopeList->withProviderID($source, (string) $providerId);
+ if (!$result instanceof ScopeList) {
+ return new ScopeList();
+ }
+ return $result;
+ } catch (\Exception $e) {
+ ExceptionService::handleException($e, __FUNCTION__);
+ }
+ }
+
+}
diff --git a/zmscitizenapi/src/Zmscitizenapi/Services/Core/ZmsApiFacadeService.php b/zmscitizenapi/src/Zmscitizenapi/Services/Core/ZmsApiFacadeService.php
new file mode 100644
index 000000000..c57454300
--- /dev/null
+++ b/zmscitizenapi/src/Zmscitizenapi/Services/Core/ZmsApiFacadeService.php
@@ -0,0 +1,949 @@
+getProvider()) {
+ $scopeMap[$scope->getProvider()->source . '_' . $scope->getProvider()->id] = $scope;
+ }
+ }
+
+ foreach ($providerList as $provider) {
+ $matchingScope = $scopeMap[$provider->source . '_' . $provider->id] ?? null;
+
+ $offices[] = new Office(
+ id: (int) $provider->id,
+ name: $provider->displayName ?? $provider->name,
+ address: $provider->data['address'] ?? null,
+ geo: $provider->data['geo'] ?? null,
+ scope: $matchingScope ? new ThinnedScope(
+ id: (int) $matchingScope->id,
+ provider: MapperService::providerToThinnedProvider($provider),
+ shortName: $matchingScope->getShortName(),
+ telephoneActivated: (bool) $matchingScope->getTelephoneActivated(),
+ telephoneRequired: (bool) $matchingScope->getTelephoneRequired(),
+ customTextfieldActivated: (bool) $matchingScope->getCustomTextfieldActivated(),
+ customTextfieldRequired: (bool) $matchingScope->getCustomTextfieldRequired(),
+ customTextfieldLabel: $matchingScope->getCustomTextfieldLabel(),
+ captchaActivatedRequired: (bool) $matchingScope->getCaptchaActivatedRequired(),
+ displayInfo: $matchingScope->getDisplayInfo()
+ ) : null
+ );
+ }
+
+ return new OfficeList($offices);
+ } catch (\RuntimeException $e) {
+ if (strpos($e->getMessage(), 'officesNotFound') !== false) {
+ return ExceptionService::officesNotFound();
+ }
+ if (strpos($e->getMessage(), 'scopeNotFound') !== false) {
+ return ExceptionService::scopeNotFound();
+ }
+ return ExceptionService::internalError();
+ }
+ }
+
+ public static function getScopes(): ThinnedScopeList|array
+ {
+ try {
+ $providerList = ZmsApiClientService::getOffices() ?? new ProviderList();
+ $scopeList = ZmsApiClientService::getScopes() ?? new ScopeList();
+
+ $scopeMap = [];
+ foreach ($scopeList as $scope) {
+ $scopeProvider = $scope->getProvider();
+ if ($scopeProvider && $scopeProvider->id && $scopeProvider->source) {
+ $key = $scopeProvider->source . '_' . $scopeProvider->id;
+ $scopeMap[$key] = $scope;
+ }
+ }
+
+ $scopesProjectionList = [];
+ foreach ($providerList as $provider) {
+ $key = $provider->source . '_' . $provider->id;
+ if (isset($scopeMap[$key])) {
+ $matchingScope = $scopeMap[$key];
+ $scopesProjectionList[] = new ThinnedScope(
+ id: (int) $matchingScope->id,
+ provider: MapperService::providerToThinnedProvider($provider),
+ shortName: $matchingScope->getShortName(),
+ telephoneActivated: (bool) $matchingScope->getTelephoneActivated(),
+ telephoneRequired: (bool) $matchingScope->getTelephoneRequired(),
+ customTextfieldActivated: (bool) $matchingScope->getCustomTextfieldActivated(),
+ customTextfieldRequired: (bool) $matchingScope->getCustomTextfieldRequired(),
+ customTextfieldLabel: $matchingScope->getCustomTextfieldLabel(),
+ captchaActivatedRequired: (bool) $matchingScope->getCaptchaActivatedRequired(),
+ displayInfo: $matchingScope->getDisplayInfo()
+ );
+ }
+ }
+
+ return new ThinnedScopeList($scopesProjectionList);
+ } catch (\RuntimeException $e) {
+ if (strpos($e->getMessage(), 'officesNotFound') !== false) {
+ return ExceptionService::officesNotFound();
+ }
+ if (strpos($e->getMessage(), 'scopeNotFound') !== false) {
+ return ExceptionService::scopeNotFound();
+ }
+ return ExceptionService::internalError();
+ }
+ }
+
+ public static function getServices(): ServiceList|array
+ {
+ try {
+ $requestList = ZmsApiClientService::getServices() ?? new RequestList();
+ $services = [];
+
+ foreach ($requestList as $request) {
+ $additionalData = $request->getAdditionalData();
+
+ $services[] = new Service(
+ id: (int) $request->getId(),
+ name: $request->getName(),
+ maxQuantity: $additionalData['maxQuantity'] ?? 1
+ );
+ }
+
+ return new ServiceList($services);
+ } catch (\RuntimeException $e) {
+ return ExceptionService::servicesNotFound();
+ }
+ }
+
+ public static function getServicesAndOffices(): OfficeServiceAndRelationList|array
+ {
+ try {
+ $providerList = ZmsApiClientService::getOffices() ?? new ProviderList();
+ $requestList = ZmsApiClientService::getServices() ?? new RequestList();
+ $relationList = ZmsApiClientService::getRequestRelationList() ?? new RequestRelationList();
+
+ $offices = MapperService::mapOfficesWithScope($providerList) ?? new OfficeList;
+ $services = MapperService::mapServicesWithCombinations($requestList, $relationList) ?? new ServiceList();
+ $relations = MapperService::mapRelations($relationList) ?? new OfficeServiceRelationList();
+
+ return new OfficeServiceAndRelationList($offices, $services, $relations);
+ } catch (\RuntimeException $e) {
+ if (strpos($e->getMessage(), 'officesNotFound') !== false) {
+ return ExceptionService::officesNotFound();
+ }
+ if (strpos($e->getMessage(), 'scopeNotFound') !== false) {
+ return ExceptionService::scopeNotFound();
+ }
+ if (strpos($e->getMessage(), 'servicesNotFound') !== false) {
+ return ExceptionService::servicesNotFound();
+ }
+ return ExceptionService::internalError();
+ }
+ }
+
+ /* Todo add method
+ * getCombinableServicesByIds
+ *
+ *
+ *
+ */
+
+ public static function getScopeByOfficeId(int $officeId): ThinnedScope|array
+ {
+ try {
+ $matchingScope = ZmsApiClientService::getScopesByProviderId(
+ \App::$source_name,
+ $officeId
+ )->getIterator()->current();
+
+ if (!$matchingScope instanceof Scope) {
+ return ExceptionService::scopeNotFound();
+ }
+
+ $providerList = ZmsApiClientService::getOffices() ?? new ProviderList();
+
+ $providerMap = [];
+ foreach ($providerList as $provider) {
+ $key = $provider->source . '_' . $provider->id;
+ $providerMap[$key] = $provider;
+ }
+
+ $scopeProvider = $matchingScope->getProvider();
+ $providerKey = $scopeProvider ? ($scopeProvider->source . '_' . $scopeProvider->id) : null;
+
+ $finalProvider = $providerKey && isset($providerMap[$providerKey])
+ ? $providerMap[$providerKey]
+ : $scopeProvider;
+
+ $result = [
+ 'id' => $matchingScope->id,
+ 'provider' => MapperService::providerToThinnedProvider($finalProvider) ?? null,
+ 'shortName' => $matchingScope->getShortName() ?? null,
+ 'telephoneActivated' => (bool) $matchingScope->getTelephoneActivated() ?? null,
+ 'telephoneRequired' => (bool) $matchingScope->getTelephoneRequired() ?? null,
+ 'customTextfieldActivated' => (bool) $matchingScope->getCustomTextfieldActivated() ?? null,
+ 'customTextfieldRequired' => (bool) $matchingScope->getCustomTextfieldRequired() ?? null,
+ 'customTextfieldLabel' => $matchingScope->getCustomTextfieldLabel() ?? null,
+ 'captchaActivatedRequired' => (bool) $matchingScope->getCaptchaActivatedRequired() ?? null,
+ 'displayInfo' => $matchingScope->getDisplayInfo() ?? null,
+ ];
+
+ return new ThinnedScope(
+ id: (int) $result['id'],
+ provider: $result['provider'],
+ shortName: $result['shortName'],
+ telephoneActivated: $result['telephoneActivated'],
+ telephoneRequired: $result['telephoneRequired'],
+ customTextfieldActivated: $result['customTextfieldActivated'],
+ customTextfieldRequired: $result['customTextfieldRequired'],
+ customTextfieldLabel: $result['customTextfieldLabel'],
+ captchaActivatedRequired: $result['captchaActivatedRequired'],
+ displayInfo: $result['displayInfo']
+ );
+ } catch (\RuntimeException $e) {
+ if (strpos($e->getMessage(), 'officesNotFound') !== false) {
+ return ExceptionService::officesNotFound();
+ }
+ if (strpos($e->getMessage(), 'scopeNotFound') !== false) {
+ return ExceptionService::scopeNotFound();
+ }
+ return ExceptionService::internalError();
+ }
+ }
+
+ /* Todo add method
+ * getOfficeById
+ *
+ *
+ *
+ */
+
+ public static function getOfficeListByServiceId(int $serviceId): OfficeList|array
+ {
+ try {
+
+ $providerList = ZmsApiClientService::getOffices() ?? new ProviderList();
+ $requestRelationList = ZmsApiClientService::getRequestRelationList() ?? new RequestRelationList();
+
+ $providerMap = [];
+ foreach ($providerList as $provider) {
+ $providerMap[$provider->id] = $provider;
+ }
+
+ $offices = [];
+ foreach ($requestRelationList as $relation) {
+ if ((int) $relation->request->id === $serviceId) {
+ $providerId = $relation->provider->id;
+
+ if (!isset($providerMap[$providerId])) {
+ continue;
+ }
+
+ $provider = $providerMap[$providerId];
+ $scope = null;
+
+ $scopeData = self::getScopeByOfficeId((int) $provider->id);
+ if ($scopeData instanceof ThinnedScope) {
+ $scope = $scopeData;
+ }
+
+ $offices[] = new Office(
+ id: (int) $provider->id,
+ name: $provider->name,
+ address: $provider->address ?? null,
+ geo: $provider->geo ?? null,
+ scope: $scope
+ );
+ }
+ }
+
+ $errors = ValidationService::validateOfficesNotFound($offices);
+ if (is_array($errors) && !empty($errors['errors'])) {
+ return $errors;
+ }
+
+ return new OfficeList($offices);
+
+ } catch (\RuntimeException $e) {
+ if (strpos($e->getMessage(), 'officesNotFound') !== false) {
+ return ExceptionService::officesNotFound();
+ }
+ if (strpos($e->getMessage(), 'scopeNotFound') !== false) {
+ return ExceptionService::scopeNotFound();
+ }
+ return ExceptionService::internalError();
+ }
+ }
+
+ public static function getScopeById(?int $scopeId): ThinnedScope|array
+ {
+ try {
+
+ $scopeList = ZmsApiClientService::getScopes() ?? new ScopeList();
+ $providerList = ZmsApiClientService::getOffices() ?? new ProviderList();
+
+ $matchingScope = null;
+ foreach ($scopeList as $scope) {
+ if ((int) $scope->id === (int) $scopeId) {
+ $matchingScope = $scope;
+ break;
+ }
+ }
+
+ $tempScopeList = new ScopeList();
+ if ($matchingScope !== null) {
+ $tempScopeList->addEntity($matchingScope);
+ }
+ $errors = ValidationService::validateScopesNotFound($tempScopeList);
+ if (is_array($errors) && !empty($errors['errors'])) {
+ return $errors;
+ }
+
+ $providerMap = [];
+ foreach ($providerList as $provider) {
+ $key = $provider->source . '_' . $provider->id;
+ $providerMap[$key] = $provider;
+ }
+
+ $scopeProvider = $matchingScope->getProvider();
+ $providerKey = $scopeProvider ? ($scopeProvider->source . '_' . $scopeProvider->id) : null;
+ $matchingProv = ($providerKey && isset($providerMap[$providerKey]))
+ ? $providerMap[$providerKey]
+ : $scopeProvider;
+
+ return new ThinnedScope(
+ id: (int) $matchingScope->id,
+ provider: MapperService::providerToThinnedProvider($matchingProv),
+ shortName: $matchingScope->getShortName() ?? null,
+ telephoneActivated: (bool) $matchingScope->getTelephoneActivated() ?? null,
+ telephoneRequired: (bool) $matchingScope->getTelephoneRequired() ?? null,
+ customTextfieldActivated: (bool) $matchingScope->getCustomTextfieldActivated() ?? null,
+ customTextfieldRequired: (bool) $matchingScope->getCustomTextfieldRequired() ?? null,
+ customTextfieldLabel: $matchingScope->getCustomTextfieldLabel() ?? null,
+ captchaActivatedRequired: (bool) $matchingScope->getCaptchaActivatedRequired() ?? null,
+ displayInfo: $matchingScope->getDisplayInfo() ?? null
+ );
+ } catch (\RuntimeException $e) {
+ if (strpos($e->getMessage(), 'officesNotFound') !== false) {
+ return ExceptionService::officesNotFound();
+ }
+ if (strpos($e->getMessage(), 'scopeNotFound') !== false) {
+ return ExceptionService::scopeNotFound();
+ }
+ return ExceptionService::internalError();
+ }
+ }
+
+ public static function getServicesByOfficeId(int $officeId): ServiceList|array
+ {
+ try {
+ $requestList = ZmsApiClientService::getServices() ?? new RequestList();
+ $requestRelationList = ZmsApiClientService::getRequestRelationList() ?? new RequestRelationList();
+
+ $requestMap = [];
+ foreach ($requestList as $request) {
+ $requestMap[$request->id] = $request;
+ }
+
+ $services = [];
+ foreach ($requestRelationList as $relation) {
+ if ((int) $relation->provider->id === $officeId) {
+ $requestId = $relation->request->id;
+
+ if (isset($requestMap[$requestId])) {
+ $request = $requestMap[$requestId];
+ $services[] = new Service(
+ id: (int) $request->id,
+ name: $request->name,
+ maxQuantity: $request->getAdditionalData()['maxQuantity'] ?? 1
+ );
+ }
+ }
+ }
+
+ $errors = ValidationService::validateServicesNotFound($services);
+ if (is_array($errors) && !empty($errors['errors'])) {
+ return $errors;
+ }
+
+ return new ServiceList($services);
+ } catch (\RuntimeException $e) {
+ return ExceptionService::servicesNotFound();
+ }
+ }
+
+ public static function getOfficesThatProvideService(int $serviceId): OfficeList|array
+ {
+ try {
+ $providerList = ZmsApiClientService::getOffices() ?? new ProviderList();
+ $requestRelationList = ZmsApiClientService::getRequestRelationList() ?? new RequestRelationList();
+
+ $providerIds = [];
+ foreach ($requestRelationList as $relation) {
+ if ((int) $relation->request->id === $serviceId) {
+ $providerIds[] = $relation->provider->id;
+ }
+ }
+
+ $offices = [];
+ foreach ($providerList as $provider) {
+ if (in_array($provider->id, $providerIds) &&
+ isset($provider->data['public']) &&
+ $provider->data['public'] === true
+ ) {
+ $scope = self::getScopeByOfficeId((int) $provider->id);
+ if (!is_array($scope)) {
+ $offices[] = new Office(
+ id: (int) $provider->id,
+ name: $provider->displayName ?? $provider->name,
+ address: $provider->data['address'] ?? null,
+ geo: $provider->data['geo'] ?? null,
+ scope: $scope instanceof ThinnedScope ? $scope : null
+ );
+ }
+ }
+ }
+
+ $errors = ValidationService::validateOfficesNotFound($offices);
+ if (is_array($errors) && !empty($errors['errors'])) {
+ return $errors;
+ }
+
+ return new OfficeList($offices);
+
+ } catch (\RuntimeException $e) {
+ if (strpos($e->getMessage(), 'officesNotFound') !== false) {
+ return ExceptionService::officesNotFound();
+ }
+ if (strpos($e->getMessage(), 'scopeNotFound') !== false) {
+ return ExceptionService::scopeNotFound();
+ }
+ return ExceptionService::internalError();
+ }
+ }
+
+ public static function getServicesProvidedAtOffice(int $officeId): RequestList|array
+ {
+ try {
+ $requestRelationList = ZmsApiClientService::getRequestRelationList() ?? new RequestRelationList();
+
+ $requestRelationArray = [];
+ foreach ($requestRelationList as $relation) {
+ $requestRelationArray[] = $relation;
+ }
+
+ $serviceIds = array_filter($requestRelationArray, function ($relation) use ($officeId) {
+ return $relation->provider->id === $officeId || (string) $relation->provider->id === (string) $officeId;
+ });
+
+ $serviceIds = array_map(function ($relation) {
+ return $relation->request->id;
+ }, $serviceIds);
+
+ $requestList = ZmsApiClientService::getServices() ?? new RequestList();
+ $requestArray = [];
+ foreach ($requestList as $request) {
+ $requestArray[] = $request;
+ }
+
+ $filteredRequests = array_filter($requestArray, function ($request) use ($serviceIds) {
+ return in_array($request->id, $serviceIds);
+ });
+
+ $resultRequestList = new RequestList();
+ foreach ($filteredRequests as $request) {
+ $resultRequestList->addEntity($request);
+ }
+
+ return $resultRequestList;
+ } catch (\RuntimeException $e) {
+ return ExceptionService::servicesNotFound();
+ }
+ }
+
+ public static function getBookableFreeDays(int $officeId, int $serviceId, array $serviceCounts, string $startDate, string $endDate): AvailableDays|array
+ {
+ try {
+ $firstDay = DateTimeFormatHelper::getInternalDateFromISO($startDate);
+ $lastDay = DateTimeFormatHelper::getInternalDateFromISO($endDate);
+
+ $freeDays = ZmsApiClientService::getFreeDays(
+ new ProviderList([['id' => $officeId, 'source' => \App::$source_name]]),
+ new RequestList([
+ [
+ 'id' => $serviceId,
+ 'source' => \App::$source_name,
+ 'slotCount' => $serviceCounts,
+ ]
+ ]),
+ $firstDay,
+ $lastDay,
+ ) ?? new Calendar();
+
+ $daysCollection = $freeDays->days;
+ $formattedDays = [];
+ foreach ($daysCollection as $day) {
+ $formattedDays[] = sprintf('%04d-%02d-%02d', $day->year, $day->month, $day->day);
+ }
+
+ $errors = ValidationService::validateAppointmentDaysNotFound($formattedDays);
+ if (is_array($errors) && !empty($errors['errors'])) {
+ return $errors;
+ }
+
+ return new AvailableDays($formattedDays);
+ } catch (\RuntimeException $e) {
+ if (strpos($e->getMessage(), 'noAppointmentsAtLocation') !== false) {
+ return ExceptionService::noAppointmentsAtLocation();
+ }
+ return ExceptionService::internalError();
+ }
+ }
+
+ public static function getFreeAppointments(
+ int $officeId,
+ array $serviceIds,
+ array $serviceCounts,
+ array $date
+ ): ProcessList|array {
+ try {
+ $office = [
+ 'id' => $officeId,
+ 'source' => \App::$source_name
+ ];
+
+ $requests = [];
+ foreach ($serviceIds as $index => $serviceId) {
+ $service = [
+ 'id' => $serviceId,
+ 'source' => \App::$source_name,
+ 'slotCount' => $serviceCounts[$index]
+ ];
+ $requests = array_merge($requests, array_fill(0, $service['slotCount'], $service));
+ }
+
+ return ZmsApiClientService::getFreeTimeslots(
+ new ProviderList([$office]),
+ new RequestList($requests),
+ $date,
+ $date
+ );
+ } catch (\RuntimeException $e) {
+ if (strpos($e->getMessage(), 'noAppointmentsAtLocation') !== false) {
+ return ExceptionService::noAppointmentsAtLocation();
+ }
+ return ExceptionService::internalError();
+ }
+ }
+
+ public static function getAvailableAppointments(
+ ?string $date,
+ ?int $officeId,
+ ?array $serviceIds,
+ ?array $serviceCounts
+ ): AvailableAppointments|array {
+
+ try {
+ $requests = [];
+ foreach ($serviceIds as $index => $serviceId) {
+ $slotCount = isset($serviceCounts[$index]) ? intval($serviceCounts[$index]) : 1;
+ for ($i = 0; $i < $slotCount; $i++) {
+ $requests[] = [
+ 'id' => $serviceId,
+ 'source' => \App::$source_name,
+ 'slotCount' => 1,
+ ];
+ }
+ }
+
+ $freeSlots = ZmsApiClientService::getFreeTimeslots(
+ new ProviderList([['id' => $officeId, 'source' => \App::$source_name]]),
+ new RequestList($requests),
+ DateTimeFormatHelper::getInternalDateFromISO($date),
+ DateTimeFormatHelper::getInternalDateFromISO($date)
+ ) ?? new ProcessList();
+
+ $timestamps = self::processFreeSlots($freeSlots);
+ if (!empty($timestamps['errors'])) {
+ return $timestamps;
+ }
+
+ return isset($timestamps->toArray()['appointmentTimestamps'])
+ ? new AvailableAppointments($timestamps->toArray()['appointmentTimestamps'])
+ : new AvailableAppointments();
+
+ } catch (\RuntimeException $e) {
+ if (strpos($e->getMessage(), 'noAppointmentsAtLocation') !== false) {
+ return ExceptionService::noAppointmentsAtLocation();
+ }
+ return ExceptionService::internalError();
+ }
+ }
+
+ private static function processFreeSlots(ProcessList $freeSlots): ProcessFreeSlots|array
+ {
+
+ $errors = ValidationService::validateGetProcessFreeSlots($freeSlots);
+ if (is_array($errors) && !empty($errors['errors'])) {
+ return $errors;
+ }
+
+ $currentTimestamp = time();
+
+ $appointmentTimestamps = array_reduce(
+ iterator_to_array($freeSlots),
+ function ($timestamps, $slot) use ($currentTimestamp) {
+ if (isset($slot->appointments) && is_iterable($slot->appointments)) {
+ foreach ($slot->appointments as $appointment) {
+ if (isset($appointment->date)) {
+ $timestamp = (int) $appointment->date;
+ if ($timestamp > $currentTimestamp) {
+ $timestamps[$timestamp] = true;
+ }
+ }
+ }
+ }
+ return $timestamps;
+ },
+ []
+ );
+
+ $appointmentTimestamps = array_keys($appointmentTimestamps);
+ sort($appointmentTimestamps);
+
+ $errors = ValidationService::validateGetProcessByIdTimestamps($appointmentTimestamps);
+ if (is_array($errors) && !empty($errors['errors'])) {
+ return $errors;
+ }
+
+ return new ProcessFreeSlots($appointmentTimestamps);
+ }
+
+ public static function reserveTimeslot(Process $appointmentProcess, array $serviceIds, array $serviceCounts): ThinnedProcess|array
+ {
+ try {
+ $errors = ValidationService::validateServiceArrays($serviceIds, $serviceCounts);
+ if (!empty($errors)) {
+ return $errors;
+ }
+ $process = ZmsApiClientService::reserveTimeslot($appointmentProcess, $serviceIds, $serviceCounts);
+ return MapperService::processToThinnedProcess($process);
+ } catch (\RuntimeException $e) {
+ if (strpos($e->getMessage(), 'processAlreadyExists') !== false) {
+ return ExceptionService::processAlreadyExists();
+ }
+ if (strpos($e->getMessage(), 'emailIsRequired') !== false) {
+ return ExceptionService::emailIsRequired();
+ }
+ if (strpos($e->getMessage(), 'telephoneIsRequired') !== false) {
+ return ExceptionService::telephoneIsRequired();
+ }
+ return ExceptionService::internalError();
+ }
+ }
+
+ public static function getThinnedProcessById(?int $processId, ?string $authKey): ThinnedProcess|array
+ {
+
+ try {
+
+ $process = ZmsApiClientService::getProcessById($processId, $authKey);
+
+ $thinnedProcess = MapperService::processToThinnedProcess($process);
+
+ $errors = ValidationService::validateGetProcessNotFound($process);
+ if (is_array($errors) && !empty($errors['errors'])) {
+ return $errors;
+ }
+
+ $providerList = ZmsApiClientService::getOffices() ?? new ProviderList();
+
+ $providerMap = [];
+ foreach ($providerList as $provider) {
+ $key = $provider->getSource() . '_' . $provider->id;
+ $providerMap[$key] = $provider;
+ }
+
+ $thinnedScope = null;
+ if ($process->scope instanceof Scope) {
+ $scopeProvider = $process->scope->getProvider();
+ $providerKey = $scopeProvider ? ($scopeProvider->getSource() . '_' . $scopeProvider->id) : null;
+ $matchingProvider = $providerKey && isset($providerMap[$providerKey]) ? $providerMap[$providerKey] : $scopeProvider;
+
+ $thinnedProvider = MapperService::providerToThinnedProvider($matchingProvider);
+
+
+ $thinnedScope = new ThinnedScope(
+ id: (int) $process->scope->id,
+ provider: $thinnedProvider,
+ shortName: $process->scope->getShortName() ?? null,
+ telephoneActivated: (bool) $process->scope->getTelephoneActivated() ?? false,
+ telephoneRequired: (bool) $process->scope->getTelephoneRequired() ?? false,
+ customTextfieldActivated: (bool) $process->scope->getCustomTextfieldActivated() ?? false,
+ customTextfieldRequired: (bool) $process->scope->getCustomTextfieldRequired() ?? false,
+ customTextfieldLabel: $process->scope->getCustomTextfieldLabel() ?? null,
+ captchaActivatedRequired: (bool) $process->scope->getCaptchaActivatedRequired() ?? false,
+ displayInfo: $process->scope->getDisplayInfo() ?? null
+ );
+ }
+
+ $thinnedProcess->scope = $thinnedScope;
+
+ return $thinnedProcess;
+
+ } catch (\RuntimeException $e) {
+ if (strpos($e->getMessage(), 'appointmentNotFound') !== false) {
+ return ExceptionService::appointmentNotFound();
+ }
+ if (strpos($e->getMessage(), 'authKeyMismatch') !== false) {
+ return ExceptionService::authKeyMismatch();
+ }
+ if (strpos($e->getMessage(), 'officesNotFound') !== false) {
+ return ExceptionService::officesNotFound();
+ }
+ return ExceptionService::internalError();
+ }
+ }
+
+ public static function updateClientData(Process $reservedProcess): Process|array
+ {
+ try {
+ $clientUpdateResult = ZmsApiClientService::submitClientData($reservedProcess);
+ if (isset($clientUpdateResult['error'])) {
+ return $clientUpdateResult;
+ }
+ return $clientUpdateResult;
+ } catch (\RuntimeException $e) {
+ if (strpos($e->getMessage(), 'tooManyAppointmentsWithSameMail') !== false) {
+ return ExceptionService::tooManyAppointmentsWithSameMail();
+ }
+ if (strpos($e->getMessage(), 'emailIsRequired') !== false) {
+ return ExceptionService::emailIsRequired();
+ }
+ if (strpos($e->getMessage(), 'telephoneIsRequired') !== false) {
+ return ExceptionService::telephoneIsRequired();
+ }
+ if (strpos($e->getMessage(), 'appointmentNotFound') !== false) {
+ return ExceptionService::appointmentNotFound();
+ }
+ if (strpos($e->getMessage(), 'authKeyMismatch') !== false) {
+ return ExceptionService::authKeyMismatch();
+ }
+ return ExceptionService::internalError();
+ }
+ }
+
+ public static function preconfirmAppointment(Process $reservedProcess): Process|array
+ {
+ try {
+ $clientUpdateResult = ZmsApiClientService::preconfirmProcess($reservedProcess);
+ if (isset($clientUpdateResult['error'])) {
+ return $clientUpdateResult;
+ }
+ return $clientUpdateResult;
+ } catch (\RuntimeException $e) {
+ if (strpos($e->getMessage(), 'tooManyAppointmentsWithSameMail') !== false) {
+ return ExceptionService::tooManyAppointmentsWithSameMail();
+ }
+ if (strpos($e->getMessage(), 'emailIsRequired') !== false) {
+ return ExceptionService::emailIsRequired();
+ }
+ if (strpos($e->getMessage(), 'telephoneIsRequired') !== false) {
+ return ExceptionService::telephoneIsRequired();
+ }
+ if (strpos($e->getMessage(), 'appointmentNotFound') !== false) {
+ return ExceptionService::appointmentNotFound();
+ }
+ if (strpos($e->getMessage(), 'authKeyMismatch') !== false) {
+ return ExceptionService::authKeyMismatch();
+ }
+ if (strpos($e->getMessage(), 'preconfirmationExpired') !== false) {
+ return ExceptionService::preconfirmationExpired();
+ }
+ return ExceptionService::internalError();
+ }
+ }
+
+ public static function confirmAppointment(Process $preconfirmedProcess): Process|array
+ {
+ try {
+ $clientUpdateResult = ZmsApiClientService::confirmProcess($preconfirmedProcess);
+ if (isset($clientUpdateResult['error'])) {
+ return $clientUpdateResult;
+ }
+ return $clientUpdateResult;
+ } catch (\RuntimeException $e) {
+ if (strpos($e->getMessage(), 'tooManyAppointmentsWithSameMail') !== false) {
+ return ExceptionService::tooManyAppointmentsWithSameMail();
+ }
+ if (strpos($e->getMessage(), 'emailIsRequired') !== false) {
+ return ExceptionService::emailIsRequired();
+ }
+ if (strpos($e->getMessage(), 'telephoneIsRequired') !== false) {
+ return ExceptionService::telephoneIsRequired();
+ }
+ if (strpos($e->getMessage(), 'appointmentNotFound') !== false) {
+ return ExceptionService::appointmentNotFound();
+ }
+ if (strpos($e->getMessage(), 'authKeyMismatch') !== false) {
+ return ExceptionService::authKeyMismatch();
+ }
+ return ExceptionService::internalError();
+ }
+ }
+
+ public static function cancelAppointment(Process $confirmedProcess): Process|array
+ {
+ try {
+ $clientUpdateResult = ZmsApiClientService::cancelAppointment($confirmedProcess);
+ if (isset($clientUpdateResult['error'])) {
+ return $clientUpdateResult;
+ }
+ return $clientUpdateResult;
+ } catch (\RuntimeException $e) {
+ if (strpos($e->getMessage(), 'tooManyAppointmentsWithSameMail') !== false) {
+ return ExceptionService::tooManyAppointmentsWithSameMail();
+ }
+ if (strpos($e->getMessage(), 'emailIsRequired') !== false) {
+ return ExceptionService::emailIsRequired();
+ }
+ if (strpos($e->getMessage(), 'telephoneIsRequired') !== false) {
+ return ExceptionService::telephoneIsRequired();
+ }
+ if (strpos($e->getMessage(), 'appointmentNotFound') !== false) {
+ return ExceptionService::appointmentNotFound();
+ }
+ if (strpos($e->getMessage(), 'authKeyMismatch') !== false) {
+ return ExceptionService::authKeyMismatch();
+ }
+ return ExceptionService::internalError();
+ }
+ }
+
+ public static function sendPreconfirmationEmail(Process $reservedProcess): Process|array
+ {
+ try {
+ $clientUpdateResult = ZmsApiClientService::sendPreconfirmationEmail($reservedProcess);
+ if (isset($clientUpdateResult['error'])) {
+ return $clientUpdateResult;
+ }
+ return $clientUpdateResult;
+ } catch (\RuntimeException $e) {
+ if (strpos($e->getMessage(), 'tooManyAppointmentsWithSameMail') !== false) {
+ return ExceptionService::tooManyAppointmentsWithSameMail();
+ }
+ if (strpos($e->getMessage(), 'emailIsRequired') !== false) {
+ return ExceptionService::emailIsRequired();
+ }
+ if (strpos($e->getMessage(), 'telephoneIsRequired') !== false) {
+ return ExceptionService::telephoneIsRequired();
+ }
+ if (strpos($e->getMessage(), 'appointmentNotFound') !== false) {
+ return ExceptionService::appointmentNotFound();
+ }
+ if (strpos($e->getMessage(), 'authKeyMismatch') !== false) {
+ return ExceptionService::authKeyMismatch();
+ }
+ if (strpos($e->getMessage(), 'mailNotFound') !== false) {
+ return ExceptionService::mailNotFound();
+ }
+ return ExceptionService::internalError();
+ }
+ }
+
+ public static function sendConfirmationEmail(Process $preconfirmedProcess): Process|array
+ {
+ try {
+ $clientUpdateResult = ZmsApiClientService::sendConfirmationEmail($preconfirmedProcess);
+ if (isset($clientUpdateResult['error'])) {
+ return $clientUpdateResult;
+ }
+ return $clientUpdateResult;
+ } catch (\RuntimeException $e) {
+ if (strpos($e->getMessage(), 'tooManyAppointmentsWithSameMail') !== false) {
+ return ExceptionService::tooManyAppointmentsWithSameMail();
+ }
+ if (strpos($e->getMessage(), 'emailIsRequired') !== false) {
+ return ExceptionService::emailIsRequired();
+ }
+ if (strpos($e->getMessage(), 'telephoneIsRequired') !== false) {
+ return ExceptionService::telephoneIsRequired();
+ }
+ if (strpos($e->getMessage(), 'appointmentNotFound') !== false) {
+ return ExceptionService::appointmentNotFound();
+ }
+ if (strpos($e->getMessage(), 'authKeyMismatch') !== false) {
+ return ExceptionService::authKeyMismatch();
+ }
+ if (strpos($e->getMessage(), 'mailNotFound') !== false) {
+ return ExceptionService::mailNotFound();
+ }
+ return ExceptionService::internalError();
+ }
+ }
+
+ public static function sendCancelationEmail(Process $confirmedProcess): Process|array
+ {
+ try {
+ $clientUpdateResult = ZmsApiClientService::sendCancelationEmail($confirmedProcess);
+ if (isset($clientUpdateResult['error'])) {
+ return $clientUpdateResult;
+ }
+ return $clientUpdateResult;
+ } catch (\RuntimeException $e) {
+ if (strpos($e->getMessage(), 'tooManyAppointmentsWithSameMail') !== false) {
+ return ExceptionService::tooManyAppointmentsWithSameMail();
+ }
+ if (strpos($e->getMessage(), 'emailIsRequired') !== false) {
+ return ExceptionService::emailIsRequired();
+ }
+ if (strpos($e->getMessage(), 'telephoneIsRequired') !== false) {
+ return ExceptionService::telephoneIsRequired();
+ }
+ if (strpos($e->getMessage(), 'appointmentNotFound') !== false) {
+ return ExceptionService::appointmentNotFound();
+ }
+ if (strpos($e->getMessage(), 'authKeyMismatch') !== false) {
+ return ExceptionService::authKeyMismatch();
+ }
+ if (strpos($e->getMessage(), 'mailNotFound') !== false) {
+ return ExceptionService::mailNotFound();
+ }
+ return ExceptionService::internalError();
+ }
+ }
+
+}
diff --git a/zmscitizenapi/src/Zmscitizenapi/Services/Office/OfficeListByServiceService.php b/zmscitizenapi/src/Zmscitizenapi/Services/Office/OfficeListByServiceService.php
new file mode 100644
index 000000000..abf071b3f
--- /dev/null
+++ b/zmscitizenapi/src/Zmscitizenapi/Services/Office/OfficeListByServiceService.php
@@ -0,0 +1,42 @@
+extractClientData($queryParams);
+
+ $errors = $this->validateClientData($clientData);
+ if (!empty($errors['errors'])) {
+ return $errors;
+ }
+
+ return $this->getOfficeListByService($clientData);
+ }
+
+ private function extractClientData(array $queryParams): object
+ {
+ return (object) [
+ 'serviceId' => isset($queryParams['serviceId']) && is_numeric($queryParams['serviceId'])
+ ? (int) $queryParams['serviceId']
+ : null
+ ];
+ }
+
+ private function validateClientData(object $data): array
+ {
+ return ValidationService::validateGetOfficeListByServiceId($data->serviceId);
+ }
+
+ private function getOfficeListByService(object $data): array|OfficeList
+ {
+ return ZmsApiFacadeService::getOfficeListByServiceId($data->serviceId);
+ }
+}
\ No newline at end of file
diff --git a/zmscitizenapi/src/Zmscitizenapi/Services/Office/OfficesListService.php b/zmscitizenapi/src/Zmscitizenapi/Services/Office/OfficesListService.php
new file mode 100644
index 000000000..f78031ed0
--- /dev/null
+++ b/zmscitizenapi/src/Zmscitizenapi/Services/Office/OfficesListService.php
@@ -0,0 +1,20 @@
+getOffices();
+ }
+
+ private function getOffices(): array|OfficeList
+ {
+ return ZmsApiFacadeService::getOffices();
+ }
+}
\ No newline at end of file
diff --git a/zmscitizenapi/src/Zmscitizenapi/Services/Office/OfficesServicesRelationsService.php b/zmscitizenapi/src/Zmscitizenapi/Services/Office/OfficesServicesRelationsService.php
new file mode 100644
index 000000000..e575c32f4
--- /dev/null
+++ b/zmscitizenapi/src/Zmscitizenapi/Services/Office/OfficesServicesRelationsService.php
@@ -0,0 +1,20 @@
+getServicesAndOffices();
+ }
+
+ private function getServicesAndOffices(): array|OfficeServiceAndRelationList
+ {
+ return ZmsApiFacadeService::getServicesAndOffices();
+ }
+}
\ No newline at end of file
diff --git a/zmscitizenapi/src/Zmscitizenapi/Services/Scope/ScopeByIdService.php b/zmscitizenapi/src/Zmscitizenapi/Services/Scope/ScopeByIdService.php
new file mode 100644
index 000000000..1db31733d
--- /dev/null
+++ b/zmscitizenapi/src/Zmscitizenapi/Services/Scope/ScopeByIdService.php
@@ -0,0 +1,42 @@
+extractClientData($queryParams);
+
+ $errors = $this->validateClientData($clientData);
+ if (!empty($errors['errors'])) {
+ return $errors;
+ }
+
+ return $this->getScopeById($clientData);
+ }
+
+ private function extractClientData(array $queryParams): object
+ {
+ return (object) [
+ 'scopeId' => isset($queryParams['scopeId']) && is_numeric($queryParams['scopeId'])
+ ? (int) $queryParams['scopeId']
+ : null
+ ];
+ }
+
+ private function validateClientData(object $clientData): array
+ {
+ return ValidationService::validateGetScopeById($clientData->scopeId);
+ }
+
+ private function getScopeById(object $clientData): array|ThinnedScope
+ {
+ return ZmsApiFacadeService::getScopeById($clientData->scopeId);
+ }
+}
\ No newline at end of file
diff --git a/zmscitizenapi/src/Zmscitizenapi/Services/Scope/ScopesListService.php b/zmscitizenapi/src/Zmscitizenapi/Services/Scope/ScopesListService.php
new file mode 100644
index 000000000..bc6482d38
--- /dev/null
+++ b/zmscitizenapi/src/Zmscitizenapi/Services/Scope/ScopesListService.php
@@ -0,0 +1,20 @@
+getScopes();
+ }
+
+ private function getScopes(): array|ThinnedScopeList
+ {
+ return ZmsApiFacadeService::getScopes();
+ }
+}
\ No newline at end of file
diff --git a/zmscitizenapi/src/Zmscitizenapi/Services/Security/CaptchaService.php b/zmscitizenapi/src/Zmscitizenapi/Services/Security/CaptchaService.php
new file mode 100644
index 000000000..cc3ea655a
--- /dev/null
+++ b/zmscitizenapi/src/Zmscitizenapi/Services/Security/CaptchaService.php
@@ -0,0 +1,19 @@
+getCaptchaDetails()->getCaptchaDetails();
+ }
+
+ private function getCaptchaDetails(): FriendlyCaptcha
+ {
+ return new FriendlyCaptcha();
+ }
+}
\ No newline at end of file
diff --git a/zmscitizenapi/src/Zmscitizenapi/Services/Service/ServiceListByOfficeService.php b/zmscitizenapi/src/Zmscitizenapi/Services/Service/ServiceListByOfficeService.php
new file mode 100644
index 000000000..c69bb1525
--- /dev/null
+++ b/zmscitizenapi/src/Zmscitizenapi/Services/Service/ServiceListByOfficeService.php
@@ -0,0 +1,42 @@
+extractClientData($queryParams);
+
+ $errors = $this->validateClientData($clientData);
+ if (!empty($errors['errors'])) {
+ return $errors;
+ }
+
+ return $this->getServicesByOffice($clientData);
+ }
+
+ private function extractClientData(array $queryParams): object
+ {
+ return (object) [
+ 'officeId' => isset($queryParams['officeId']) && is_numeric($queryParams['officeId'])
+ ? (int) $queryParams['officeId']
+ : null
+ ];
+ }
+
+ private function validateClientData(object $clientData): array
+ {
+ return ValidationService::validateGetServicesByOfficeId($clientData->officeId);
+ }
+
+ private function getServicesByOffice(object $clientData): array|ServiceList
+ {
+ return ZmsApiFacadeService::getServicesByOfficeId($clientData->officeId);
+ }
+}
\ No newline at end of file
diff --git a/zmscitizenapi/src/Zmscitizenapi/Services/Service/ServicesListService.php b/zmscitizenapi/src/Zmscitizenapi/Services/Service/ServicesListService.php
new file mode 100644
index 000000000..3a8bc34f7
--- /dev/null
+++ b/zmscitizenapi/src/Zmscitizenapi/Services/Service/ServicesListService.php
@@ -0,0 +1,20 @@
+getServices();
+ }
+
+ private function getServices(): array|ServiceList
+ {
+ return ZmsApiFacadeService::getServices();
+ }
+}
\ No newline at end of file
diff --git a/zmscitizenapi/templates/.keep b/zmscitizenapi/templates/.keep
new file mode 100644
index 000000000..e69de29bb
diff --git a/zmscitizenapi/tests/Zmscitizenapi/ControllerTestCase.php b/zmscitizenapi/tests/Zmscitizenapi/ControllerTestCase.php
new file mode 100644
index 000000000..915dbffe3
--- /dev/null
+++ b/zmscitizenapi/tests/Zmscitizenapi/ControllerTestCase.php
@@ -0,0 +1,35 @@
+render($arguments, $parameters, $sessionData, $method);
+ $this->assertContainsEquals($response->getStatusCode(), $assertStatusCodes);
+ return json_decode($response->getBody(), true);
+ }
+}
diff --git a/zmscitizenapi/tests/Zmscitizenapi/Controllers/Appointment/AppointmentByIdTest.php b/zmscitizenapi/tests/Zmscitizenapi/Controllers/Appointment/AppointmentByIdTest.php
new file mode 100644
index 000000000..9b37d980f
--- /dev/null
+++ b/zmscitizenapi/tests/Zmscitizenapi/Controllers/Appointment/AppointmentByIdTest.php
@@ -0,0 +1,255 @@
+clear();
+ }
+ }
+
+ public function testRendering()
+ {
+ $this->setApiCalls(
+ [
+ [
+ 'function' => 'readGetResult',
+ 'url' => '/process/101002/fb43/',
+ 'parameters' => [
+ 'resolveReferences' => 2,
+ ],
+ 'response' => $this->readFixture("GET_process.json")
+ ],
+ [
+ 'function' => 'readGetResult',
+ 'url' => '/source/unittest/',
+ 'parameters' => [
+ 'resolveReferences' => 2,
+ ],
+ 'response' => $this->readFixture("GET_SourceGet_dldb.json")
+ ]
+ ]
+ );
+
+ $parameters = [
+ 'processId' => '101002',
+ 'authKey' => 'fb43',
+ ];
+ $response = $this->render([], $parameters, []);
+ $responseBody = json_decode((string) $response->getBody(), true);
+ $expectedResponse = [
+ "processId" => 101002,
+ "timestamp" => "1724907600",
+ "authKey" => "fb43",
+ "familyName" => "Doe",
+ "customTextfield" => "",
+ "email" => "johndoe@example.com",
+ "telephone" => "0123456789",
+ "officeName" => "Bürgerbüro Orleansplatz DEV (KVR-II/231 DEV)",
+ "officeId" => 102522,
+ "status" => "confirmed",
+ "scope" => [
+ "id" => 64,
+ "provider" => [
+ "id" => 102522,
+ "name" => "Bürgerbüro Orleansplatz DEV (KVR-II/231 DEV)",
+ "lat" => null,
+ "lon" => null,
+ "source" => "dldb",
+ "contact" => [
+ "city" => "Muenchen",
+ "country" => "Germany",
+ "name" => "Bürgerbüro Orleansplatz DEV (KVR-II/231 DEV)",
+ "postalCode" => "81667",
+ "region" => "Muenchen",
+ "street" => "Orleansstraße",
+ "streetNumber" => "50"
+ ]
+ ],
+ "shortName" => "DEVV",
+ "telephoneActivated" => true,
+ "telephoneRequired" => true,
+ "customTextfieldActivated" => true,
+ "customTextfieldRequired" => true,
+ "customTextfieldLabel" => "Nachname des Kindes",
+ "captchaActivatedRequired" => false,
+ "displayInfo" => null
+ ],
+ "subRequestCounts" => [],
+ "serviceId" => 1063424,
+ "serviceCount" => 1
+ ];
+ $this->assertEquals(200, $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, $responseBody);
+ }
+
+ public function testMissingProcessId()
+ {
+ $parameters = [
+ 'authKey' => 'fb43',
+ ];
+
+ $response = $this->render([], $parameters, []);
+ $responseBody = json_decode((string) $response->getBody(), true);
+ $expectedResponse = [
+ 'errors' => [
+ ErrorMessages::get('invalidProcessId')
+ ]
+ ];
+ $this->assertEquals(ErrorMessages::get('invalidProcessId')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, $responseBody);
+ }
+
+ public function testMissingAuthKey()
+ {
+ $parameters = [
+ 'processId' => '101002',
+ ];
+
+ $response = $this->render([], $parameters, []);
+ $responseBody = json_decode((string) $response->getBody(), true);
+ $expectedResponse = [
+ 'errors' => [
+ ErrorMessages::get('invalidAuthKey')
+ ]
+ ];
+ $this->assertEquals(ErrorMessages::get('invalidAuthKey')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, $responseBody);
+ }
+
+ public function testInvalidProcessId()
+ {
+ $parameters = [
+ 'processId' => 'invalid',
+ 'authKey' => 'fb43',
+ ];
+
+ $response = $this->render([], $parameters, []);
+ $responseBody = json_decode((string) $response->getBody(), true);
+ $expectedResponse = [
+ 'errors' => [
+ ErrorMessages::get('invalidProcessId')
+ ]
+ ];
+ $this->assertEquals(ErrorMessages::get('invalidProcessId')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, $responseBody);
+ }
+
+ public function testInvalidAuthKey()
+ {
+ $parameters = [
+ 'processId' => '101002',
+ 'authKey' => 12345,
+ ];
+
+ $response = $this->render([], $parameters, []);
+ $responseBody = json_decode((string) $response->getBody(), true);
+ $expectedResponse = [
+ 'errors' => [
+ ErrorMessages::get('invalidAuthKey')
+ ]
+ ];
+ $this->assertEquals(ErrorMessages::get('invalidAuthKey')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, $responseBody);
+ }
+
+ public function testBothParametersMissing()
+ {
+ $parameters = [];
+
+ $response = $this->render([], $parameters, []);
+ $responseBody = json_decode((string) $response->getBody(), true);
+ $expectedResponse = [
+ 'errors' => [
+ ErrorMessages::get('invalidProcessId'),
+ ErrorMessages::get('invalidAuthKey')
+ ]
+ ];
+ $this->assertEquals(ErrorMessages::get('invalidProcessId')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidAuthKey')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, $responseBody);
+
+ }
+
+ public function testAppointmentNotFound()
+ {
+
+ $exception = new \BO\Zmsclient\Exception();
+ $exception->template = 'BO\\Zmsapi\\Exception\\Process\\ProcessNotFound';
+
+ $this->setApiCalls(
+ [
+ [
+ 'function' => 'readGetResult',
+ 'url' => '/process/101002/fb43/',
+ 'parameters' => [
+ 'resolveReferences' => 2,
+ ],
+ 'exception' => $exception
+ ]
+ ]
+ );
+
+ $parameters = [
+ 'processId' => '101002',
+ 'authKey' => 'fb43',
+ ];
+
+ $response = $this->render([], $parameters, []);
+ $responseBody = json_decode((string) $response->getBody(), true);
+ $expectedResponse = [
+ 'errors' => [
+ ErrorMessages::get('appointmentNotFound')
+ ]
+ ];
+ $this->assertEquals(ErrorMessages::get('appointmentNotFound')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, $responseBody);
+ }
+
+ public function testAuthKeyMismatchException()
+ {
+ $exception = new \BO\Zmsclient\Exception();
+ $exception->template = 'BO\\Zmsapi\\Exception\\Process\\AuthKeyMatchFailed';
+
+ $this->setApiCalls(
+ [
+ [
+ 'function' => 'readGetResult',
+ 'url' => '/process/101002/wrongKey/',
+ 'parameters' => [
+ 'resolveReferences' => 2,
+ ],
+ 'exception' => $exception
+ ]
+ ]
+ );
+
+ $parameters = [
+ 'processId' => '101002',
+ 'authKey' => 'wrongKey',
+ ];
+
+ $response = $this->render([], $parameters, []);
+ $responseBody = json_decode((string) $response->getBody(), true);
+ $expectedResponse = [
+ 'errors' => [
+ ErrorMessages::get('authKeyMismatch')
+ ]
+ ];
+ $this->assertEquals(ErrorMessages::get('authKeyMismatch')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, $responseBody);
+ }
+
+}
\ No newline at end of file
diff --git a/zmscitizenapi/tests/Zmscitizenapi/Controllers/Appointment/AppointmentCancelTest.php b/zmscitizenapi/tests/Zmscitizenapi/Controllers/Appointment/AppointmentCancelTest.php
new file mode 100644
index 000000000..48fc088c2
--- /dev/null
+++ b/zmscitizenapi/tests/Zmscitizenapi/Controllers/Appointment/AppointmentCancelTest.php
@@ -0,0 +1,272 @@
+clear();
+ }
+ }
+
+ public function testRendering()
+ {
+ $processResponse = $this->readFixture("GET_process.json");
+ $processData = json_decode($processResponse, true);
+ $processData['data']['appointments'][0]['date'] = time() + 86400;
+
+ $this->setApiCalls(
+ [
+ [
+ 'function' => 'readGetResult',
+ 'url' => '/process/101002/fb43/',
+ 'parameters' => [
+ 'resolveReferences' => 2,
+ ],
+ 'response' => json_encode($processData)
+ ],
+ [
+ 'function' => 'readGetResult',
+ 'url' => '/source/unittest/',
+ 'parameters' => [
+ 'resolveReferences' => 2,
+ ],
+ 'response' => $this->readFixture("GET_SourceGet_dldb.json")
+ ],
+ [
+ 'function' => 'readPostResult',
+ 'url' => '/process/101002/fb43/delete/mail/',
+ 'response' => $this->readFixture("POST_cancel_appointment.json")
+ ],
+ [
+ 'function' => 'readDeleteResult',
+ 'url' => '/process/101002/fb43/',
+ 'parameters' => [],
+ 'response' => $this->readFixture("POST_cancel_appointment.json")
+ ]
+ ]
+ );
+
+ $parameters = [
+ 'processId' => '101002',
+ 'authKey' => 'fb43'
+ ];
+ $response = $this->render([], $parameters, [], 'POST');
+ $responseBody = json_decode((string) $response->getBody(), true);
+
+ $expectedResponse = [
+ 'processId' => 101002,
+ 'timestamp' => '1727865900',
+ 'authKey' => 'fb43',
+ 'familyName' => 'TEST_USER',
+ 'customTextfield' => 'Some custom text',
+ 'email' => 'test@muenchen.de',
+ 'telephone' => '123456789',
+ 'officeName' => null,
+ 'officeId' => 0,
+ 'scope' => [
+ 'id' => 0,
+ 'provider' => null,
+ 'shortName' => null,
+ 'telephoneActivated' => null,
+ 'telephoneRequired' => null,
+ 'customTextfieldActivated' => null,
+ 'customTextfieldRequired' => null,
+ 'customTextfieldLabel' => null,
+ 'captchaActivatedRequired' => null,
+ 'displayInfo' => null
+ ],
+ 'subRequestCounts' => [],
+ 'serviceId' => 10242339,
+ 'serviceCount' => 1,
+ 'status' => 'deleted'
+ ];
+
+ $this->assertEquals(200, $response->getStatusCode());
+ $this->assertEquals($expectedResponse, $responseBody);
+ }
+
+ public function testPastAppointment()
+ {
+ $processResponse = $this->readFixture("GET_process.json");
+ $processData = json_decode($processResponse, true);
+ $processData['data']['appointments'][0]['date'] = time() - 3600; // 1 hour ago
+
+ $this->setApiCalls([
+ [
+ 'function' => 'readGetResult',
+ 'url' => '/process/101002/fb43/',
+ 'parameters' => [
+ 'resolveReferences' => 2,
+ ],
+ 'response' => json_encode($processData)
+ ],
+ [
+ 'function' => 'readGetResult',
+ 'url' => '/source/unittest/',
+ 'parameters' => [
+ 'resolveReferences' => 2,
+ ],
+ 'response' => $this->readFixture("GET_SourceGet_dldb.json")
+ ]
+ ]);
+
+ $parameters = [
+ 'processId' => '101002',
+ 'authKey' => 'fb43'
+ ];
+ $response = $this->render([], $parameters, [], 'POST');
+ $responseBody = json_decode((string) $response->getBody(), true);
+
+ $this->assertEquals(ErrorMessages::get('appointmentCanNotBeCanceled')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing(
+ ['errors' => [ErrorMessages::get('appointmentCanNotBeCanceled')]],
+ $responseBody
+ );
+ }
+
+ public function testAppointmentNotFoundException()
+ {
+ $exception = new \BO\Zmsclient\Exception();
+ $exception->template = 'BO\\Zmsapi\\Exception\\Process\\ProcessNotFound';
+
+ $this->setApiCalls([
+ [
+ 'function' => 'readGetResult',
+ 'url' => '/process/999999/fb43/',
+ 'parameters' => [
+ 'resolveReferences' => 2,
+ ],
+ 'exception' => $exception
+ ]
+ ]);
+
+ $parameters = [
+ 'processId' => '999999',
+ 'authKey' => 'fb43'
+ ];
+ $response = $this->render([], $parameters, [], 'POST');
+ $responseBody = json_decode((string) $response->getBody(), true);
+
+ $this->assertEquals(ErrorMessages::get('appointmentNotFound')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing(
+ ['errors' => [ErrorMessages::get('appointmentNotFound')]],
+ $responseBody
+ );
+ }
+
+ public function testAuthKeyMismatch()
+ {
+ $exception = new \BO\Zmsclient\Exception();
+ $exception->template = 'BO\\Zmsapi\\Exception\\Process\\AuthKeyMatchFailed';
+
+ $this->setApiCalls([
+ [
+ 'function' => 'readGetResult',
+ 'url' => '/process/101002/wrongkey/',
+ 'parameters' => [
+ 'resolveReferences' => 2,
+ ],
+ 'exception' => $exception
+ ]
+ ]);
+
+ $parameters = [
+ 'processId' => '101002',
+ 'authKey' => 'wrongkey'
+ ];
+ $response = $this->render([], $parameters, [], 'POST');
+ $responseBody = json_decode((string) $response->getBody(), true);
+
+ $this->assertEquals(ErrorMessages::get('authKeyMismatch')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing(
+ ['errors' => [ErrorMessages::get('authKeyMismatch')]],
+ $responseBody
+ );
+ }
+
+ public function testInvalidProcessId()
+ {
+ $parameters = [
+ 'processId' => null,
+ 'authKey' => 'fb43'
+ ];
+ $response = $this->render([], $parameters, [], 'POST');
+ $responseBody = json_decode((string) $response->getBody(), true);
+
+ $this->assertEquals(ErrorMessages::get('invalidProcessId')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing(
+ ['errors' => [ErrorMessages::get('invalidProcessId')]],
+ $responseBody
+ );
+ }
+
+ public function testInvalidAuthKey()
+ {
+ $parameters = [
+ 'processId' => '101002',
+ 'authKey' => ''
+ ];
+ $response = $this->render([], $parameters, [], 'POST');
+ $responseBody = json_decode((string) $response->getBody(), true);
+
+ $this->assertEquals(ErrorMessages::get('invalidAuthKey')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing(
+ ['errors' => [ErrorMessages::get('invalidAuthKey')]],
+ $responseBody
+ );
+ }
+
+ public function testMissingProcessId()
+ {
+ $parameters = [
+ 'authKey' => 'fb43'
+ ];
+ $response = $this->render([], $parameters, [], 'POST');
+ $responseBody = json_decode((string) $response->getBody(), true);
+
+ $this->assertEquals(ErrorMessages::get('invalidProcessId')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing(
+ ['errors' => [ErrorMessages::get('invalidProcessId')]],
+ $responseBody
+ );
+ }
+
+ public function testMissingAuthKey()
+ {
+ $parameters = [
+ 'processId' => '101002'
+ ];
+ $response = $this->render([], $parameters, [], 'POST');
+ $responseBody = json_decode((string) $response->getBody(), true);
+
+ $this->assertEquals(ErrorMessages::get('invalidAuthKey')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing(
+ ['errors' => [ErrorMessages::get('invalidAuthKey')]],
+ $responseBody
+ );
+ }
+
+ public function testInvalidRequest()
+ {
+ $response = $this->render([], [], [], 'GET');
+ $responseBody = json_decode((string) $response->getBody(), true);
+
+ $this->assertEquals(ErrorMessages::get('invalidRequest')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing(
+ ['errors' => [ErrorMessages::get('invalidRequest')]],
+ $responseBody
+ );
+ }
+}
\ No newline at end of file
diff --git a/zmscitizenapi/tests/Zmscitizenapi/Controllers/Appointment/AppointmentConfirmTest.php b/zmscitizenapi/tests/Zmscitizenapi/Controllers/Appointment/AppointmentConfirmTest.php
new file mode 100644
index 000000000..a9cef0502
--- /dev/null
+++ b/zmscitizenapi/tests/Zmscitizenapi/Controllers/Appointment/AppointmentConfirmTest.php
@@ -0,0 +1,270 @@
+clear();
+ }
+ }
+
+ public function testRendering()
+ {
+ $this->setApiCalls(
+ [
+ [
+ 'function' => 'readGetResult',
+ 'url' => '/process/101002/fb43/',
+ 'parameters' => [
+ 'resolveReferences' => 2,
+ ],
+ 'response' => $this->readFixture("GET_process.json")
+ ],
+ [
+ 'function' => 'readGetResult',
+ 'url' => '/source/unittest/',
+ 'parameters' => [
+ 'resolveReferences' => 2,
+ ],
+ 'response' => $this->readFixture("GET_SourceGet_dldb.json")
+ ],
+ [
+ 'function' => 'readPostResult',
+ 'url' => '/process/status/confirmed/',
+ 'response' => $this->readFixture("POST_confirm_appointment.json")
+ ],
+ [
+ 'function' => 'readPostResult',
+ 'url' => '/process/101002/fb43/confirmation/mail/',
+ 'response' => $this->readFixture("POST_confirm_appointment.json")
+ ]
+ ]
+ );
+
+ $parameters = [
+ 'processId' => '101002',
+ 'authKey' => 'fb43'
+ ];
+ $response = $this->render([], $parameters, [], 'POST');
+ $responseBody = json_decode((string) $response->getBody(), true);
+
+ $expectedResponse = [
+ 'processId' => 101002,
+ 'timestamp' => '1727865900',
+ 'authKey' => 'fb43',
+ 'familyName' => 'TEST_USER',
+ 'customTextfield' => 'Some custom text',
+ 'email' => 'test@muenchen.de',
+ 'telephone' => '123456789',
+ 'officeName' => null,
+ 'officeId' => 0,
+ 'scope' => [
+ 'id' => 0,
+ 'provider' => null,
+ 'shortName' => null,
+ 'telephoneActivated' => null,
+ 'telephoneRequired' => null,
+ 'customTextfieldActivated' => null,
+ 'customTextfieldRequired' => null,
+ 'customTextfieldLabel' => null,
+ 'captchaActivatedRequired' => null,
+ 'displayInfo' => null
+ ],
+ 'subRequestCounts' => [],
+ 'serviceId' => 10242339,
+ 'serviceCount' => 1,
+ 'status' => 'confirmed'
+ ];
+
+ $this->assertEquals(200, $response->getStatusCode());
+ $this->assertEquals($expectedResponse, $responseBody);
+ }
+
+ public function testAppointmentNotFoundException()
+ {
+ $exception = new \BO\Zmsclient\Exception();
+ $exception->template = 'BO\\Zmsapi\\Exception\\Process\\ProcessNotFound';
+
+ $this->setApiCalls([
+ [
+ 'function' => 'readGetResult',
+ 'url' => '/process/999999/fb43/',
+ 'parameters' => [
+ 'resolveReferences' => 2,
+ ],
+ 'exception' => $exception
+ ]
+ ]);
+
+ $parameters = [
+ 'processId' => '999999',
+ 'authKey' => 'fb43'
+ ];
+ $response = $this->render([], $parameters, [], 'POST');
+ $responseBody = json_decode((string) $response->getBody(), true);
+
+ $this->assertEquals(ErrorMessages::get('appointmentNotFound')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing(
+ ['errors' => [ErrorMessages::get('appointmentNotFound')]],
+ $responseBody
+ );
+ }
+
+ public function testAuthKeyMismatch()
+ {
+ $exception = new \BO\Zmsclient\Exception();
+ $exception->template = 'BO\\Zmsapi\\Exception\\Process\\AuthKeyMatchFailed';
+
+ $this->setApiCalls([
+ [
+ 'function' => 'readGetResult',
+ 'url' => '/process/101002/wrongkey/',
+ 'parameters' => [
+ 'resolveReferences' => 2,
+ ],
+ 'exception' => $exception
+ ]
+ ]);
+
+ $parameters = [
+ 'processId' => '101002',
+ 'authKey' => 'wrongkey'
+ ];
+ $response = $this->render([], $parameters, [], 'POST');
+ $responseBody = json_decode((string) $response->getBody(), true);
+
+ $this->assertEquals(ErrorMessages::get('authKeyMismatch')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing(
+ ['errors' => [ErrorMessages::get('authKeyMismatch')]],
+ $responseBody
+ );
+ }
+
+ public function testInvalidProcessId()
+ {
+ $parameters = [
+ 'processId' => null,
+ 'authKey' => 'fb43'
+ ];
+ $response = $this->render([], $parameters, [], 'POST');
+ $responseBody = json_decode((string) $response->getBody(), true);
+
+ $this->assertEquals(ErrorMessages::get('invalidProcessId')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing(
+ ['errors' => [ErrorMessages::get('invalidProcessId')]],
+ $responseBody
+ );
+ }
+
+ public function testInvalidAuthKey()
+ {
+ $parameters = [
+ 'processId' => '101002',
+ 'authKey' => ''
+ ];
+ $response = $this->render([], $parameters, [], 'POST');
+ $responseBody = json_decode((string) $response->getBody(), true);
+
+ $this->assertEquals(ErrorMessages::get('invalidAuthKey')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing(
+ ['errors' => [ErrorMessages::get('invalidAuthKey')]],
+ $responseBody
+ );
+ }
+
+ public function testMissingProcessId()
+ {
+ $parameters = [
+ 'authKey' => 'fb43'
+ ];
+ $response = $this->render([], $parameters, [], 'POST');
+ $responseBody = json_decode((string) $response->getBody(), true);
+
+ $this->assertEquals(ErrorMessages::get('invalidProcessId')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing(
+ ['errors' => [ErrorMessages::get('invalidProcessId')]],
+ $responseBody
+ );
+ }
+
+ public function testMissingAuthKey()
+ {
+ $parameters = [
+ 'processId' => '101002'
+ ];
+ $response = $this->render([], $parameters, [], 'POST');
+ $responseBody = json_decode((string) $response->getBody(), true);
+
+ $this->assertEquals(ErrorMessages::get('invalidAuthKey')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing(
+ ['errors' => [ErrorMessages::get('invalidAuthKey')]],
+ $responseBody
+ );
+ }
+
+ public function testNoEmailSendingWhenStatusNotConfirmed()
+ {
+ $processResponse = $this->readFixture("POST_confirm_appointment.json");
+ $processData = json_decode($processResponse, true);
+ $processData['data']['queue']['status'] = 'reserved'; // Change status to something else
+
+ $this->setApiCalls([
+ [
+ 'function' => 'readGetResult',
+ 'url' => '/process/101002/fb43/',
+ 'parameters' => [
+ 'resolveReferences' => 2,
+ ],
+ 'response' => $this->readFixture("GET_process.json")
+ ],
+ [
+ 'function' => 'readGetResult',
+ 'url' => '/source/unittest/',
+ 'parameters' => [
+ 'resolveReferences' => 2,
+ ],
+ 'response' => $this->readFixture("GET_SourceGet_dldb.json")
+ ],
+ [
+ 'function' => 'readPostResult',
+ 'url' => '/process/status/confirmed/',
+ 'response' => json_encode($processData)
+ ]
+ // Note: No email API call should be made
+ ]);
+
+ $parameters = [
+ 'processId' => '101002',
+ 'authKey' => 'fb43'
+ ];
+ $response = $this->render([], $parameters, [], 'POST');
+ $responseBody = json_decode((string) $response->getBody(), true);
+
+ $this->assertEquals(200, $response->getStatusCode());
+ $this->assertEquals('reserved', $responseBody['status']);
+ }
+
+ public function testInvalidRequest()
+ {
+ $response = $this->render([], [], [], 'GET'); // Using GET instead of POST
+ $responseBody = json_decode((string) $response->getBody(), true);
+
+ $this->assertEquals(ErrorMessages::get('invalidRequest')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing(
+ ['errors' => [ErrorMessages::get('invalidRequest')]],
+ $responseBody
+ );
+ }
+}
\ No newline at end of file
diff --git a/zmscitizenapi/tests/Zmscitizenapi/Controllers/Appointment/AppointmentPreconfirmTest.php b/zmscitizenapi/tests/Zmscitizenapi/Controllers/Appointment/AppointmentPreconfirmTest.php
new file mode 100644
index 000000000..7288bfd03
--- /dev/null
+++ b/zmscitizenapi/tests/Zmscitizenapi/Controllers/Appointment/AppointmentPreconfirmTest.php
@@ -0,0 +1,356 @@
+clear();
+ }
+ }
+
+ public function testRendering()
+ {
+ $this->setApiCalls(
+ [
+ [
+ 'function' => 'readGetResult',
+ 'url' => '/process/101002/fb43/',
+ 'parameters' => [
+ 'resolveReferences' => 2,
+ ],
+ 'response' => $this->readFixture("GET_process.json")
+ ],
+ [
+ 'function' => 'readGetResult',
+ 'url' => '/source/unittest/',
+ 'parameters' => [
+ 'resolveReferences' => 2,
+ ],
+ 'response' => $this->readFixture("GET_SourceGet_dldb.json")
+ ],
+ [
+ 'function' => 'readPostResult',
+ 'url' => '/process/status/preconfirmed/',
+ 'response' => $this->readFixture("POST_preconfirm_appointment.json")
+ ],
+ [
+ 'function' => 'readPostResult',
+ 'url' => '/process/101002/fb43/preconfirmation/mail/',
+ 'response' => $this->readFixture("POST_preconfirm_appointment.json")
+ ]
+ ]
+ );
+
+ $parameters = [
+ 'processId' => '101002',
+ 'authKey' => 'fb43'
+ ];
+ $response = $this->render([], $parameters, [], 'POST');
+ $responseBody = json_decode((string) $response->getBody(), true);
+
+ $expectedResponse = [
+ 'processId' => 101002,
+ 'timestamp' => '1727865900',
+ 'authKey' => 'fb43',
+ 'familyName' => 'TEST_USER',
+ 'customTextfield' => 'Some custom text',
+ 'email' => 'test@muenchen.de',
+ 'telephone' => '123456789',
+ 'officeName' => null,
+ 'officeId' => 0,
+ 'scope' => [
+ 'id' => 0,
+ 'provider' => null,
+ 'shortName' => null,
+ 'telephoneActivated' => null,
+ 'telephoneRequired' => null,
+ 'customTextfieldActivated' => null,
+ 'customTextfieldRequired' => null,
+ 'customTextfieldLabel' => null,
+ 'captchaActivatedRequired' => null,
+ 'displayInfo' => null
+ ],
+ 'subRequestCounts' => [],
+ 'serviceId' => 10242339,
+ 'serviceCount' => 1,
+ 'status' => 'preconfirmed'
+ ];
+
+ $this->assertEquals(200, $response->getStatusCode());
+ $this->assertEquals($expectedResponse, $responseBody);
+ }
+
+ public function testAppointmentNotFoundException()
+ {
+ $exception = new \BO\Zmsclient\Exception();
+ $exception->template = 'BO\\Zmsapi\\Exception\\Process\\ProcessNotFound';
+
+ $this->setApiCalls([
+ [
+ 'function' => 'readGetResult',
+ 'url' => '/process/999999/fb43/',
+ 'parameters' => [
+ 'resolveReferences' => 2,
+ ],
+ 'exception' => $exception
+ ]
+ ]);
+
+ $parameters = [
+ 'processId' => '999999',
+ 'authKey' => 'fb43'
+ ];
+ $response = $this->render([], $parameters, [], 'POST');
+ $responseBody = json_decode((string) $response->getBody(), true);
+
+ $this->assertEquals(ErrorMessages::get('appointmentNotFound')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing(
+ ['errors' => [ErrorMessages::get('appointmentNotFound')]],
+ $responseBody
+ );
+ }
+
+ public function testAuthKeyMismatch()
+ {
+ $exception = new \BO\Zmsclient\Exception();
+ $exception->template = 'BO\\Zmsapi\\Exception\\Process\\AuthKeyMatchFailed';
+
+ $this->setApiCalls([
+ [
+ 'function' => 'readGetResult',
+ 'url' => '/process/101002/wrongkey/',
+ 'parameters' => [
+ 'resolveReferences' => 2,
+ ],
+ 'exception' => $exception
+ ]
+ ]);
+
+ $parameters = [
+ 'processId' => '101002',
+ 'authKey' => 'wrongkey'
+ ];
+ $response = $this->render([], $parameters, [], 'POST');
+ $responseBody = json_decode((string) $response->getBody(), true);
+
+ $this->assertEquals(ErrorMessages::get('authKeyMismatch')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing(
+ ['errors' => [ErrorMessages::get('authKeyMismatch')]],
+ $responseBody
+ );
+ }
+
+ public function testTooManyEmailsAtLocation()
+ {
+ $exception = new \BO\Zmsclient\Exception();
+ $exception->template = 'BO\\Zmsapi\\Exception\\Process\\MoreThanAllowedAppointmentsPerMail';
+
+ $this->setApiCalls([
+ [
+ 'function' => 'readGetResult',
+ 'url' => '/process/101002/fb43/',
+ 'parameters' => [
+ 'resolveReferences' => 2,
+ ],
+ 'response' => $this->readFixture("GET_process.json")
+ ],
+ [
+ 'function' => 'readGetResult',
+ 'url' => '/source/unittest/',
+ 'parameters' => [
+ 'resolveReferences' => 2,
+ ],
+ 'response' => $this->readFixture("GET_SourceGet_dldb.json")
+ ],
+ [
+ 'function' => 'readPostResult',
+ 'url' => '/process/status/preconfirmed/',
+ 'exception' => $exception
+ ]
+ ]);
+
+ $parameters = [
+ 'processId' => '101002',
+ 'authKey' => 'fb43'
+ ];
+ $response = $this->render([], $parameters, [], 'POST');
+ $responseBody = json_decode((string) $response->getBody(), true);
+
+ $this->assertEquals(406, $response->getStatusCode());
+ $this->assertEqualsCanonicalizing(
+ ['errors' => [ErrorMessages::get('tooManyAppointmentsWithSameMail')]],
+ $responseBody
+ );
+ }
+
+ public function testInvalidProcessId()
+ {
+ $parameters = [
+ 'processId' => null,
+ 'authKey' => 'fb43'
+ ];
+ $response = $this->render([], $parameters, [], 'POST');
+ $responseBody = json_decode((string) $response->getBody(), true);
+
+ $this->assertEquals(ErrorMessages::get('invalidProcessId')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing(
+ ['errors' => [ErrorMessages::get('invalidProcessId')]],
+ $responseBody
+ );
+ }
+
+ public function testInvalidAuthKey()
+ {
+ $parameters = [
+ 'processId' => '101002',
+ 'authKey' => ''
+ ];
+ $response = $this->render([], $parameters, [], 'POST');
+ $responseBody = json_decode((string) $response->getBody(), true);
+
+ $this->assertEquals(ErrorMessages::get('invalidAuthKey')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing(
+ ['errors' => [ErrorMessages::get('invalidAuthKey')]],
+ $responseBody
+ );
+ }
+
+ public function testMissingProcessId()
+ {
+ $parameters = [
+ 'authKey' => 'fb43'
+ ];
+ $response = $this->render([], $parameters, [], 'POST');
+ $responseBody = json_decode((string) $response->getBody(), true);
+
+ $this->assertEquals(ErrorMessages::get('invalidProcessId')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing(
+ ['errors' => [ErrorMessages::get('invalidProcessId')]],
+ $responseBody
+ );
+ }
+
+ public function testMissingAuthKey()
+ {
+ $parameters = [
+ 'processId' => '101002'
+ ];
+ $response = $this->render([], $parameters, [], 'POST');
+ $responseBody = json_decode((string) $response->getBody(), true);
+
+ $this->assertEquals(ErrorMessages::get('invalidAuthKey')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing(
+ ['errors' => [ErrorMessages::get('invalidAuthKey')]],
+ $responseBody
+ );
+ }
+
+ public function testNoEmailSendingWhenStatusNotPreconfirmed()
+ {
+ $processResponse = $this->readFixture("POST_preconfirm_appointment.json");
+ $processData = json_decode($processResponse, true);
+ $processData['data']['queue']['status'] = 'confirmed'; // Change status to something else
+
+ $this->setApiCalls([
+ [
+ 'function' => 'readGetResult',
+ 'url' => '/process/101002/fb43/',
+ 'parameters' => [
+ 'resolveReferences' => 2,
+ ],
+ 'response' => $this->readFixture("GET_process.json")
+ ],
+ [
+ 'function' => 'readGetResult',
+ 'url' => '/source/unittest/',
+ 'parameters' => [
+ 'resolveReferences' => 2,
+ ],
+ 'response' => $this->readFixture("GET_SourceGet_dldb.json")
+ ],
+ [
+ 'function' => 'readPostResult',
+ 'url' => '/process/status/preconfirmed/',
+ 'response' => json_encode($processData)
+ ]
+ // Note: No email API call should be made
+ ]);
+
+ $parameters = [
+ 'processId' => '101002',
+ 'authKey' => 'fb43'
+ ];
+ $response = $this->render([], $parameters, [], 'POST');
+ $responseBody = json_decode((string) $response->getBody(), true);
+
+ $this->assertEquals(200, $response->getStatusCode());
+ $this->assertEquals('confirmed', $responseBody['status']);
+ }
+
+ public function testInvalidRequest()
+ {
+ $response = $this->render([], [], [], 'GET'); // Using GET instead of POST
+ $responseBody = json_decode((string) $response->getBody(), true);
+
+ $this->assertEquals(ErrorMessages::get('invalidRequest')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing(
+ ['errors' => [ErrorMessages::get('invalidRequest')]],
+ $responseBody
+ );
+ }
+
+ public function testPreconfirmationExpired()
+ {
+ $exception = new \BO\Zmsclient\Exception();
+ $exception->template = 'BO\\Zmsapi\\Exception\\Process\\PreconfirmationExpired';
+
+ $this->setApiCalls([
+ [
+ 'function' => 'readGetResult',
+ 'url' => '/process/101002/fb43/',
+ 'parameters' => [
+ 'resolveReferences' => 2,
+ ],
+ 'response' => $this->readFixture("GET_process.json")
+ ],
+ [
+ 'function' => 'readGetResult',
+ 'url' => '/source/unittest/',
+ 'parameters' => [
+ 'resolveReferences' => 2,
+ ],
+ 'response' => $this->readFixture("GET_SourceGet_dldb.json")
+ ],
+ [
+ 'function' => 'readPostResult',
+ 'url' => '/process/status/preconfirmed/',
+ 'exception' => $exception
+ ]
+ ]);
+
+ $parameters = [
+ 'processId' => '101002',
+ 'authKey' => 'fb43'
+ ];
+ $response = $this->render([], $parameters, [], 'POST');
+ $responseBody = json_decode((string) $response->getBody(), true);
+ $this->assertEquals(ErrorMessages::get('preconfirmationExpired')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing(
+ ['errors' => [ErrorMessages::get('preconfirmationExpired')]],
+ $responseBody
+ );
+ }
+
+}
\ No newline at end of file
diff --git a/zmscitizenapi/tests/Zmscitizenapi/Controllers/Appointment/AppointmentReserveTest.php b/zmscitizenapi/tests/Zmscitizenapi/Controllers/Appointment/AppointmentReserveTest.php
new file mode 100644
index 000000000..1ea67b3e2
--- /dev/null
+++ b/zmscitizenapi/tests/Zmscitizenapi/Controllers/Appointment/AppointmentReserveTest.php
@@ -0,0 +1,415 @@
+clear();
+ }
+ }
+
+ public function testRendering()
+ {
+ $this->setApiCalls(
+ [
+ [
+ 'function' => 'readGetResult',
+ 'url' => '/source/unittest/',
+ 'parameters' => [
+ 'resolveReferences' => 2,
+ ],
+ 'response' => $this->readFixture("GET_reserve_SourceGet_dldb.json"),
+ ],
+ [
+ 'function' => 'readPostResult',
+ 'url' => '/process/status/free/',
+ 'response' => $this->readFixture("GET_appointments_free.json")
+ ],
+ [
+ 'function' => 'readPostResult',
+ 'url' => '/process/status/reserved/',
+ 'response' => $this->readFixture("POST_reserve_appointment.json")
+ ]
+ ]
+ );
+
+ $parameters = [
+ 'officeId' => 10546,
+ 'serviceId' => ['1063423'],
+ 'serviceCount' => [0],
+ 'timestamp' => "32526616522",
+ 'captchaSolution' => null
+ ];
+
+ $response = $this->render([], $parameters, [], 'POST');
+ $responseBody = json_decode((string)$response->getBody(), true);
+
+ $expectedResponse = [
+ "processId" => 101002,
+ "timestamp" => "32526616522",
+ "authKey" => "fb43",
+ "familyName" => "TEST_USER",
+ "customTextfield" => "",
+ "email" => "test@muenchen.de",
+ "telephone" => "123456789",
+ "officeName" => null,
+ "officeId" => 10546,
+ "status" => "reserved",
+ "scope" => [
+ "id" => 58,
+ "provider" => [
+ "id" => 10546,
+ "name" => "Gewerbeamt (KVR-III/21)",
+ "lat" => null,
+ "lon" => null,
+ "source" => "dldb",
+ "contact" => [
+ "city" => "Muenchen",
+ "country" => "Germany",
+ "name" => "Gewerbeamt (KVR-III/21)",
+ "postalCode" => "81371",
+ "region" => "Muenchen",
+ "street" => "Implerstraße",
+ "streetNumber" => "11"
+ ]
+ ],
+ "shortName" => "Gewerbemeldungen",
+ "telephoneActivated" => false,
+ "telephoneRequired" => true,
+ "customTextfieldActivated" => false,
+ "customTextfieldRequired" => true,
+ "customTextfieldLabel" => "",
+ "captchaActivatedRequired" => false,
+ "displayInfo" => null
+ ],
+ "subRequestCounts" => [],
+ "serviceId" => 0,
+ "serviceCount" => 0
+ ];
+
+ $this->assertEquals(200, $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, $responseBody);
+ }
+
+ public function testAppointmentNotAvailable()
+ {
+ $this->setApiCalls(
+ [
+ [
+ 'function' => 'readGetResult',
+ 'url' => '/source/unittest/',
+ 'parameters' => [
+ 'resolveReferences' => 2,
+ ],
+ 'response' => $this->readFixture("GET_reserve_SourceGet_dldb.json"),
+ ],
+ [
+ 'function' => 'readPostResult',
+ 'url' => '/process/status/free/',
+ 'response' => $this->readFixture("GET_appointments_free.json")
+ ]
+ ]
+ );
+
+ $parameters = [
+ 'officeId' => 10546,
+ 'serviceId' => ['1063423'],
+ 'serviceCount' => [0],
+ 'timestamp' => "32526616300",
+ 'captchaSolution' => null
+ ];
+
+ $response = $this->render([], $parameters, [], 'POST');
+ $responseBody = json_decode((string)$response->getBody(), true);
+ $expectedResponse = [
+ 'errors' => [
+ ErrorMessages::get('appointmentNotAvailable')
+ ]
+ ];
+ $this->assertEquals(ErrorMessages::get('appointmentNotAvailable')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, $responseBody);
+ }
+
+ public function testMissingOfficeId()
+ {
+ $this->setApiCalls([]);
+
+ $parameters = [
+ 'serviceId' => ['1063423'],
+ 'serviceCount' => [0],
+ 'timestamp' => "32526616522",
+ 'captchaSolution' => null
+ ];
+
+ $response = $this->render([], $parameters, [], 'POST');
+ $responseBody = json_decode((string)$response->getBody(), true);
+ $expectedResponse = [
+ 'errors' => [
+ ErrorMessages::get('invalidOfficeId')
+ ]
+ ];
+ $this->assertEquals(ErrorMessages::get('invalidOfficeId')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, $responseBody);
+ }
+
+ public function testMissingServiceId()
+ {
+ $this->setApiCalls([]);
+
+ $parameters = [
+ 'officeId' => 10546,
+ 'serviceCount' => [0],
+ 'timestamp' => "32526616522",
+ 'captchaSolution' => null
+ ];
+
+ $response = $this->render([], $parameters, [], 'POST');
+ $responseBody = json_decode((string)$response->getBody(), true);
+ $expectedResponse = [
+ 'errors' => [
+ ErrorMessages::get('invalidServiceId')
+ ]
+ ];
+ $this->assertEquals(ErrorMessages::get('invalidServiceId')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, $responseBody);
+ }
+
+ public function testMissingTimestamp()
+ {
+ $this->setApiCalls([]);
+
+ $parameters = [
+ 'officeId' => 10546,
+ 'serviceId' => ['1063423'],
+ 'serviceCount' => [0],
+ 'captchaSolution' => null
+ ];
+
+ $response = $this->render([], $parameters, [], 'POST');
+ $responseBody = json_decode((string)$response->getBody(), true);
+ $expectedResponse = [
+ 'errors' => [
+ ErrorMessages::get('invalidTimestamp')
+ ]
+ ];
+ $this->assertEquals(ErrorMessages::get('invalidTimestamp')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, $responseBody);
+ }
+
+ public function testMissingOfficeIdAndServiceId()
+ {
+ $this->setApiCalls([]);
+
+ $parameters = [
+ 'serviceCount' => [0],
+ 'timestamp' => "32526616522",
+ 'captchaSolution' => null
+ ];
+
+ $response = $this->render([], $parameters, [], 'POST');
+ $responseBody = json_decode((string)$response->getBody(), true);
+ $expectedResponse = [
+ 'errors' => [
+ ErrorMessages::get('invalidOfficeId'),
+ ErrorMessages::get('invalidServiceId')
+ ]
+ ];
+ $this->assertEquals(ErrorMessages::get('invalidOfficeId')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidServiceId')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, $responseBody);
+ }
+
+ public function testMissingOfficeIdAndTimestamp()
+ {
+ $this->setApiCalls([]);
+
+ $parameters = [
+ 'serviceId' => ['1063423'],
+ 'serviceCount' => [0],
+ 'captchaSolution' => null
+ ];
+
+ $response = $this->render([], $parameters, [], 'POST');
+ $responseBody = json_decode((string)$response->getBody(), true);
+ $expectedResponse = [
+ 'errors' => [
+ ErrorMessages::get('invalidOfficeId'),
+ ErrorMessages::get('invalidTimestamp')
+ ]
+ ];
+ $this->assertEquals(ErrorMessages::get('invalidOfficeId')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidTimestamp')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, $responseBody);
+ }
+
+ public function testMissingServiceIdAndTimestamp()
+ {
+ $this->setApiCalls([]);
+
+ $parameters = [
+ 'officeId' => 10546,
+ 'serviceCount' => [0],
+ 'captchaSolution' => null
+ ];
+
+ $response = $this->render([], $parameters, [], 'POST');
+ $responseBody = json_decode((string)$response->getBody(), true);
+ $expectedResponse = [
+ 'errors' => [
+ ErrorMessages::get('invalidServiceId'),
+ ErrorMessages::get('invalidTimestamp')
+ ]
+ ];
+ $this->assertEquals(ErrorMessages::get('invalidServiceId')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidTimestamp')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, $responseBody);
+ }
+
+ public function testMissingAllFields()
+ {
+ $this->setApiCalls([]);
+
+ $parameters = [];
+
+ $response = $this->render([], $parameters, [], 'POST');
+ $responseBody = json_decode((string)$response->getBody(), true);
+ $expectedResponse = [
+ 'errors' => [
+ ErrorMessages::get('invalidOfficeId'),
+ ErrorMessages::get('invalidServiceId'),
+ ErrorMessages::get('invalidTimestamp')
+ ]
+ ];
+ $this->assertEquals(ErrorMessages::get('invalidOfficeId')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidServiceId')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidTimestamp')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, $responseBody);
+ }
+
+ public function testInvalidOfficeIdFormat()
+ {
+ $this->setApiCalls([]);
+
+ $parameters = [
+ 'officeId' => 'invalid_id',
+ 'serviceId' => ['1063423'],
+ 'serviceCount' => [0],
+ 'timestamp' => "32526616522",
+ 'captchaSolution' => null
+ ];
+
+ $response = $this->render([], $parameters, [], 'POST');
+ $responseBody = json_decode((string)$response->getBody(), true);
+ $expectedResponse = [
+ 'errors' => [
+ ErrorMessages::get('invalidOfficeId')
+ ]
+ ];
+ $this->assertEquals(ErrorMessages::get('invalidOfficeId')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, $responseBody);
+ }
+
+ public function testInvalidServiceIdFormat()
+ {
+ $this->setApiCalls([]);
+
+ $parameters = [
+ 'officeId' => 10546,
+ 'serviceId' => ['invalid_service_id'],
+ 'serviceCount' => [0],
+ 'timestamp' => "32526616522",
+ 'captchaSolution' => null
+ ];
+
+ $response = $this->render([], $parameters, [], 'POST');
+ $responseBody = json_decode((string)$response->getBody(), true);
+ $expectedResponse = [
+ 'errors' => [
+ ErrorMessages::get('invalidServiceId')
+ ]
+ ];
+ $this->assertEquals(ErrorMessages::get('invalidServiceId')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, $responseBody);
+ }
+
+ public function testInvalidTimestampFormat()
+ {
+ $this->setApiCalls([]);
+
+ $parameters = [
+ 'officeId' => 10546,
+ 'serviceId' => ['1063423'],
+ 'serviceCount' => [0],
+ 'timestamp' => 'invalid_timestamp',
+ 'captchaSolution' => null
+ ];
+
+ $response = $this->render([], $parameters, [], 'POST');
+ $responseBody = json_decode((string)$response->getBody(), true);
+ $expectedResponse = [
+ 'errors' => [
+ ErrorMessages::get('invalidTimestamp')
+ ]
+ ];
+ $this->assertEquals(ErrorMessages::get('invalidTimestamp')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, $responseBody);
+ }
+ public function testEmptyServiceIdArray()
+ {
+ $this->setApiCalls([]);
+
+ $parameters = [
+ 'officeId' => 10546,
+ 'serviceId' => [],
+ 'serviceCount' => [0],
+ 'timestamp' => "32526616522",
+ 'captchaSolution' => null
+ ];
+
+ $response = $this->render([], $parameters, [], 'POST');
+ $responseBody = json_decode((string)$response->getBody(), true);
+ $expectedResponse = [
+ 'errors' => [
+ ErrorMessages::get('invalidServiceId')
+ ]
+ ];
+ $this->assertEquals(ErrorMessages::get('invalidServiceId')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, $responseBody);
+ }
+
+ public function testInvalidServiceCount()
+ {
+ $this->setApiCalls([]);
+
+ $parameters = [
+ 'officeId' => 10546,
+ 'serviceId' => ['1063423'],
+ 'serviceCount' => ['invalid'],
+ 'timestamp' => "32526616522",
+ 'captchaSolution' => null
+ ];
+
+ $response = $this->render([], $parameters, [], 'POST');
+ $responseBody = json_decode((string)$response->getBody(), true);
+ $expectedResponse = [
+ 'errors' => [
+ ErrorMessages::get('invalidServiceCount')
+ ]
+ ];
+ $this->assertEquals(ErrorMessages::get('invalidServiceCount')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, $responseBody);
+ }
+
+}
diff --git a/zmscitizenapi/tests/Zmscitizenapi/Controllers/Appointment/AppointmentUpdateTest.php b/zmscitizenapi/tests/Zmscitizenapi/Controllers/Appointment/AppointmentUpdateTest.php
new file mode 100644
index 000000000..67e1919fa
--- /dev/null
+++ b/zmscitizenapi/tests/Zmscitizenapi/Controllers/Appointment/AppointmentUpdateTest.php
@@ -0,0 +1,1865 @@
+clear();
+ }
+ }
+
+ public function testRendering()
+ {
+ $this->setApiCalls(
+ [
+ [
+ 'function' => 'readGetResult',
+ 'url' => '/process/101002/fb43/',
+ 'parameters' => [
+ 'resolveReferences' => 2,
+ ],
+ 'response' => $this->readFixture("GET_process.json")
+ ],
+ [
+ 'function' => 'readGetResult',
+ 'url' => '/source/unittest/',
+ 'parameters' => [
+ 'resolveReferences' => 2,
+ ],
+ 'response' => $this->readFixture("GET_SourceGet_dldb.json")
+ ],
+ [
+ 'function' => 'readPostResult',
+ 'url' => '/process/101002/fb43/',
+ 'response' => $this->readFixture("POST_update_appointment.json")
+ ]
+ ]
+ );
+
+ $parameters = [
+ 'processId' => '101002',
+ 'authKey' => 'fb43',
+ 'familyName' => 'TEST_USER',
+ 'email' => "test@muenchen.de",
+ 'telephone' => '123456789',
+ 'customTextfield' => "Some custom text",
+ ];
+ $response = $this->render([], $parameters, [], 'POST');
+ $responseBody = json_decode((string) $response->getBody(), true);
+ $expectedResponse = [
+ "processId" => 101002,
+ "timestamp" => "1727865900",
+ "authKey" => "fb43",
+ "familyName" => "TEST_USER",
+ "customTextfield" => "Some custom text",
+ "email" => "test@muenchen.de",
+ "telephone" => "123456789",
+ "officeName" => null,
+ "officeId" => 0,
+ "scope" => [
+ "id" => 0,
+ "provider" => null,
+ "shortName" => null,
+ "telephoneActivated" => null,
+ "telephoneRequired" => null,
+ "customTextfieldActivated" => null,
+ "customTextfieldRequired" => null,
+ "customTextfieldLabel" => null,
+ "captchaActivatedRequired" => null,
+ "displayInfo" => null
+ ],
+ "status" => "reserved",
+ "subRequestCounts" => [],
+ "serviceId" => 10242339,
+ "serviceCount" => 1
+ ];
+ $this->assertEquals(200, $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, $responseBody);
+ }
+
+ public function testTooManyEmailsAtLocation()
+ {
+
+ $exception = new \BO\Zmsclient\Exception();
+ $exception->template = 'BO\\Zmsapi\\Exception\\Process\\MoreThanAllowedAppointmentsPerMail';
+
+ $this->setApiCalls(
+ [
+ [
+ 'function' => 'readGetResult',
+ 'url' => '/process/101002/fb43/',
+ 'parameters' => [
+ 'resolveReferences' => 2,
+ ],
+ 'response' => $this->readFixture("GET_process.json")
+ ],
+ [
+ 'function' => 'readGetResult',
+ 'url' => '/source/unittest/',
+ 'parameters' => [
+ 'resolveReferences' => 2,
+ ],
+ 'response' => $this->readFixture("GET_SourceGet_dldb.json")
+ ],
+ [
+ 'function' => 'readPostResult',
+ 'url' => '/process/101002/fb43/',
+ 'exception' => $exception
+ ]
+ ]
+ );
+
+ $parameters = [
+ 'processId' => '101002',
+ 'authKey' => 'fb43',
+ 'familyName' => 'TEST_USER',
+ 'email' => "test@muenchen.de",
+ 'telephone' => '123456789',
+ 'customTextfield' => "Some custom text",
+ ];
+ $response = $this->render([], $parameters, [], 'POST');
+ $responseBody = json_decode((string) $response->getBody(), true);
+ $expectedResponse = [
+ 'errors' => [
+ ErrorMessages::get('tooManyAppointmentsWithSameMail')
+ ]
+ ];
+
+ $this->assertEquals(ErrorMessages::get('tooManyAppointmentsWithSameMail')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, $responseBody);
+ }
+
+ public function testAppointmentNotFoundException()
+ {
+
+ $exception = new \BO\Zmsclient\Exception();
+ $exception->template = 'BO\\Zmsapi\\Exception\\Process\\ProcessNotFound';
+
+ $this->setApiCalls(
+ [
+ [
+ 'function' => 'readGetResult',
+ 'url' => '/process/101003/fb43/',
+ 'parameters' => [
+ 'resolveReferences' => 2,
+ ],
+ 'exception' => $exception
+ ]
+ ]
+ );
+
+ $parameters = [
+ 'processId' => '101003',
+ 'authKey' => 'fb43',
+ 'familyName' => 'TEST_USER',
+ 'email' => "test@muenchen.de",
+ 'telephone' => '123456789',
+ 'customTextfield' => "Some custom text",
+ ];
+ $response = $this->render([], $parameters, [], 'POST');
+ $responseBody = json_decode((string) $response->getBody(), true);
+ $expectedResponse = [
+ 'errors' => [
+ ErrorMessages::get('appointmentNotFound')
+ ]
+ ];
+ $this->assertEquals(ErrorMessages::get('appointmentNotFound')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, $responseBody);
+ }
+
+ public function testAuthKeyMismatchException()
+ {
+
+ $exception = new \BO\Zmsclient\Exception();
+ $exception->template = 'BO\\Zmsapi\\Exception\\Process\\AuthKeyMatchFailed';
+
+ $this->setApiCalls(
+ [
+ [
+ 'function' => 'readGetResult',
+ 'url' => '/process/101003/wrongKey/',
+ 'parameters' => [
+ 'resolveReferences' => 2,
+ ],
+ 'exception' => $exception
+ ]
+ ]
+ );
+
+ $parameters = [
+ 'processId' => '101003',
+ 'authKey' => 'wrongKey',
+ 'familyName' => 'TEST_USER',
+ 'email' => "test@muenchen.de",
+ 'telephone' => '123456789',
+ 'customTextfield' => "Some custom text",
+ ];
+ $response = $this->render([], $parameters, [], 'POST');
+ $responseBody = json_decode((string) $response->getBody(), true);
+ $expectedResponse = [
+ 'errors' => [
+ ErrorMessages::get('authKeyMismatch')
+ ]
+ ];
+ $this->assertEquals(ErrorMessages::get('authKeyMismatch')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, $responseBody);
+ }
+
+ public function testInvalidProcessid_InvalidAuthkey_InvalidFamilyname_InvalidEmail_InvalidTelephone_InvalidCustomtextfield()
+ {
+ $parameters = [
+ 'processId' => null,
+ 'authKey' => '',
+ 'familyName' => '',
+ 'email' => 'invalid-email',
+ 'telephone' => '123',
+ 'customTextfield' => ''
+ ];
+ $response = $this->render([], $parameters, [], 'POST');
+ $responseBody = json_decode((string) $response->getBody(), true);
+ $expectedResponse = [
+ 'errors' => [
+ ErrorMessages::get('invalidProcessId'),
+ ErrorMessages::get('invalidAuthKey'),
+ ErrorMessages::get('invalidFamilyName'),
+ ErrorMessages::get('invalidEmail'),
+ ErrorMessages::get('invalidTelephone'),
+ ErrorMessages::get('invalidCustomTextfield')
+ ]
+ ];
+
+ $this->assertEquals(ErrorMessages::get('invalidProcessId')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidAuthKey')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidFamilyName')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidEmail')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidTelephone')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidCustomTextfield')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, $responseBody);
+ }
+
+ public function testInvalidProcessid_InvalidAuthkey_InvalidFamilyname_InvalidEmail_InvalidTelephone_ValidCustomtextfield()
+ {
+ $parameters = [
+ 'processId' => null,
+ 'authKey' => '',
+ 'familyName' => '',
+ 'email' => 'invalid-email',
+ 'telephone' => '123',
+ 'customTextfield' => 'Some custom text'
+ ];
+ $response = $this->render([], $parameters, [], 'POST');
+ $responseBody = json_decode((string) $response->getBody(), true);
+ $expectedResponse = [
+ 'errors' => [
+ ErrorMessages::get('invalidProcessId'),
+ ErrorMessages::get('invalidAuthKey'),
+ ErrorMessages::get('invalidFamilyName'),
+ ErrorMessages::get('invalidEmail'),
+ ErrorMessages::get('invalidTelephone')
+ ]
+ ];
+
+ $this->assertEquals(ErrorMessages::get('invalidProcessId')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidAuthKey')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidFamilyName')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidEmail')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidTelephone')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, $responseBody);
+ }
+
+ public function testInvalidProcessid_InvalidAuthkey_InvalidFamilyname_InvalidEmail_ValidTelephone_InvalidCustomtextfield()
+ {
+ $parameters = [
+ 'processId' => null,
+ 'authKey' => '',
+ 'familyName' => '',
+ 'email' => 'invalid-email',
+ 'telephone' => '123456789',
+ 'customTextfield' => ''
+ ];
+ $response = $this->render([], $parameters, [], 'POST');
+ $responseBody = json_decode((string) $response->getBody(), true);
+ $expectedResponse = [
+ 'errors' => [
+ ErrorMessages::get('invalidProcessId'),
+ ErrorMessages::get('invalidAuthKey'),
+ ErrorMessages::get('invalidFamilyName'),
+ ErrorMessages::get('invalidEmail'),
+ ErrorMessages::get('invalidCustomTextfield')
+ ]
+ ];
+
+ $this->assertEquals(ErrorMessages::get('invalidProcessId')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidAuthKey')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidFamilyName')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidEmail')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidCustomTextfield')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, $responseBody);
+ }
+
+ public function testInvalidProcessid_InvalidAuthkey_InvalidFamilyname_InvalidEmail_ValidTelephone_ValidCustomtextfield()
+ {
+ $parameters = [
+ 'processId' => null,
+ 'authKey' => '',
+ 'familyName' => '',
+ 'email' => 'invalid-email',
+ 'telephone' => '123456789',
+ 'customTextfield' => 'Some custom text'
+ ];
+ $response = $this->render([], $parameters, [], 'POST');
+ $responseBody = json_decode((string) $response->getBody(), true);
+ $expectedResponse = [
+ 'errors' => [
+ ErrorMessages::get('invalidProcessId'),
+ ErrorMessages::get('invalidAuthKey'),
+ ErrorMessages::get('invalidFamilyName'),
+ ErrorMessages::get('invalidEmail')
+ ]
+ ];
+
+ $this->assertEquals(ErrorMessages::get('invalidProcessId')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidAuthKey')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidFamilyName')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidEmail')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, $responseBody);
+ }
+
+ public function testInvalidProcessid_InvalidAuthkey_InvalidFamilyname_ValidEmail_InvalidTelephone_InvalidCustomtextfield()
+ {
+ $parameters = [
+ 'processId' => null,
+ 'authKey' => '',
+ 'familyName' => '',
+ 'email' => 'test@muenchen.de',
+ 'telephone' => '123',
+ 'customTextfield' => ''
+ ];
+ $response = $this->render([], $parameters, [], 'POST');
+ $responseBody = json_decode((string) $response->getBody(), true);
+ $expectedResponse = [
+ 'errors' => [
+ ErrorMessages::get('invalidProcessId'),
+ ErrorMessages::get('invalidAuthKey'),
+ ErrorMessages::get('invalidFamilyName'),
+ ErrorMessages::get('invalidTelephone'),
+ ErrorMessages::get('invalidCustomTextfield')
+ ]
+ ];
+
+ $this->assertEquals(ErrorMessages::get('invalidProcessId')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidAuthKey')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidFamilyName')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidTelephone')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidCustomTextfield')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, $responseBody);
+ }
+
+ public function testInvalidProcessid_InvalidAuthkey_InvalidFamilyname_ValidEmail_InvalidTelephone_ValidCustomtextfield()
+ {
+ $parameters = [
+ 'processId' => null,
+ 'authKey' => '',
+ 'familyName' => '',
+ 'email' => 'test@muenchen.de',
+ 'telephone' => '123',
+ 'customTextfield' => 'Some custom text'
+ ];
+ $response = $this->render([], $parameters, [], 'POST');
+ $responseBody = json_decode((string) $response->getBody(), true);
+ $expectedResponse = [
+ 'errors' => [
+ ErrorMessages::get('invalidProcessId'),
+ ErrorMessages::get('invalidAuthKey'),
+ ErrorMessages::get('invalidFamilyName'),
+ ErrorMessages::get('invalidTelephone')
+ ]
+ ];
+
+ $this->assertEquals(ErrorMessages::get('invalidProcessId')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidAuthKey')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidFamilyName')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidTelephone')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, $responseBody);
+ }
+
+ public function testInvalidProcessid_InvalidAuthkey_InvalidFamilyname_ValidEmail_ValidTelephone_InvalidCustomtextfield()
+ {
+ $parameters = [
+ 'processId' => null,
+ 'authKey' => '',
+ 'familyName' => '',
+ 'email' => 'test@muenchen.de',
+ 'telephone' => '123456789',
+ 'customTextfield' => ''
+ ];
+ $response = $this->render([], $parameters, [], 'POST');
+ $responseBody = json_decode((string) $response->getBody(), true);
+ $expectedResponse = [
+ 'errors' => [
+ ErrorMessages::get('invalidProcessId'),
+ ErrorMessages::get('invalidAuthKey'),
+ ErrorMessages::get('invalidFamilyName'),
+ ErrorMessages::get('invalidCustomTextfield')
+ ]
+ ];
+
+ $this->assertEquals(ErrorMessages::get('invalidProcessId')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidAuthKey')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidFamilyName')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidCustomTextfield')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, $responseBody);
+ }
+
+ public function testInvalidProcessid_InvalidAuthkey_InvalidFamilyname_ValidEmail_ValidTelephone_ValidCustomtextfield()
+ {
+ $parameters = [
+ 'processId' => null,
+ 'authKey' => '',
+ 'familyName' => '',
+ 'email' => 'test@muenchen.de',
+ 'telephone' => '123456789',
+ 'customTextfield' => 'Some custom text'
+ ];
+ $response = $this->render([], $parameters, [], 'POST');
+ $responseBody = json_decode((string) $response->getBody(), true);
+ $expectedResponse = [
+ 'errors' => [
+ ErrorMessages::get('invalidProcessId'),
+ ErrorMessages::get('invalidAuthKey'),
+ ErrorMessages::get('invalidFamilyName')
+ ]
+ ];
+
+ $this->assertEquals(ErrorMessages::get('invalidProcessId')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidAuthKey')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidFamilyName')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, $responseBody);
+ }
+
+ public function testInvalidProcessid_InvalidAuthkey_ValidFamilyname_InvalidEmail_InvalidTelephone_InvalidCustomtextfield()
+ {
+ $parameters = [
+ 'processId' => null,
+ 'authKey' => '',
+ 'familyName' => 'TEST_USER',
+ 'email' => 'invalid-email',
+ 'telephone' => '123',
+ 'customTextfield' => ''
+ ];
+ $response = $this->render([], $parameters, [], 'POST');
+ $responseBody = json_decode((string) $response->getBody(), true);
+ $expectedResponse = [
+ 'errors' => [
+ ErrorMessages::get('invalidProcessId'),
+ ErrorMessages::get('invalidAuthKey'),
+ ErrorMessages::get('invalidEmail'),
+ ErrorMessages::get('invalidTelephone'),
+ ErrorMessages::get('invalidCustomTextfield')
+ ]
+ ];
+
+ $this->assertEquals(ErrorMessages::get('invalidProcessId')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidAuthKey')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidEmail')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidTelephone')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidCustomTextfield')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, $responseBody);
+ }
+
+ public function testInvalidProcessid_InvalidAuthkey_ValidFamilyname_InvalidEmail_InvalidTelephone_ValidCustomtextfield()
+ {
+ $parameters = [
+ 'processId' => null,
+ 'authKey' => '',
+ 'familyName' => 'TEST_USER',
+ 'email' => 'invalid-email',
+ 'telephone' => '123',
+ 'customTextfield' => 'Some custom text'
+ ];
+ $response = $this->render([], $parameters, [], 'POST');
+ $responseBody = json_decode((string) $response->getBody(), true);
+ $expectedResponse = [
+ 'errors' => [
+ ErrorMessages::get('invalidProcessId'),
+ ErrorMessages::get('invalidAuthKey'),
+ ErrorMessages::get('invalidEmail'),
+ ErrorMessages::get('invalidTelephone')
+ ]
+ ];
+
+ $this->assertEquals(ErrorMessages::get('invalidProcessId')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidAuthKey')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidEmail')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidTelephone')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, $responseBody);
+ }
+
+ public function testInvalidProcessid_InvalidAuthkey_ValidFamilyname_InvalidEmail_ValidTelephone_InvalidCustomtextfield()
+ {
+ $parameters = [
+ 'processId' => null,
+ 'authKey' => '',
+ 'familyName' => 'TEST_USER',
+ 'email' => 'invalid-email',
+ 'telephone' => '123456789',
+ 'customTextfield' => ''
+ ];
+ $response = $this->render([], $parameters, [], 'POST');
+ $responseBody = json_decode((string) $response->getBody(), true);
+ $expectedResponse = [
+ 'errors' => [
+ ErrorMessages::get('invalidProcessId'),
+ ErrorMessages::get('invalidAuthKey'),
+ ErrorMessages::get('invalidEmail'),
+ ErrorMessages::get('invalidCustomTextfield')
+ ]
+ ];
+
+ $this->assertEquals(ErrorMessages::get('invalidProcessId')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidAuthKey')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidEmail')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidCustomTextfield')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, $responseBody);
+ }
+
+ public function testInvalidProcessid_InvalidAuthkey_ValidFamilyname_InvalidEmail_ValidTelephone_ValidCustomtextfield()
+ {
+ $parameters = [
+ 'processId' => null,
+ 'authKey' => '',
+ 'familyName' => 'TEST_USER',
+ 'email' => 'invalid-email',
+ 'telephone' => '123456789',
+ 'customTextfield' => 'Some custom text'
+ ];
+ $response = $this->render([], $parameters, [], 'POST');
+ $responseBody = json_decode((string) $response->getBody(), true);
+ $expectedResponse = [
+ 'errors' => [
+ ErrorMessages::get('invalidProcessId'),
+ ErrorMessages::get('invalidAuthKey'),
+ ErrorMessages::get('invalidEmail')
+ ]
+ ];
+
+ $this->assertEquals(ErrorMessages::get('invalidProcessId')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidAuthKey')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidEmail')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, $responseBody);
+ }
+
+ public function testInvalidProcessid_InvalidAuthkey_ValidFamilyname_ValidEmail_InvalidTelephone_InvalidCustomtextfield()
+ {
+ $parameters = [
+ 'processId' => null,
+ 'authKey' => '',
+ 'familyName' => 'TEST_USER',
+ 'email' => 'test@muenchen.de',
+ 'telephone' => '123',
+ 'customTextfield' => ''
+ ];
+ $response = $this->render([], $parameters, [], 'POST');
+ $responseBody = json_decode((string) $response->getBody(), true);
+ $expectedResponse = [
+ 'errors' => [
+ ErrorMessages::get('invalidProcessId'),
+ ErrorMessages::get('invalidAuthKey'),
+ ErrorMessages::get('invalidTelephone'),
+ ErrorMessages::get('invalidCustomTextfield')
+ ]
+ ];
+
+ $this->assertEquals(ErrorMessages::get('invalidProcessId')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidAuthKey')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidTelephone')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidCustomTextfield')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, $responseBody);
+ }
+
+ public function testInvalidProcessid_InvalidAuthkey_ValidFamilyname_ValidEmail_InvalidTelephone_ValidCustomtextfield()
+ {
+ $parameters = [
+ 'processId' => null,
+ 'authKey' => '',
+ 'familyName' => 'TEST_USER',
+ 'email' => 'test@muenchen.de',
+ 'telephone' => '123',
+ 'customTextfield' => 'Some custom text'
+ ];
+ $response = $this->render([], $parameters, [], 'POST');
+ $responseBody = json_decode((string) $response->getBody(), true);
+ $expectedResponse = [
+ 'errors' => [
+ ErrorMessages::get('invalidProcessId'),
+ ErrorMessages::get('invalidAuthKey'),
+ ErrorMessages::get('invalidTelephone')
+ ]
+ ];
+
+ $this->assertEquals(ErrorMessages::get('invalidProcessId')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidAuthKey')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidTelephone')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, $responseBody);
+ }
+
+ public function testInvalidProcessid_InvalidAuthkey_ValidFamilyname_ValidEmail_ValidTelephone_InvalidCustomtextfield()
+ {
+ $parameters = [
+ 'processId' => null,
+ 'authKey' => '',
+ 'familyName' => 'TEST_USER',
+ 'email' => 'test@muenchen.de',
+ 'telephone' => '123456789',
+ 'customTextfield' => ''
+ ];
+ $response = $this->render([], $parameters, [], 'POST');
+ $responseBody = json_decode((string) $response->getBody(), true);
+ $expectedResponse = [
+ 'errors' => [
+ ErrorMessages::get('invalidProcessId'),
+ ErrorMessages::get('invalidAuthKey'),
+ ErrorMessages::get('invalidCustomTextfield')
+ ]
+ ];
+
+ $this->assertEquals(ErrorMessages::get('invalidProcessId')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidAuthKey')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidCustomTextfield')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, $responseBody);
+ }
+
+ public function testInvalidProcessid_InvalidAuthkey_ValidFamilyname_ValidEmail_ValidTelephone_ValidCustomtextfield()
+ {
+ $parameters = [
+ 'processId' => null,
+ 'authKey' => '',
+ 'familyName' => 'TEST_USER',
+ 'email' => 'test@muenchen.de',
+ 'telephone' => '123456789',
+ 'customTextfield' => 'Some custom text'
+ ];
+ $response = $this->render([], $parameters, [], 'POST');
+ $responseBody = json_decode((string) $response->getBody(), true);
+ $expectedResponse = [
+ 'errors' => [
+ ErrorMessages::get('invalidProcessId'),
+ ErrorMessages::get('invalidAuthKey')
+ ]
+ ];
+
+ $this->assertEquals(ErrorMessages::get('invalidProcessId')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidAuthKey')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, $responseBody);
+ }
+
+ public function testInvalidProcessid_ValidAuthkey_InvalidFamilyname_InvalidEmail_InvalidTelephone_InvalidCustomtextfield()
+ {
+ $parameters = [
+ 'processId' => null,
+ 'authKey' => 'fb43',
+ 'familyName' => '',
+ 'email' => 'invalid-email',
+ 'telephone' => '123',
+ 'customTextfield' => ''
+ ];
+ $response = $this->render([], $parameters, [], 'POST');
+ $responseBody = json_decode((string) $response->getBody(), true);
+ $expectedResponse = [
+ 'errors' => [
+ ErrorMessages::get('invalidProcessId'),
+ ErrorMessages::get('invalidFamilyName'),
+ ErrorMessages::get('invalidEmail'),
+ ErrorMessages::get('invalidTelephone'),
+ ErrorMessages::get('invalidCustomTextfield')
+ ]
+ ];
+
+ $this->assertEquals(ErrorMessages::get('invalidProcessId')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidFamilyName')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidEmail')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidTelephone')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidCustomTextfield')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, $responseBody);
+ }
+
+ public function testInvalidProcessid_ValidAuthkey_InvalidFamilyname_InvalidEmail_InvalidTelephone_ValidCustomtextfield()
+ {
+ $parameters = [
+ 'processId' => null,
+ 'authKey' => 'fb43',
+ 'familyName' => '',
+ 'email' => 'invalid-email',
+ 'telephone' => '123',
+ 'customTextfield' => 'Some custom text'
+ ];
+ $response = $this->render([], $parameters, [], 'POST');
+ $responseBody = json_decode((string) $response->getBody(), true);
+ $expectedResponse = [
+ 'errors' => [
+ ErrorMessages::get('invalidProcessId'),
+ ErrorMessages::get('invalidFamilyName'),
+ ErrorMessages::get('invalidEmail'),
+ ErrorMessages::get('invalidTelephone')
+ ]
+ ];
+
+ $this->assertEquals(ErrorMessages::get('invalidProcessId')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidFamilyName')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidEmail')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidTelephone')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, $responseBody);
+ }
+
+ public function testInvalidProcessid_ValidAuthkey_InvalidFamilyname_InvalidEmail_ValidTelephone_InvalidCustomtextfield()
+ {
+ $parameters = [
+ 'processId' => null,
+ 'authKey' => 'fb43',
+ 'familyName' => '',
+ 'email' => 'invalid-email',
+ 'telephone' => '123456789',
+ 'customTextfield' => ''
+ ];
+ $response = $this->render([], $parameters, [], 'POST');
+ $responseBody = json_decode((string) $response->getBody(), true);
+ $expectedResponse = [
+ 'errors' => [
+ ErrorMessages::get('invalidProcessId'),
+ ErrorMessages::get('invalidFamilyName'),
+ ErrorMessages::get('invalidEmail'),
+ ErrorMessages::get('invalidCustomTextfield')
+ ]
+ ];
+
+ $this->assertEquals(ErrorMessages::get('invalidProcessId')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidFamilyName')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidEmail')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidCustomTextfield')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, $responseBody);
+ }
+
+ public function testInvalidProcessid_ValidAuthkey_InvalidFamilyname_InvalidEmail_ValidTelephone_ValidCustomtextfield()
+ {
+ $parameters = [
+ 'processId' => null,
+ 'authKey' => 'fb43',
+ 'familyName' => '',
+ 'email' => 'invalid-email',
+ 'telephone' => '123456789',
+ 'customTextfield' => 'Some custom text'
+ ];
+ $response = $this->render([], $parameters, [], 'POST');
+ $responseBody = json_decode((string) $response->getBody(), true);
+ $expectedResponse = [
+ 'errors' => [
+ ErrorMessages::get('invalidProcessId'),
+ ErrorMessages::get('invalidFamilyName'),
+ ErrorMessages::get('invalidEmail')
+ ]
+ ];
+
+ $this->assertEquals(ErrorMessages::get('invalidProcessId')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidFamilyName')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidEmail')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, $responseBody);
+ }
+
+ public function testInvalidProcessid_ValidAuthkey_InvalidFamilyname_ValidEmail_InvalidTelephone_InvalidCustomtextfield()
+ {
+ $parameters = [
+ 'processId' => null,
+ 'authKey' => 'fb43',
+ 'familyName' => '',
+ 'email' => 'test@muenchen.de',
+ 'telephone' => '123',
+ 'customTextfield' => ''
+ ];
+ $response = $this->render([], $parameters, [], 'POST');
+ $responseBody = json_decode((string) $response->getBody(), true);
+ $expectedResponse = [
+ 'errors' => [
+ ErrorMessages::get('invalidProcessId'),
+ ErrorMessages::get('invalidFamilyName'),
+ ErrorMessages::get('invalidTelephone'),
+ ErrorMessages::get('invalidCustomTextfield')
+ ]
+ ];
+
+ $this->assertEquals(ErrorMessages::get('invalidProcessId')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidFamilyName')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidTelephone')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidCustomTextfield')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, $responseBody);
+ }
+
+ public function testInvalidProcessid_ValidAuthkey_InvalidFamilyname_ValidEmail_InvalidTelephone_ValidCustomtextfield()
+ {
+ $parameters = [
+ 'processId' => null,
+ 'authKey' => 'fb43',
+ 'familyName' => '',
+ 'email' => 'test@muenchen.de',
+ 'telephone' => '123',
+ 'customTextfield' => 'Some custom text'
+ ];
+ $response = $this->render([], $parameters, [], 'POST');
+ $responseBody = json_decode((string) $response->getBody(), true);
+ $expectedResponse = [
+ 'errors' => [
+ ErrorMessages::get('invalidProcessId'),
+ ErrorMessages::get('invalidFamilyName'),
+ ErrorMessages::get('invalidTelephone')
+ ]
+ ];
+
+ $this->assertEquals(ErrorMessages::get('invalidProcessId')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidFamilyName')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidTelephone')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, $responseBody);
+ }
+
+ public function testInvalidProcessid_ValidAuthkey_InvalidFamilyname_ValidEmail_ValidTelephone_InvalidCustomtextfield()
+ {
+ $parameters = [
+ 'processId' => null,
+ 'authKey' => 'fb43',
+ 'familyName' => '',
+ 'email' => 'test@muenchen.de',
+ 'telephone' => '123456789',
+ 'customTextfield' => ''
+ ];
+ $response = $this->render([], $parameters, [], 'POST');
+ $responseBody = json_decode((string) $response->getBody(), true);
+ $expectedResponse = [
+ 'errors' => [
+ ErrorMessages::get('invalidProcessId'),
+ ErrorMessages::get('invalidFamilyName'),
+ ErrorMessages::get('invalidCustomTextfield')
+ ]
+ ];
+
+ $this->assertEquals(ErrorMessages::get('invalidProcessId')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidFamilyName')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidCustomTextfield')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, $responseBody);
+ }
+
+ public function testInvalidProcessid_ValidAuthkey_InvalidFamilyname_ValidEmail_ValidTelephone_ValidCustomtextfield()
+ {
+ $parameters = [
+ 'processId' => null,
+ 'authKey' => 'fb43',
+ 'familyName' => '',
+ 'email' => 'test@muenchen.de',
+ 'telephone' => '123456789',
+ 'customTextfield' => 'Some custom text'
+ ];
+ $response = $this->render([], $parameters, [], 'POST');
+ $responseBody = json_decode((string) $response->getBody(), true);
+ $expectedResponse = [
+ 'errors' => [
+ ErrorMessages::get('invalidProcessId'),
+ ErrorMessages::get('invalidFamilyName')
+ ]
+ ];
+
+ $this->assertEquals(ErrorMessages::get('invalidProcessId')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidFamilyName')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, $responseBody);
+ }
+
+ public function testInvalidProcessid_ValidAuthkey_ValidFamilyname_InvalidEmail_InvalidTelephone_InvalidCustomtextfield()
+ {
+ $parameters = [
+ 'processId' => null,
+ 'authKey' => 'fb43',
+ 'familyName' => 'TEST_USER',
+ 'email' => 'invalid-email',
+ 'telephone' => '123',
+ 'customTextfield' => ''
+ ];
+ $response = $this->render([], $parameters, [], 'POST');
+ $responseBody = json_decode((string) $response->getBody(), true);
+ $expectedResponse = [
+ 'errors' => [
+ ErrorMessages::get('invalidProcessId'),
+ ErrorMessages::get('invalidEmail'),
+ ErrorMessages::get('invalidTelephone'),
+ ErrorMessages::get('invalidCustomTextfield')
+ ]
+ ];
+
+ $this->assertEquals(ErrorMessages::get('invalidProcessId')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidEmail')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidTelephone')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidCustomTextfield')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, $responseBody);
+ }
+
+ public function testInvalidProcessid_ValidAuthkey_ValidFamilyname_InvalidEmail_InvalidTelephone_ValidCustomtextfield()
+ {
+ $parameters = [
+ 'processId' => null,
+ 'authKey' => 'fb43',
+ 'familyName' => 'TEST_USER',
+ 'email' => 'invalid-email',
+ 'telephone' => '123',
+ 'customTextfield' => 'Some custom text'
+ ];
+ $response = $this->render([], $parameters, [], 'POST');
+ $responseBody = json_decode((string) $response->getBody(), true);
+ $expectedResponse = [
+ 'errors' => [
+ ErrorMessages::get('invalidProcessId'),
+ ErrorMessages::get('invalidEmail'),
+ ErrorMessages::get('invalidTelephone')
+ ]
+ ];
+
+ $this->assertEquals(ErrorMessages::get('invalidProcessId')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidEmail')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidTelephone')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, $responseBody);
+ }
+
+ public function testInvalidProcessid_ValidAuthkey_ValidFamilyname_InvalidEmail_ValidTelephone_InvalidCustomtextfield()
+ {
+ $parameters = [
+ 'processId' => null,
+ 'authKey' => 'fb43',
+ 'familyName' => 'TEST_USER',
+ 'email' => 'invalid-email',
+ 'telephone' => '123456789',
+ 'customTextfield' => ''
+ ];
+ $response = $this->render([], $parameters, [], 'POST');
+ $responseBody = json_decode((string) $response->getBody(), true);
+ $expectedResponse = [
+ 'errors' => [
+ ErrorMessages::get('invalidProcessId'),
+ ErrorMessages::get('invalidEmail'),
+ ErrorMessages::get('invalidCustomTextfield')
+ ]
+ ];
+
+ $this->assertEquals(ErrorMessages::get('invalidProcessId')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidEmail')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidCustomTextfield')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, $responseBody);
+ }
+
+ public function testInvalidProcessid_ValidAuthkey_ValidFamilyname_InvalidEmail_ValidTelephone_ValidCustomtextfield()
+ {
+ $parameters = [
+ 'processId' => null,
+ 'authKey' => 'fb43',
+ 'familyName' => 'TEST_USER',
+ 'email' => 'invalid-email',
+ 'telephone' => '123456789',
+ 'customTextfield' => 'Some custom text'
+ ];
+ $response = $this->render([], $parameters, [], 'POST');
+ $responseBody = json_decode((string) $response->getBody(), true);
+ $expectedResponse = [
+ 'errors' => [
+ ErrorMessages::get('invalidProcessId'),
+ ErrorMessages::get('invalidEmail')
+ ]
+ ];
+
+ $this->assertEquals(ErrorMessages::get('invalidProcessId')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidEmail')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, $responseBody);
+ }
+
+ public function testInvalidProcessid_ValidAuthkey_ValidFamilyname_ValidEmail_InvalidTelephone_InvalidCustomtextfield()
+ {
+ $parameters = [
+ 'processId' => null,
+ 'authKey' => 'fb43',
+ 'familyName' => 'TEST_USER',
+ 'email' => 'test@muenchen.de',
+ 'telephone' => '123',
+ 'customTextfield' => ''
+ ];
+ $response = $this->render([], $parameters, [], 'POST');
+ $responseBody = json_decode((string) $response->getBody(), true);
+ $expectedResponse = [
+ 'errors' => [
+ ErrorMessages::get('invalidProcessId'),
+ ErrorMessages::get('invalidTelephone'),
+ ErrorMessages::get('invalidCustomTextfield')
+ ]
+ ];
+
+ $this->assertEquals(ErrorMessages::get('invalidProcessId')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidTelephone')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidCustomTextfield')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, $responseBody);
+ }
+
+ public function testInvalidProcessid_ValidAuthkey_ValidFamilyname_ValidEmail_InvalidTelephone_ValidCustomtextfield()
+ {
+ $parameters = [
+ 'processId' => null,
+ 'authKey' => 'fb43',
+ 'familyName' => 'TEST_USER',
+ 'email' => 'test@muenchen.de',
+ 'telephone' => '123',
+ 'customTextfield' => 'Some custom text'
+ ];
+ $response = $this->render([], $parameters, [], 'POST');
+ $responseBody = json_decode((string) $response->getBody(), true);
+ $expectedResponse = [
+ 'errors' => [
+ ErrorMessages::get('invalidProcessId'),
+ ErrorMessages::get('invalidTelephone')
+ ]
+ ];
+
+ $this->assertEquals(ErrorMessages::get('invalidProcessId')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidTelephone')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, $responseBody);
+ }
+
+ public function testInvalidProcessid_ValidAuthkey_ValidFamilyname_ValidEmail_ValidTelephone_InvalidCustomtextfield()
+ {
+ $parameters = [
+ 'processId' => null,
+ 'authKey' => 'fb43',
+ 'familyName' => 'TEST_USER',
+ 'email' => 'test@muenchen.de',
+ 'telephone' => '123456789',
+ 'customTextfield' => ''
+ ];
+ $response = $this->render([], $parameters, [], 'POST');
+ $responseBody = json_decode((string) $response->getBody(), true);
+ $expectedResponse = [
+ 'errors' => [
+ ErrorMessages::get('invalidProcessId'),
+ ErrorMessages::get('invalidCustomTextfield')
+ ]
+ ];
+
+ $this->assertEquals(ErrorMessages::get('invalidProcessId')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidCustomTextfield')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, $responseBody);
+ }
+
+ public function testInvalidProcessid_ValidAuthkey_ValidFamilyname_ValidEmail_ValidTelephone_ValidCustomtextfield()
+ {
+ $parameters = [
+ 'processId' => null,
+ 'authKey' => 'fb43',
+ 'familyName' => 'TEST_USER',
+ 'email' => 'test@muenchen.de',
+ 'telephone' => '123456789',
+ 'customTextfield' => 'Some custom text'
+ ];
+ $response = $this->render([], $parameters, [], 'POST');
+ $responseBody = json_decode((string) $response->getBody(), true);
+ $expectedResponse = [
+ 'errors' => [
+ ErrorMessages::get('invalidProcessId')
+ ]
+ ];
+
+ $this->assertEquals(ErrorMessages::get('invalidProcessId')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, $responseBody);
+ }
+
+ public function testValidProcessid_InvalidAuthkey_InvalidFamilyname_InvalidEmail_InvalidTelephone_InvalidCustomtextfield()
+ {
+ $parameters = [
+ 'processId' => '101002',
+ 'authKey' => '',
+ 'familyName' => '',
+ 'email' => 'invalid-email',
+ 'telephone' => '123',
+ 'customTextfield' => ''
+ ];
+ $response = $this->render([], $parameters, [], 'POST');
+ $responseBody = json_decode((string) $response->getBody(), true);
+ $expectedResponse = [
+ 'errors' => [
+ ErrorMessages::get('invalidAuthKey'),
+ ErrorMessages::get('invalidFamilyName'),
+ ErrorMessages::get('invalidEmail'),
+ ErrorMessages::get('invalidTelephone'),
+ ErrorMessages::get('invalidCustomTextfield')
+ ]
+ ];
+
+ $this->assertEquals(ErrorMessages::get('invalidAuthKey')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidFamilyName')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidEmail')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidTelephone')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidCustomTextfield')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, $responseBody);
+ }
+
+ public function testValidProcessid_InvalidAuthkey_InvalidFamilyname_InvalidEmail_InvalidTelephone_ValidCustomtextfield()
+ {
+ $parameters = [
+ 'processId' => '101002',
+ 'authKey' => '',
+ 'familyName' => '',
+ 'email' => 'invalid-email',
+ 'telephone' => '123',
+ 'customTextfield' => 'Some custom text'
+ ];
+ $response = $this->render([], $parameters, [], 'POST');
+ $responseBody = json_decode((string) $response->getBody(), true);
+ $expectedResponse = [
+ 'errors' => [
+ ErrorMessages::get('invalidAuthKey'),
+ ErrorMessages::get('invalidFamilyName'),
+ ErrorMessages::get('invalidEmail'),
+ ErrorMessages::get('invalidTelephone')
+ ]
+ ];
+
+ $this->assertEquals(ErrorMessages::get('invalidAuthKey')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidFamilyName')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidEmail')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidTelephone')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, $responseBody);
+ }
+
+ public function testValidProcessid_InvalidAuthkey_InvalidFamilyname_InvalidEmail_ValidTelephone_InvalidCustomtextfield()
+ {
+ $parameters = [
+ 'processId' => '101002',
+ 'authKey' => '',
+ 'familyName' => '',
+ 'email' => 'invalid-email',
+ 'telephone' => '123456789',
+ 'customTextfield' => ''
+ ];
+ $response = $this->render([], $parameters, [], 'POST');
+ $responseBody = json_decode((string) $response->getBody(), true);
+ $expectedResponse = [
+ 'errors' => [
+ ErrorMessages::get('invalidAuthKey'),
+ ErrorMessages::get('invalidFamilyName'),
+ ErrorMessages::get('invalidEmail'),
+ ErrorMessages::get('invalidCustomTextfield')
+ ]
+ ];
+
+ $this->assertEquals(ErrorMessages::get('invalidAuthKey')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidFamilyName')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidEmail')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidCustomTextfield')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, $responseBody);
+ }
+
+ public function testValidProcessid_InvalidAuthkey_InvalidFamilyname_InvalidEmail_ValidTelephone_ValidCustomtextfield()
+ {
+ $parameters = [
+ 'processId' => '101002',
+ 'authKey' => '',
+ 'familyName' => '',
+ 'email' => 'invalid-email',
+ 'telephone' => '123456789',
+ 'customTextfield' => 'Some custom text'
+ ];
+ $response = $this->render([], $parameters, [], 'POST');
+ $responseBody = json_decode((string) $response->getBody(), true);
+ $expectedResponse = [
+ 'errors' => [
+ ErrorMessages::get('invalidAuthKey'),
+ ErrorMessages::get('invalidFamilyName'),
+ ErrorMessages::get('invalidEmail')
+ ]
+ ];
+
+ $this->assertEquals(ErrorMessages::get('invalidAuthKey')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidFamilyName')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidEmail')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, $responseBody);
+ }
+
+ public function testValidProcessid_InvalidAuthkey_InvalidFamilyname_ValidEmail_InvalidTelephone_InvalidCustomtextfield()
+ {
+ $parameters = [
+ 'processId' => '101002',
+ 'authKey' => '',
+ 'familyName' => '',
+ 'email' => 'test@muenchen.de',
+ 'telephone' => '123',
+ 'customTextfield' => ''
+ ];
+ $response = $this->render([], $parameters, [], 'POST');
+ $responseBody = json_decode((string) $response->getBody(), true);
+ $expectedResponse = [
+ 'errors' => [
+ ErrorMessages::get('invalidAuthKey'),
+ ErrorMessages::get('invalidFamilyName'),
+ ErrorMessages::get('invalidTelephone'),
+ ErrorMessages::get('invalidCustomTextfield')
+ ]
+ ];
+
+ $this->assertEquals(ErrorMessages::get('invalidAuthKey')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidFamilyName')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidTelephone')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidCustomTextfield')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, $responseBody);
+ }
+
+ public function testValidProcessid_InvalidAuthkey_InvalidFamilyname_ValidEmail_InvalidTelephone_ValidCustomtextfield()
+ {
+ $parameters = [
+ 'processId' => '101002',
+ 'authKey' => '',
+ 'familyName' => '',
+ 'email' => 'test@muenchen.de',
+ 'telephone' => '123',
+ 'customTextfield' => 'Some custom text'
+ ];
+ $response = $this->render([], $parameters, [], 'POST');
+ $responseBody = json_decode((string) $response->getBody(), true);
+ $expectedResponse = [
+ 'errors' => [
+ ErrorMessages::get('invalidAuthKey'),
+ ErrorMessages::get('invalidFamilyName'),
+ ErrorMessages::get('invalidTelephone')
+ ]
+ ];
+
+ $this->assertEquals(ErrorMessages::get('invalidAuthKey')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidFamilyName')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidTelephone')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, $responseBody);
+ }
+
+ public function testValidProcessid_InvalidAuthkey_InvalidFamilyname_ValidEmail_ValidTelephone_InvalidCustomtextfield()
+ {
+ $parameters = [
+ 'processId' => '101002',
+ 'authKey' => '',
+ 'familyName' => '',
+ 'email' => 'test@muenchen.de',
+ 'telephone' => '123456789',
+ 'customTextfield' => ''
+ ];
+ $response = $this->render([], $parameters, [], 'POST');
+ $responseBody = json_decode((string) $response->getBody(), true);
+ $expectedResponse = [
+ 'errors' => [
+ ErrorMessages::get('invalidAuthKey'),
+ ErrorMessages::get('invalidFamilyName'),
+ ErrorMessages::get('invalidCustomTextfield')
+ ]
+ ];
+
+ $this->assertEquals(ErrorMessages::get('invalidAuthKey')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidFamilyName')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidCustomTextfield')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, $responseBody);
+ }
+
+ public function testValidProcessid_InvalidAuthkey_InvalidFamilyname_ValidEmail_ValidTelephone_ValidCustomtextfield()
+ {
+ $parameters = [
+ 'processId' => '101002',
+ 'authKey' => '',
+ 'familyName' => '',
+ 'email' => 'test@muenchen.de',
+ 'telephone' => '123456789',
+ 'customTextfield' => 'Some custom text'
+ ];
+ $response = $this->render([], $parameters, [], 'POST');
+ $responseBody = json_decode((string) $response->getBody(), true);
+ $expectedResponse = [
+ 'errors' => [
+ ErrorMessages::get('invalidAuthKey'),
+ ErrorMessages::get('invalidFamilyName')
+ ]
+ ];
+
+ $this->assertEquals(ErrorMessages::get('invalidAuthKey')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidFamilyName')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, $responseBody);
+ }
+
+ public function testValidProcessid_InvalidAuthkey_ValidFamilyname_InvalidEmail_InvalidTelephone_InvalidCustomtextfield()
+ {
+ $parameters = [
+ 'processId' => '101002',
+ 'authKey' => '',
+ 'familyName' => 'TEST_USER',
+ 'email' => 'invalid-email',
+ 'telephone' => '123',
+ 'customTextfield' => ''
+ ];
+ $response = $this->render([], $parameters, [], 'POST');
+ $responseBody = json_decode((string) $response->getBody(), true);
+ $expectedResponse = [
+ 'errors' => [
+ ErrorMessages::get('invalidAuthKey'),
+ ErrorMessages::get('invalidEmail'),
+ ErrorMessages::get('invalidTelephone'),
+ ErrorMessages::get('invalidCustomTextfield')
+ ]
+ ];
+
+ $this->assertEquals(ErrorMessages::get('invalidAuthKey')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidEmail')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidTelephone')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidCustomTextfield')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, $responseBody);
+ }
+
+ public function testValidProcessid_InvalidAuthkey_ValidFamilyname_InvalidEmail_InvalidTelephone_ValidCustomtextfield()
+ {
+ $parameters = [
+ 'processId' => '101002',
+ 'authKey' => '',
+ 'familyName' => 'TEST_USER',
+ 'email' => 'invalid-email',
+ 'telephone' => '123',
+ 'customTextfield' => 'Some custom text'
+ ];
+ $response = $this->render([], $parameters, [], 'POST');
+ $responseBody = json_decode((string) $response->getBody(), true);
+ $expectedResponse = [
+ 'errors' => [
+ ErrorMessages::get('invalidAuthKey'),
+ ErrorMessages::get('invalidEmail'),
+ ErrorMessages::get('invalidTelephone')
+ ]
+ ];
+
+ $this->assertEquals(ErrorMessages::get('invalidAuthKey')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidEmail')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidTelephone')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, $responseBody);
+ }
+
+ public function testValidProcessid_InvalidAuthkey_ValidFamilyname_InvalidEmail_ValidTelephone_InvalidCustomtextfield()
+ {
+ $parameters = [
+ 'processId' => '101002',
+ 'authKey' => '',
+ 'familyName' => 'TEST_USER',
+ 'email' => 'invalid-email',
+ 'telephone' => '123456789',
+ 'customTextfield' => ''
+ ];
+ $response = $this->render([], $parameters, [], 'POST');
+ $responseBody = json_decode((string) $response->getBody(), true);
+ $expectedResponse = [
+ 'errors' => [
+ ErrorMessages::get('invalidAuthKey'),
+ ErrorMessages::get('invalidEmail'),
+ ErrorMessages::get('invalidCustomTextfield')
+ ]
+ ];
+
+ $this->assertEquals(ErrorMessages::get('invalidAuthKey')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidEmail')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidCustomTextfield')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, $responseBody);
+ }
+
+ public function testValidProcessid_InvalidAuthkey_ValidFamilyname_InvalidEmail_ValidTelephone_ValidCustomtextfield()
+ {
+ $parameters = [
+ 'processId' => '101002',
+ 'authKey' => '',
+ 'familyName' => 'TEST_USER',
+ 'email' => 'invalid-email',
+ 'telephone' => '123456789',
+ 'customTextfield' => 'Some custom text'
+ ];
+ $response = $this->render([], $parameters, [], 'POST');
+ $responseBody = json_decode((string) $response->getBody(), true);
+ $expectedResponse = [
+ 'errors' => [
+ ErrorMessages::get('invalidAuthKey'),
+ ErrorMessages::get('invalidEmail')
+ ]
+ ];
+
+ $this->assertEquals(ErrorMessages::get('invalidAuthKey')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidEmail')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, $responseBody);
+ }
+
+ public function testValidProcessid_InvalidAuthkey_ValidFamilyname_ValidEmail_InvalidTelephone_InvalidCustomtextfield()
+ {
+ $parameters = [
+ 'processId' => '101002',
+ 'authKey' => '',
+ 'familyName' => 'TEST_USER',
+ 'email' => 'test@muenchen.de',
+ 'telephone' => '123',
+ 'customTextfield' => ''
+ ];
+ $response = $this->render([], $parameters, [], 'POST');
+ $responseBody = json_decode((string) $response->getBody(), true);
+ $expectedResponse = [
+ 'errors' => [
+ ErrorMessages::get('invalidAuthKey'),
+ ErrorMessages::get('invalidTelephone'),
+ ErrorMessages::get('invalidCustomTextfield')
+ ]
+ ];
+
+ $this->assertEquals(ErrorMessages::get('invalidAuthKey')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidTelephone')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidCustomTextfield')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, $responseBody);
+ }
+
+ public function testValidProcessid_InvalidAuthkey_ValidFamilyname_ValidEmail_InvalidTelephone_ValidCustomtextfield()
+ {
+ $parameters = [
+ 'processId' => '101002',
+ 'authKey' => '',
+ 'familyName' => 'TEST_USER',
+ 'email' => 'test@muenchen.de',
+ 'telephone' => '123',
+ 'customTextfield' => 'Some custom text'
+ ];
+ $response = $this->render([], $parameters, [], 'POST');
+ $responseBody = json_decode((string) $response->getBody(), true);
+ $expectedResponse = [
+ 'errors' => [
+ ErrorMessages::get('invalidAuthKey'),
+ ErrorMessages::get('invalidTelephone')
+ ]
+ ];
+
+ $this->assertEquals(ErrorMessages::get('invalidAuthKey')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidTelephone')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, $responseBody);
+ }
+
+ public function testValidProcessid_InvalidAuthkey_ValidFamilyname_ValidEmail_ValidTelephone_InvalidCustomtextfield()
+ {
+ $parameters = [
+ 'processId' => '101002',
+ 'authKey' => '',
+ 'familyName' => 'TEST_USER',
+ 'email' => 'test@muenchen.de',
+ 'telephone' => '123456789',
+ 'customTextfield' => ''
+ ];
+ $response = $this->render([], $parameters, [], 'POST');
+ $responseBody = json_decode((string) $response->getBody(), true);
+ $expectedResponse = [
+ 'errors' => [
+ ErrorMessages::get('invalidAuthKey'),
+ ErrorMessages::get('invalidCustomTextfield')
+ ]
+ ];
+
+ $this->assertEquals(ErrorMessages::get('invalidAuthKey')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidCustomTextfield')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, $responseBody);
+ }
+
+ public function testValidProcessid_InvalidAuthkey_ValidFamilyname_ValidEmail_ValidTelephone_ValidCustomtextfield()
+ {
+ $parameters = [
+ 'processId' => '101002',
+ 'authKey' => '',
+ 'familyName' => 'TEST_USER',
+ 'email' => 'test@muenchen.de',
+ 'telephone' => '123456789',
+ 'customTextfield' => 'Some custom text'
+ ];
+ $response = $this->render([], $parameters, [], 'POST');
+ $responseBody = json_decode((string) $response->getBody(), true);
+ $expectedResponse = [
+ 'errors' => [
+ ErrorMessages::get('invalidAuthKey')
+ ]
+ ];
+
+ $this->assertEquals(ErrorMessages::get('invalidAuthKey')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, $responseBody);
+ }
+
+ public function testValidProcessid_ValidAuthkey_InvalidFamilyname_InvalidEmail_InvalidTelephone_InvalidCustomtextfield()
+ {
+ $parameters = [
+ 'processId' => '101002',
+ 'authKey' => 'fb43',
+ 'familyName' => '',
+ 'email' => 'invalid-email',
+ 'telephone' => '123',
+ 'customTextfield' => ''
+ ];
+ $response = $this->render([], $parameters, [], 'POST');
+ $responseBody = json_decode((string) $response->getBody(), true);
+ $expectedResponse = [
+ 'errors' => [
+ ErrorMessages::get('invalidFamilyName'),
+ ErrorMessages::get('invalidEmail'),
+ ErrorMessages::get('invalidTelephone'),
+ ErrorMessages::get('invalidCustomTextfield')
+ ]
+ ];
+
+ $this->assertEquals(ErrorMessages::get('invalidFamilyName')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidEmail')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidTelephone')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidCustomTextfield')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, $responseBody);
+ }
+
+ public function testValidProcessid_ValidAuthkey_InvalidFamilyname_InvalidEmail_InvalidTelephone_ValidCustomtextfield()
+ {
+ $parameters = [
+ 'processId' => '101002',
+ 'authKey' => 'fb43',
+ 'familyName' => '',
+ 'email' => 'invalid-email',
+ 'telephone' => '123',
+ 'customTextfield' => 'Some custom text'
+ ];
+ $response = $this->render([], $parameters, [], 'POST');
+ $responseBody = json_decode((string) $response->getBody(), true);
+ $expectedResponse = [
+ 'errors' => [
+ ErrorMessages::get('invalidFamilyName'),
+ ErrorMessages::get('invalidEmail'),
+ ErrorMessages::get('invalidTelephone')
+ ]
+ ];
+
+ $this->assertEquals(ErrorMessages::get('invalidFamilyName')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidEmail')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidTelephone')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, $responseBody);
+ }
+
+ public function testValidProcessid_ValidAuthkey_InvalidFamilyname_InvalidEmail_ValidTelephone_InvalidCustomtextfield()
+ {
+ $parameters = [
+ 'processId' => '101002',
+ 'authKey' => 'fb43',
+ 'familyName' => '',
+ 'email' => 'invalid-email',
+ 'telephone' => '123456789',
+ 'customTextfield' => ''
+ ];
+ $response = $this->render([], $parameters, [], 'POST');
+ $responseBody = json_decode((string) $response->getBody(), true);
+ $expectedResponse = [
+ 'errors' => [
+ ErrorMessages::get('invalidFamilyName'),
+ ErrorMessages::get('invalidEmail'),
+ ErrorMessages::get('invalidCustomTextfield')
+ ]
+ ];
+
+ $this->assertEquals(ErrorMessages::get('invalidFamilyName')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidEmail')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidCustomTextfield')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, $responseBody);
+ }
+
+ public function testValidProcessid_ValidAuthkey_InvalidFamilyname_InvalidEmail_ValidTelephone_ValidCustomtextfield()
+ {
+ $parameters = [
+ 'processId' => '101002',
+ 'authKey' => 'fb43',
+ 'familyName' => '',
+ 'email' => 'invalid-email',
+ 'telephone' => '123456789',
+ 'customTextfield' => 'Some custom text'
+ ];
+ $response = $this->render([], $parameters, [], 'POST');
+ $responseBody = json_decode((string) $response->getBody(), true);
+ $expectedResponse = [
+ 'errors' => [
+ ErrorMessages::get('invalidFamilyName'),
+ ErrorMessages::get('invalidEmail')
+ ]
+ ];
+
+ $this->assertEquals(ErrorMessages::get('invalidFamilyName')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidEmail')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, $responseBody);
+ }
+
+ public function testValidProcessid_ValidAuthkey_InvalidFamilyname_ValidEmail_InvalidTelephone_InvalidCustomtextfield()
+ {
+ $parameters = [
+ 'processId' => '101002',
+ 'authKey' => 'fb43',
+ 'familyName' => '',
+ 'email' => 'test@muenchen.de',
+ 'telephone' => '123',
+ 'customTextfield' => ''
+ ];
+ $response = $this->render([], $parameters, [], 'POST');
+ $responseBody = json_decode((string) $response->getBody(), true);
+ $expectedResponse = [
+ 'errors' => [
+ ErrorMessages::get('invalidFamilyName'),
+ ErrorMessages::get('invalidTelephone'),
+ ErrorMessages::get('invalidCustomTextfield')
+ ]
+ ];
+
+ $this->assertEquals(ErrorMessages::get('invalidFamilyName')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidTelephone')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidCustomTextfield')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, $responseBody);
+ }
+
+ public function testValidProcessid_ValidAuthkey_InvalidFamilyname_ValidEmail_InvalidTelephone_ValidCustomtextfield()
+ {
+ $parameters = [
+ 'processId' => '101002',
+ 'authKey' => 'fb43',
+ 'familyName' => '',
+ 'email' => 'test@muenchen.de',
+ 'telephone' => '123',
+ 'customTextfield' => 'Some custom text'
+ ];
+ $response = $this->render([], $parameters, [], 'POST');
+ $responseBody = json_decode((string) $response->getBody(), true);
+ $expectedResponse = [
+ 'errors' => [
+ ErrorMessages::get('invalidFamilyName'),
+ ErrorMessages::get('invalidTelephone')
+ ]
+ ];
+
+ $this->assertEquals(ErrorMessages::get('invalidFamilyName')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidTelephone')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, $responseBody);
+ }
+
+ public function testValidProcessid_ValidAuthkey_InvalidFamilyname_ValidEmail_ValidTelephone_InvalidCustomtextfield()
+ {
+ $parameters = [
+ 'processId' => '101002',
+ 'authKey' => 'fb43',
+ 'familyName' => '',
+ 'email' => 'test@muenchen.de',
+ 'telephone' => '123456789',
+ 'customTextfield' => ''
+ ];
+ $response = $this->render([], $parameters, [], 'POST');
+ $responseBody = json_decode((string) $response->getBody(), true);
+ $expectedResponse = [
+ 'errors' => [
+ ErrorMessages::get('invalidFamilyName'),
+ ErrorMessages::get('invalidCustomTextfield')
+ ]
+ ];
+
+ $this->assertEquals(ErrorMessages::get('invalidFamilyName')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidCustomTextfield')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, $responseBody);
+ }
+
+ public function testValidProcessid_ValidAuthkey_InvalidFamilyname_ValidEmail_ValidTelephone_ValidCustomtextfield()
+ {
+ $parameters = [
+ 'processId' => '101002',
+ 'authKey' => 'fb43',
+ 'familyName' => '',
+ 'email' => 'test@muenchen.de',
+ 'telephone' => '123456789',
+ 'customTextfield' => 'Some custom text'
+ ];
+ $response = $this->render([], $parameters, [], 'POST');
+ $responseBody = json_decode((string) $response->getBody(), true);
+ $expectedResponse = [
+ 'errors' => [
+ ErrorMessages::get('invalidFamilyName')
+ ]
+ ];
+
+ $this->assertEquals(ErrorMessages::get('invalidFamilyName')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, $responseBody);
+ }
+
+ public function testValidProcessid_ValidAuthkey_ValidFamilyname_InvalidEmail_InvalidTelephone_InvalidCustomtextfield()
+ {
+ $parameters = [
+ 'processId' => '101002',
+ 'authKey' => 'fb43',
+ 'familyName' => 'TEST_USER',
+ 'email' => 'invalid-email',
+ 'telephone' => '123',
+ 'customTextfield' => ''
+ ];
+ $response = $this->render([], $parameters, [], 'POST');
+ $responseBody = json_decode((string) $response->getBody(), true);
+ $expectedResponse = [
+ 'errors' => [
+ ErrorMessages::get('invalidEmail'),
+ ErrorMessages::get('invalidTelephone'),
+ ErrorMessages::get('invalidCustomTextfield')
+ ]
+ ];
+
+ $this->assertEquals(ErrorMessages::get('invalidEmail')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidTelephone')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidCustomTextfield')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, $responseBody);
+ }
+
+ public function testValidProcessid_ValidAuthkey_ValidFamilyname_InvalidEmail_InvalidTelephone_ValidCustomtextfield()
+ {
+ $parameters = [
+ 'processId' => '101002',
+ 'authKey' => 'fb43',
+ 'familyName' => 'TEST_USER',
+ 'email' => 'invalid-email',
+ 'telephone' => '123',
+ 'customTextfield' => 'Some custom text'
+ ];
+ $response = $this->render([], $parameters, [], 'POST');
+ $responseBody = json_decode((string) $response->getBody(), true);
+ $expectedResponse = [
+ 'errors' => [
+ ErrorMessages::get('invalidEmail'),
+ ErrorMessages::get('invalidTelephone')
+ ]
+ ];
+
+ $this->assertEquals(ErrorMessages::get('invalidEmail')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidTelephone')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, $responseBody);
+ }
+
+ public function testValidProcessid_ValidAuthkey_ValidFamilyname_InvalidEmail_ValidTelephone_InvalidCustomtextfield()
+ {
+ $parameters = [
+ 'processId' => '101002',
+ 'authKey' => 'fb43',
+ 'familyName' => 'TEST_USER',
+ 'email' => 'invalid-email',
+ 'telephone' => '123456789',
+ 'customTextfield' => ''
+ ];
+ $response = $this->render([], $parameters, [], 'POST');
+ $responseBody = json_decode((string) $response->getBody(), true);
+ $expectedResponse = [
+ 'errors' => [
+ ErrorMessages::get('invalidEmail'),
+ ErrorMessages::get('invalidCustomTextfield')
+ ]
+ ];
+
+ $this->assertEquals(ErrorMessages::get('invalidEmail')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidCustomTextfield')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, $responseBody);
+ }
+
+ public function testValidProcessid_ValidAuthkey_ValidFamilyname_InvalidEmail_ValidTelephone_ValidCustomtextfield()
+ {
+ $parameters = [
+ 'processId' => '101002',
+ 'authKey' => 'fb43',
+ 'familyName' => 'TEST_USER',
+ 'email' => 'invalid-email',
+ 'telephone' => '123456789',
+ 'customTextfield' => 'Some custom text'
+ ];
+ $response = $this->render([], $parameters, [], 'POST');
+ $responseBody = json_decode((string) $response->getBody(), true);
+ $expectedResponse = [
+ 'errors' => [
+ ErrorMessages::get('invalidEmail')
+ ]
+ ];
+
+ $this->assertEquals(ErrorMessages::get('invalidEmail')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, $responseBody);
+ }
+
+ public function testValidProcessid_ValidAuthkey_ValidFamilyname_ValidEmail_InvalidTelephone_InvalidCustomtextfield()
+ {
+ $parameters = [
+ 'processId' => '101002',
+ 'authKey' => 'fb43',
+ 'familyName' => 'TEST_USER',
+ 'email' => 'test@muenchen.de',
+ 'telephone' => '123',
+ 'customTextfield' => ''
+ ];
+ $response = $this->render([], $parameters, [], 'POST');
+ $responseBody = json_decode((string) $response->getBody(), true);
+ $expectedResponse = [
+ 'errors' => [
+ ErrorMessages::get('invalidTelephone'),
+ ErrorMessages::get('invalidCustomTextfield')
+ ]
+ ];
+
+ $this->assertEquals(ErrorMessages::get('invalidTelephone')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidCustomTextfield')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, $responseBody);
+ }
+
+ public function testValidProcessid_ValidAuthkey_ValidFamilyname_ValidEmail_InvalidTelephone_ValidCustomtextfield()
+ {
+ $parameters = [
+ 'processId' => '101002',
+ 'authKey' => 'fb43',
+ 'familyName' => 'TEST_USER',
+ 'email' => 'test@muenchen.de',
+ 'telephone' => '123',
+ 'customTextfield' => 'Some custom text'
+ ];
+ $response = $this->render([], $parameters, [], 'POST');
+ $responseBody = json_decode((string) $response->getBody(), true);
+ $expectedResponse = [
+ 'errors' => [
+ ErrorMessages::get('invalidTelephone')
+ ]
+ ];
+
+ $this->assertEquals(ErrorMessages::get('invalidTelephone')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, $responseBody);
+ }
+
+ public function testValidProcessid_ValidAuthkey_ValidFamilyname_ValidEmail_ValidTelephone_InvalidCustomtextfield()
+ {
+ $parameters = [
+ 'processId' => '101002',
+ 'authKey' => 'fb43',
+ 'familyName' => 'TEST_USER',
+ 'email' => 'test@muenchen.de',
+ 'telephone' => '123456789',
+ 'customTextfield' => ''
+ ];
+ $response = $this->render([], $parameters, [], 'POST');
+ $responseBody = json_decode((string) $response->getBody(), true);
+ $expectedResponse = [
+ 'errors' => [
+ ErrorMessages::get('invalidCustomTextfield')
+ ]
+ ];
+
+ $this->assertEquals(ErrorMessages::get('invalidCustomTextfield')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, $responseBody);
+ }
+
+}
diff --git a/zmscitizenapi/tests/Zmscitizenapi/Controllers/Availability/AvailableAppointmentsListTest.php b/zmscitizenapi/tests/Zmscitizenapi/Controllers/Availability/AvailableAppointmentsListTest.php
new file mode 100644
index 000000000..3dab6f2f2
--- /dev/null
+++ b/zmscitizenapi/tests/Zmscitizenapi/Controllers/Availability/AvailableAppointmentsListTest.php
@@ -0,0 +1,297 @@
+clear();
+ }
+ }
+
+ public function testRendering()
+ {
+ $this->setApiCalls(
+ [
+ [
+ 'function' => 'readPostResult',
+ 'url' => '/process/status/free/',
+ 'response' => $this->readFixture("GET_appointments.json")
+ ]
+ ]
+ );
+
+ $parameters = [
+ 'date' => '3000-09-21',
+ 'officeId' => 10546,
+ 'serviceId' => '1063423',
+ 'serviceCount' => '1',
+ ];
+
+ $response = $this->render([], $parameters, []);
+ $responseBody = json_decode((string) $response->getBody(), true);
+ $expectedResponse = [
+ 'appointmentTimestamps' => [32526616522]
+ ];
+ $this->assertEquals(200, $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, $responseBody);
+ }
+
+
+ public function testEmptyAppointments()
+ {
+ $this->setApiCalls(
+ [
+ [
+ 'function' => 'readPostResult',
+ 'url' => '/process/status/free/',
+ 'response' => $this->readFixture("GET_appointments_empty.json")
+ ]
+ ]
+ );
+
+ $parameters = [
+ 'date' => '3000-09-21',
+ 'officeId' => 10546,
+ 'serviceId' => '1063423',
+ 'serviceCount' => '1',
+ ];
+
+ $response = $this->render([], $parameters, []);
+ $expectedResponse = [
+ 'errors' => [
+ ErrorMessages::get('appointmentNotAvailable')
+ ]
+ ];
+ $this->assertEquals(ErrorMessages::get('appointmentNotAvailable')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, json_decode((string) $response->getBody(), true));
+ }
+
+ public function testDateMissing()
+ {
+ $parameters = [
+ 'officeId' => 10546,
+ 'serviceId' => '1063423',
+ 'serviceCount' => '1',
+ ];
+
+ $response = $this->render([], $parameters, []);
+ $responseBody = json_decode((string) $response->getBody(), true);
+ $expectedResponse = [
+ 'errors' => [
+ ErrorMessages::get('invalidDate')
+ ]
+ ];
+ $this->assertEquals(ErrorMessages::get('invalidDate')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, $responseBody);
+ }
+
+ public function testOfficeIdMissing()
+ {
+ $parameters = [
+ 'date' => '3000-09-21',
+ 'serviceId' => '1063423',
+ 'serviceCount' => '1',
+ ];
+
+ $response = $this->render([], $parameters, []);
+ $responseBody = json_decode((string) $response->getBody(), true);
+ $expectedResponse = [
+ 'errors' => [
+ ErrorMessages::get('invalidOfficeId')
+ ]
+ ];
+ $this->assertEquals(ErrorMessages::get('invalidOfficeId')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, $responseBody);
+ }
+
+ public function testServiceIdMissing()
+ {
+ $parameters = [
+ 'date' => '3000-09-21',
+ 'officeId' => 10546,
+ 'serviceCount' => '1',
+ ];
+
+ $response = $this->render([], $parameters, []);
+ $responseBody = json_decode((string) $response->getBody(), true);
+ $expectedResponse = [
+ 'errors' => [
+ ErrorMessages::get('invalidServiceId')
+ ]
+ ];
+ $this->assertEquals(ErrorMessages::get('invalidServiceId')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, $responseBody);
+ }
+
+ public function testServiceCountMissing()
+ {
+ $parameters = [
+ 'date' => '3000-09-21',
+ 'officeId' => 10546,
+ 'serviceId' => '1063423',
+ ];
+
+ $response = $this->render([], $parameters, []);
+ $responseBody = json_decode((string) $response->getBody(), true);
+ $expectedResponse = [
+ 'errors' => [
+ ErrorMessages::get('invalidServiceCount')
+ ]
+ ];
+ $this->assertEquals(ErrorMessages::get('invalidServiceCount')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, $responseBody);
+ }
+
+ public function testDateAndOfficeIdMissing()
+ {
+ $parameters = [
+ 'serviceId' => '1063423',
+ 'serviceCount' => '1',
+ ];
+
+ $response = $this->render([], $parameters, []);
+ $responseBody = json_decode((string) $response->getBody(), true);
+ $expectedResponse = [
+ 'errors' => [
+ ErrorMessages::get('invalidDate'),
+ ErrorMessages::get('invalidOfficeId')
+ ]
+ ];
+ $this->assertEquals(ErrorMessages::get('invalidDate')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidOfficeId')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, $responseBody);
+ }
+
+ public function testDateAndServiceIdMissing()
+ {
+ $parameters = [
+ 'officeId' => 10546,
+ 'serviceCount' => '1',
+ ];
+
+ $response = $this->render([], $parameters, []);
+ $responseBody = json_decode((string) $response->getBody(), true);
+ $expectedResponse = [
+ 'errors' => [
+ ErrorMessages::get('invalidDate'),
+ ErrorMessages::get('invalidServiceId')
+ ]
+ ];
+ $this->assertEquals(ErrorMessages::get('invalidDate')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidServiceId')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, $responseBody);
+ }
+
+ public function testDateAndServiceCountMissing()
+ {
+ $parameters = [
+ 'officeId' => 10546,
+ 'serviceId' => '1063423',
+ ];
+
+ $response = $this->render([], $parameters, []);
+ $responseBody = json_decode((string) $response->getBody(), true);
+ $expectedResponse = [
+ 'errors' => [
+ ErrorMessages::get('invalidDate'),
+ ErrorMessages::get('invalidServiceCount')
+ ]
+ ];
+ $this->assertEquals(ErrorMessages::get('invalidDate')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidServiceCount')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, $responseBody);
+ }
+
+ public function testOfficeIdAndServiceIdMissing()
+ {
+ $parameters = [
+ 'date' => '3000-09-21',
+ 'serviceCount' => '1',
+ ];
+
+ $response = $this->render([], $parameters, []);
+ $responseBody = json_decode((string) $response->getBody(), true);
+ $expectedResponse = [
+ 'errors' => [
+ ErrorMessages::get('invalidOfficeId'),
+ ErrorMessages::get('invalidServiceId')
+ ]
+ ];
+ $this->assertEquals(ErrorMessages::get('invalidOfficeId')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidServiceId')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, $responseBody);
+ }
+
+ public function testOfficeIdAndServiceCountMissing()
+ {
+ $parameters = [
+ 'date' => '3000-09-21',
+ 'serviceId' => '1063423',
+ ];
+
+ $response = $this->render([], $parameters, []);
+ $responseBody = json_decode((string) $response->getBody(), true);
+ $expectedResponse = [
+ 'errors' => [
+ ErrorMessages::get('invalidOfficeId'),
+ ErrorMessages::get('invalidServiceCount')
+ ]
+ ];
+ $this->assertEquals(ErrorMessages::get('invalidOfficeId')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidServiceCount')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, $responseBody);
+ }
+
+ public function testServiceIdAndServiceCountMissing()
+ {
+ $parameters = [
+ 'date' => '3000-09-21',
+ 'officeId' => 10546,
+ ];
+
+ $response = $this->render([], $parameters, []);
+ $responseBody = json_decode((string) $response->getBody(), true);
+ $expectedResponse = [
+ 'errors' => [
+ ErrorMessages::get('invalidServiceId'),
+ ErrorMessages::get('invalidServiceCount')
+ ]
+ ];
+ $this->assertEquals(ErrorMessages::get('invalidServiceId')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidServiceCount')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, $responseBody);
+ }
+
+ public function testInvalidDateFormat()
+ {
+ $parameters = [
+ 'date' => '21-09-3000',
+ 'officeId' => 10546,
+ 'serviceId' => '1063423',
+ 'serviceCount' => '1',
+ ];
+
+ $response = $this->render([], $parameters, []);
+ $responseBody = json_decode((string) $response->getBody(), true);
+ $expectedResponse = [
+ 'errors' => [
+ ErrorMessages::get('invalidDate')
+ ]
+ ];
+ $this->assertEquals(ErrorMessages::get('invalidDate')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, $responseBody);
+ }
+
+}
diff --git a/zmscitizenapi/tests/Zmscitizenapi/Controllers/Availability/AvailableDaysListTest.php b/zmscitizenapi/tests/Zmscitizenapi/Controllers/Availability/AvailableDaysListTest.php
new file mode 100644
index 000000000..1bbf5bad5
--- /dev/null
+++ b/zmscitizenapi/tests/Zmscitizenapi/Controllers/Availability/AvailableDaysListTest.php
@@ -0,0 +1,531 @@
+clear();
+ }
+ }
+
+ public function testRendering()
+ {
+ $this->setApiCalls(
+ [
+ [
+ 'function' => 'readPostResult',
+ 'url' => '/calendar/',
+ 'response' => $this->readFixture("GET_calendar.json")
+ ]
+ ]
+ );
+ $parameters = [
+ 'officeId' => '9999998',
+ 'serviceId' => '1',
+ 'startDate' => '2024-08-21',
+ 'endDate' => '2024-08-23',
+ 'serviceCount' => '1',
+ ];
+ $response = $this->render([], $parameters, []);
+ $responseBody = json_decode((string)$response->getBody(), true);
+ $expectedResponse = [
+ 'availableDays' => [
+ "2024-08-21", "2024-08-22", "2024-08-23", "2024-08-26", "2024-08-27", "2024-08-28", "2024-08-29", "2024-08-30",
+ "2024-09-02", "2024-09-03", "2024-09-04", "2024-09-05", "2024-09-06", "2024-09-09", "2024-09-10", "2024-09-11",
+ "2024-09-12", "2024-09-13", "2024-09-16", "2024-09-17", "2024-09-18", "2024-09-19", "2024-09-20"
+ ]
+ ];
+ $this->assertEquals(200, $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, $responseBody);
+
+ }
+
+ public function testNoAvailableDays()
+ {
+ $this->setApiCalls(
+ [
+ [
+ 'function' => 'readPostResult',
+ 'url' => '/calendar/',
+ 'response' => $this->readFixture("GET_calendar_empty_days.json")
+ ]
+ ]
+ );
+
+ $parameters = [
+ 'officeId' => '9999998',
+ 'serviceId' => '1',
+ 'serviceCount' => '1',
+ 'startDate' => '2024-08-21',
+ 'endDate' => '2024-08-23',
+ ];
+ $response = $this->render([], $parameters, []);
+ $responseBody = json_decode((string)$response->getBody(), true);
+ $expectedResponse = [
+ 'errors' => [
+ ErrorMessages::get('noAppointmentForThisDay')
+ ]
+ ];
+ $this->assertEquals(ErrorMessages::get('noAppointmentForThisDay')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, $responseBody);
+ }
+
+ public function testInvalidDateFormat()
+ {
+ $parameters = [
+ 'officeId' => '9999998',
+ 'serviceId' => '1',
+ 'serviceCount' => '1',
+ 'startDate' => 'invalid-date',
+ 'endDate' => 'invalid-date',
+ ];
+ $response = $this->render([], $parameters, []);
+ $responseBody = json_decode((string)$response->getBody(), true);
+ $expectedResponse = [
+ 'errors' => [
+ ErrorMessages::get('invalidStartDate'),
+ ErrorMessages::get('invalidEndDate')
+ ],
+ ];
+ $this->assertEquals(ErrorMessages::get('invalidStartDate')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidEndDate')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, $responseBody);
+
+ }
+
+ public function testMissingStartDate()
+ {
+ $parameters = [
+ 'endDate' => '2024-09-04',
+ 'officeId' => '102522',
+ 'serviceId' => '1063424',
+ 'serviceCount' => '1',
+ ];
+
+ $response = $this->render([], $parameters, []);
+ $responseBody = json_decode((string)$response->getBody(), true);
+ $expectedResponse = [
+ 'errors' => [
+ ErrorMessages::get('invalidStartDate')
+ ],
+ ];
+ $this->assertEquals(ErrorMessages::get('invalidStartDate')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, $responseBody);
+ }
+
+ public function testMissingEndDate()
+ {
+ $parameters = [
+ 'startDate' => '2024-08-29',
+ 'officeId' => '102522',
+ 'serviceId' => '1063424',
+ 'serviceCount' => '1',
+ ];
+
+ $response = $this->render([], $parameters, []);
+ $responseBody = json_decode((string)$response->getBody(), true);
+ $expectedResponse = [
+ 'errors' => [
+ ErrorMessages::get('invalidEndDate')
+ ],
+ ];
+ $this->assertEquals(ErrorMessages::get('invalidEndDate')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, $responseBody);
+ }
+
+ public function testMissingOfficeId()
+ {
+ $parameters = [
+ 'startDate' => '2024-08-29',
+ 'endDate' => '2024-09-04',
+ 'serviceId' => '1063424',
+ 'serviceCount' => '1',
+ ];
+
+ $response = $this->render([], $parameters, []);
+ $responseBody = json_decode((string)$response->getBody(), true);
+ $expectedResponse = [
+ 'errors' => [
+ ErrorMessages::get('invalidOfficeId')
+ ],
+ ];
+ $this->assertEquals(ErrorMessages::get('invalidOfficeId')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, $responseBody);
+ }
+
+ public function testMissingServiceId()
+ {
+ $parameters = [
+ 'startDate' => '2024-08-29',
+ 'endDate' => '2024-09-04',
+ 'officeId' => '102522',
+ 'serviceCount' => '1',
+ ];
+
+ $response = $this->render([], $parameters, []);
+ $responseBody = json_decode((string)$response->getBody(), true);
+ $expectedResponse = [
+ 'errors' => [
+ ErrorMessages::get('invalidServiceId')
+ ],
+ ];
+ $this->assertEquals(ErrorMessages::get('invalidServiceId')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, $responseBody);
+ }
+
+ public function testMissingServiceCount()
+ {
+ $parameters = [
+ 'startDate' => '2024-08-29',
+ 'endDate' => '2024-09-04',
+ 'officeId' => '102522',
+ 'serviceId' => '1063424',
+ ];
+
+ $response = $this->render([], $parameters, []);
+ $responseBody = json_decode((string)$response->getBody(), true);
+ $expectedResponse = [
+ 'errors' => [
+ ErrorMessages::get('invalidServiceCount')
+ ],
+ ];
+ $this->assertEquals(ErrorMessages::get('invalidServiceCount')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, $responseBody);
+ }
+
+ public function testEmptyServiceCount()
+ {
+ $parameters = [
+ 'startDate' => '2024-08-29',
+ 'endDate' => '2024-09-04',
+ 'officeId' => '102522',
+ 'serviceId' => '1063424',
+ 'serviceCount' => '',
+ ];
+
+ $response = $this->render([], $parameters, []);
+ $responseBody = json_decode((string)$response->getBody(), true);
+ $expectedResponse = [
+ 'errors' => [
+ ErrorMessages::get('invalidServiceCount')
+ ],
+ ];
+ $this->assertEquals(ErrorMessages::get('invalidServiceCount')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, $responseBody);
+ }
+
+ public function testInvalidServiceCountFormat()
+ {
+ $parameters = [
+ 'startDate' => '2024-08-29',
+ 'endDate' => '2024-09-04',
+ 'officeId' => '102522',
+ 'serviceId' => '1063424',
+ 'serviceCount' => 'one,two',
+ ];
+
+ $response = $this->render([], $parameters, []);
+ $responseBody = json_decode((string)$response->getBody(), true);
+ $expectedResponse = [
+ 'errors' => [
+ ErrorMessages::get('invalidServiceCount')
+ ],
+ ];
+ $this->assertEquals(ErrorMessages::get('invalidServiceCount')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, $responseBody);
+ }
+
+ public function testAllParametersMissing()
+ {
+ $parameters = [];
+
+ $response = $this->render([], $parameters, []);
+ $responseBody = json_decode((string)$response->getBody(), true);
+ $expectedResponse = [
+ 'errors' => [
+ ErrorMessages::get('invalidStartDate'),
+ ErrorMessages::get('invalidEndDate'),
+ ErrorMessages::get('invalidOfficeId'),
+ ErrorMessages::get('invalidServiceId'),
+ ErrorMessages::get('invalidServiceCount')
+ ],
+ ];
+ $this->assertEquals(ErrorMessages::get('invalidStartDate')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidEndDate')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidOfficeId')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidServiceId')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidServiceCount')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, $responseBody);
+ }
+
+ public function testMissingStartDateAndEndDate()
+ {
+ $parameters = [
+ 'officeId' => '102522',
+ 'serviceId' => '1063424',
+ 'serviceCount' => '1',
+ ];
+
+ $response = $this->render([], $parameters, []);
+ $responseBody = json_decode((string)$response->getBody(), true);
+ $expectedResponse = [
+ 'errors' => [
+ ErrorMessages::get('invalidStartDate'),
+ ErrorMessages::get('invalidEndDate')
+ ],
+ ];
+ $this->assertEquals(ErrorMessages::get('invalidStartDate')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidEndDate')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, $responseBody);
+ }
+
+ public function testMissingOfficeIdAndServiceId()
+ {
+ $parameters = [
+ 'startDate' => '2024-08-29',
+ 'endDate' => '2024-09-04',
+ 'serviceCount' => '1',
+ ];
+
+ $response = $this->render([], $parameters, []);
+ $responseBody = json_decode((string)$response->getBody(), true);
+ $expectedResponse = [
+ 'errors' => [
+ ErrorMessages::get('invalidOfficeId'),
+ ErrorMessages::get('invalidServiceId')
+ ],
+ ];
+ $this->assertEquals(ErrorMessages::get('invalidOfficeId')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidServiceId')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, $responseBody);
+ }
+
+ public function testMissingServiceIdAndServiceCount()
+ {
+ $parameters = [
+ 'startDate' => '2024-08-29',
+ 'endDate' => '2024-09-04',
+ 'officeId' => '102522',
+ ];
+
+ $response = $this->render([], $parameters, []);
+ $responseBody = json_decode((string)$response->getBody(), true);
+ $expectedResponse = [
+ 'errors' => [
+ ErrorMessages::get('invalidServiceId'),
+ ErrorMessages::get('invalidServiceCount')
+ ],
+ ];
+ $this->assertEquals(ErrorMessages::get('invalidServiceId')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidServiceCount')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, $responseBody);
+ }
+
+ public function testMissingStartDateAndOfficeId()
+ {
+ $parameters = [
+ 'endDate' => '2024-09-04',
+ 'serviceId' => '1063424',
+ 'serviceCount' => '1',
+ ];
+
+ $response = $this->render([], $parameters, []);
+ $responseBody = json_decode((string)$response->getBody(), true);
+ $expectedResponse = [
+ 'errors' => [
+ ErrorMessages::get('invalidStartDate'),
+ ErrorMessages::get('invalidOfficeId')
+ ],
+ ];
+ $this->assertEquals(ErrorMessages::get('invalidStartDate')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidOfficeId')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, $responseBody);
+ }
+
+ public function testMissingEndDateAndServiceCount()
+ {
+ $parameters = [
+ 'startDate' => '2024-08-29',
+ 'officeId' => '102522',
+ 'serviceId' => '1063424',
+ ];
+
+ $response = $this->render([], $parameters, []);
+ $responseBody = json_decode((string)$response->getBody(), true);
+ $expectedResponse = [
+ 'errors' => [
+ ErrorMessages::get('invalidEndDate'),
+ ErrorMessages::get('invalidServiceCount')
+ ],
+ ];
+ $this->assertEquals(ErrorMessages::get('invalidEndDate')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidServiceCount')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, $responseBody);
+ }
+
+ public function testMissingOfficeIdAndServiceCount()
+ {
+ $parameters = [
+ 'startDate' => '2024-08-29',
+ 'endDate' => '2024-09-04',
+ 'serviceId' => '1063424',
+ ];
+
+ $response = $this->render([], $parameters, []);
+ $responseBody = json_decode((string)$response->getBody(), true);
+ $expectedResponse = [
+ 'errors' => [
+ ErrorMessages::get('invalidOfficeId'),
+ ErrorMessages::get('invalidServiceCount')
+ ],
+ ];
+ $this->assertEquals(ErrorMessages::get('invalidOfficeId')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidServiceCount')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, $responseBody);
+ }
+
+ public function testMissingStartDateEndDateAndOfficeId()
+ {
+ $parameters = [
+ 'serviceId' => '1063424',
+ 'serviceCount' => '1',
+ ];
+
+ $response = $this->render([], $parameters, []);
+ $responseBody = json_decode((string)$response->getBody(), true);
+ $expectedResponse = [
+ 'errors' => [
+ ErrorMessages::get('invalidStartDate'),
+ ErrorMessages::get('invalidEndDate'),
+ ErrorMessages::get('invalidOfficeId')
+ ],
+ ];
+ $this->assertEquals(ErrorMessages::get('invalidStartDate')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidEndDate')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidOfficeId')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, $responseBody);
+ }
+
+ public function testMissingStartDateEndDateAndServiceId()
+ {
+ $parameters = [
+ 'officeId' => '102522',
+ 'serviceCount' => '1',
+ ];
+
+ $response = $this->render([], $parameters, []);
+ $responseBody = json_decode((string)$response->getBody(), true);
+ $expectedResponse = [
+ 'errors' => [
+ ErrorMessages::get('invalidStartDate'),
+ ErrorMessages::get('invalidEndDate'),
+ ErrorMessages::get('invalidServiceId')
+ ],
+ ];
+ $this->assertEquals(ErrorMessages::get('invalidStartDate')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidEndDate')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidServiceId')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, $responseBody);
+ }
+
+ public function testMissingStartDateOfficeIdAndServiceCount()
+ {
+ $parameters = [
+ 'endDate' => '2024-09-04',
+ 'serviceId' => '1063424',
+ ];
+
+ $response = $this->render([], $parameters, []);
+ $responseBody = json_decode((string)$response->getBody(), true);
+ $expectedResponse = [
+ 'errors' => [
+ ErrorMessages::get('invalidStartDate'),
+ ErrorMessages::get('invalidOfficeId'),
+ ErrorMessages::get('invalidServiceCount')
+ ],
+ ];
+ $this->assertEquals(ErrorMessages::get('invalidStartDate')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidOfficeId')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidServiceCount')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, $responseBody);
+ }
+
+ public function testMissingEndDateOfficeIdAndServiceCount()
+ {
+ $parameters = [
+ 'startDate' => '2024-08-29',
+ 'serviceId' => '1063424',
+ ];
+
+ $response = $this->render([], $parameters, []);
+ $responseBody = json_decode((string)$response->getBody(), true);
+ $expectedResponse = [
+ 'errors' => [
+ ErrorMessages::get('invalidEndDate'),
+ ErrorMessages::get('invalidOfficeId'),
+ ErrorMessages::get('invalidServiceCount')
+ ],
+ ];
+ $this->assertEquals(ErrorMessages::get('invalidEndDate')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidOfficeId')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidServiceCount')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, $responseBody);
+ }
+
+ public function testEmptyStartDateAndEndDate()
+ {
+ $parameters = [
+ 'startDate' => '',
+ 'endDate' => '',
+ 'officeId' => '102522',
+ 'serviceId' => '1063424',
+ 'serviceCount' => '1',
+ ];
+
+ $response = $this->render([], $parameters, []);
+ $responseBody = json_decode((string)$response->getBody(), true);
+ $expectedResponse = [
+ 'errors' => [
+ ErrorMessages::get('invalidStartDate'),
+ ErrorMessages::get('invalidEndDate')
+ ],
+ ];
+ $this->assertEquals(ErrorMessages::get('invalidStartDate')['statusCode'], $response->getStatusCode());
+ $this->assertEquals(ErrorMessages::get('invalidEndDate')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, $responseBody);
+ }
+
+ public function testNonNumericServiceCount()
+ {
+ $parameters = [
+ 'startDate' => '2024-08-29',
+ 'endDate' => '2024-09-04',
+ 'officeId' => '102522',
+ 'serviceId' => '1063424',
+ 'serviceCount' => 'abc,123',
+ ];
+
+ $response = $this->render([], $parameters, []);
+ $responseBody = json_decode((string)$response->getBody(), true);
+ $expectedResponse = [
+ 'errors' => [
+ ErrorMessages::get('invalidServiceCount')
+ ],
+ ];
+ $this->assertEquals(ErrorMessages::get('invalidServiceCount')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, $responseBody);
+ }
+
+}
diff --git a/zmscitizenapi/tests/Zmscitizenapi/Controllers/Office/OfficeListByServiceTest.php b/zmscitizenapi/tests/Zmscitizenapi/Controllers/Office/OfficeListByServiceTest.php
new file mode 100644
index 000000000..31f0f803a
--- /dev/null
+++ b/zmscitizenapi/tests/Zmscitizenapi/Controllers/Office/OfficeListByServiceTest.php
@@ -0,0 +1,224 @@
+clear();
+ }
+ }
+
+ public function testRendering()
+ {
+ $this->setApiCalls([
+ [
+ 'function' => 'readGetResult',
+ 'url' => '/source/unittest/',
+ 'parameters' => [
+ 'resolveReferences' => 2,
+ ],
+ 'response' => $this->readFixture("GET_SourceGet_dldb.json"),
+ ]
+ ]);
+ $response = $this->render([], [
+ 'serviceId' => '2'
+ ], []);
+ $expectedResponse = [
+ "offices" => [
+ [
+ "id" => 9999999,
+ "name" => "Unittest Source Dienstleister 2",
+ "address" => null,
+ "geo" => null,
+ "scope" => [
+ "id" => 2,
+ "provider" => [
+ "id" => 9999999,
+ "name" => "Unittest Source Dienstleister 2",
+ "lat" => 48.12750898398659,
+ "lon" => 11.604317899956524,
+ "source" => "unittest",
+ "contact" => [
+ "city" => "Berlin",
+ "country" => "Germany",
+ "name" => "Unittest Source Dienstleister 2",
+ "postalCode" => "10178",
+ "region" => "Berlin",
+ "street" => "Alte Jakobstraße",
+ "streetNumber" => "106"
+ ]
+ ],
+ "shortName" => "Scope 2",
+ "telephoneActivated" => false,
+ "telephoneRequired" => true,
+ "customTextfieldActivated" => false,
+ "customTextfieldRequired" => true,
+ "customTextfieldLabel" => "",
+ "captchaActivatedRequired" => false,
+ "displayInfo" => null
+ ]
+ ]
+ ]
+ ];
+ $this->assertEquals(200, $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, json_decode((string)$response->getBody(), true));
+ }
+
+ public function testRenderingRequestRelation()
+ {
+ $this->setApiCalls([
+ [
+ 'function' => 'readGetResult',
+ 'url' => '/source/unittest/',
+ 'parameters' => [
+ 'resolveReferences' => 2,
+ ],
+ 'response' => $this->readFixture("GET_SourceGet_dldb.json"),
+ ]
+ ]);
+ $response = $this->render([], [
+ 'serviceId' => '1'
+ ], []);
+ $expectedResponse = [
+ "offices" => [
+ [
+ "id" => 9999998,
+ "name" => "Unittest Source Dienstleister",
+ "address" => null,
+ "geo" => null,
+ "scope" => [
+ "id" => 1,
+ "provider" => [
+ "id" => 9999998,
+ "name" => "Unittest Source Dienstleister",
+ "lat" => 48.12750898398659,
+ "lon" => 11.604317899956524,
+ "source" => "unittest",
+ "contact" => [
+ "city" => "Berlin",
+ "country" => "Germany",
+ "name" => "Unittest Source Dienstleister",
+ "postalCode" => "10178",
+ "region" => "Berlin",
+ "street" => "Alte Jakobstraße",
+ "streetNumber" => "105"
+ ]
+ ],
+ "shortName" => "Scope 1",
+ "telephoneActivated" => true,
+ "telephoneRequired" => false,
+ "customTextfieldActivated" => true,
+ "customTextfieldRequired" => false,
+ "customTextfieldLabel" => "Custom Label",
+ "captchaActivatedRequired" => true,
+ "displayInfo" => null
+ ]
+ ],
+ [
+ "id" => 9999999,
+ "name" => "Unittest Source Dienstleister 2",
+ "address" => null,
+ "geo" => null,
+ "scope" => [
+ "id" => 2,
+ "provider" => [
+ "id" => 9999999,
+ "name" => "Unittest Source Dienstleister 2",
+ "lat" => 48.12750898398659,
+ "lon" => 11.604317899956524,
+ "source" => "unittest",
+ "contact" => [
+ "city" => "Berlin",
+ "country" => "Germany",
+ "name" => "Unittest Source Dienstleister 2",
+ "postalCode" => "10178",
+ "region" => "Berlin",
+ "street" => "Alte Jakobstraße",
+ "streetNumber" => "106"
+ ]
+ ],
+ "shortName" => "Scope 2",
+ "telephoneActivated" => false,
+ "telephoneRequired" => true,
+ "customTextfieldActivated" => false,
+ "customTextfieldRequired" => true,
+ "customTextfieldLabel" => "",
+ "captchaActivatedRequired" => false,
+ "displayInfo" => null
+ ]
+ ]
+ ]
+ ];
+ $this->assertEquals(200, $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, json_decode((string)$response->getBody(), true));
+ }
+
+ public function testServiceNotFound()
+ {
+ $this->setApiCalls([
+ [
+ 'function' => 'readGetResult',
+ 'url' => '/source/unittest/',
+ 'parameters' => [
+ 'resolveReferences' => 2,
+ ],
+ 'response' => $this->readFixture("GET_SourceGet_dldb.json"),
+ ]
+ ]);
+
+ $response = $this->render([], [
+ 'serviceId' => '99999999'
+ ], []);
+ $expectedResponse = [
+ 'errors' => [
+ ErrorMessages::get('officesNotFound')
+ ]
+ ];
+ $this->assertEquals(ErrorMessages::get('officesNotFound')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, json_decode((string)$response->getBody(), true));
+
+ }
+
+ public function testNoServiceIdProvided()
+ {
+ $response = $this->render([], [], []);
+
+ $expectedResponse = [
+ 'errors' => [
+ ErrorMessages::get('invalidServiceId')
+ ]
+ ];
+ $this->assertEquals(ErrorMessages::get('invalidServiceId')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, json_decode((string)$response->getBody(), true));
+
+ }
+
+ public function testInvalidServiceId()
+ {
+ $response = $this->render([], [
+ 'serviceId' => 'blahblahblah'
+ ], []);
+
+ $expectedResponse = [
+ 'errors' => [
+ ErrorMessages::get('invalidServiceId')
+ ]
+ ];
+ $this->assertEquals(ErrorMessages::get('invalidServiceId')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, json_decode((string)$response->getBody(), true));
+ }
+
+}
diff --git a/zmscitizenapi/tests/Zmscitizenapi/Controllers/Office/OfficesListTest.php b/zmscitizenapi/tests/Zmscitizenapi/Controllers/Office/OfficesListTest.php
new file mode 100644
index 000000000..045317f77
--- /dev/null
+++ b/zmscitizenapi/tests/Zmscitizenapi/Controllers/Office/OfficesListTest.php
@@ -0,0 +1,117 @@
+clear();
+ }
+ }
+
+ public function testRendering()
+ {
+ $this->setApiCalls([
+ [
+ 'function' => 'readGetResult',
+ 'url' => '/source/unittest/',
+ 'parameters' => [
+ 'resolveReferences' => 2,
+ ],
+ 'response' => $this->readFixture("GET_SourceGet_dldb.json")
+ ]
+ ]);
+
+ $response = $this->render();
+ $responseBody = json_decode((string)$response->getBody(), true);
+ $expectedResponse = [
+ "offices" => [
+ [
+ "id" => 9999998,
+ "name" => "Unittest Source Dienstleister",
+ "address" => null,
+ "geo" => [
+ "lat" => "48.12750898398659",
+ "lon" => "11.604317899956524"
+ ],
+ "scope" => [
+ "id" => 1,
+ "provider" => [
+ "id" => 9999998,
+ "name" => "Unittest Source Dienstleister",
+ "lat" => 48.12750898398659,
+ "lon" => 11.604317899956524,
+ "source" => "unittest",
+ "contact" => [
+ "city" => "Berlin",
+ "country" => "Germany",
+ "name" => "Unittest Source Dienstleister",
+ "postalCode" => "10178",
+ "region" => "Berlin",
+ "street" => "Alte Jakobstraße",
+ "streetNumber" => "105"
+ ]
+ ],
+ "shortName" => "Scope 1",
+ "telephoneActivated" => true,
+ "telephoneRequired" => false,
+ "customTextfieldActivated" => true,
+ "customTextfieldRequired" => false,
+ "customTextfieldLabel" => "Custom Label",
+ "captchaActivatedRequired" => true,
+ "displayInfo" => null
+ ]
+ ],
+ [
+ "id" => 9999999,
+ "name" => "Unittest Source Dienstleister 2",
+ "address" => null,
+ "geo" => [
+ "lat" => "48.12750898398659",
+ "lon" => "11.604317899956524"
+ ],
+ "scope" => [
+ "id" => 2,
+ "provider" => [
+ "id" => 9999999,
+ "name" => "Unittest Source Dienstleister 2",
+ "lat" => 48.12750898398659,
+ "lon" => 11.604317899956524,
+ "source" => "unittest",
+ "contact" => [
+ "city" => "Berlin",
+ "country" => "Germany",
+ "name" => "Unittest Source Dienstleister 2",
+ "postalCode" => "10178",
+ "region" => "Berlin",
+ "street" => "Alte Jakobstraße",
+ "streetNumber" => "106"
+ ]
+ ],
+ "shortName" => "Scope 2",
+ "telephoneActivated" => false,
+ "telephoneRequired" => true,
+ "customTextfieldActivated" => false,
+ "customTextfieldRequired" => true,
+ "customTextfieldLabel" => "",
+ "captchaActivatedRequired" => false,
+ "displayInfo" => null
+ ]
+ ]
+ ]
+ ];
+
+ $this->assertEquals(200, $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, $responseBody);
+ }
+}
diff --git a/zmscitizenapi/tests/Zmscitizenapi/Controllers/Office/OfficesServicesRelationsTest.php b/zmscitizenapi/tests/Zmscitizenapi/Controllers/Office/OfficesServicesRelationsTest.php
new file mode 100644
index 000000000..1343ab4e0
--- /dev/null
+++ b/zmscitizenapi/tests/Zmscitizenapi/Controllers/Office/OfficesServicesRelationsTest.php
@@ -0,0 +1,151 @@
+clear();
+ }
+ }
+
+ public function testRendering()
+ {
+ $this->setApiCalls([
+ [
+ 'function' => 'readGetResult',
+ 'url' => '/source/unittest/',
+ 'parameters' => [
+ 'resolveReferences' => 2,
+ ],
+ 'response' => $this->readFixture("GET_SourceGet_dldb.json")
+ ]
+ ]);
+
+ $response = $this->render();
+ $responseBody = json_decode((string)$response->getBody(), true);
+ $expectedResponse = [
+ "offices" => [
+ [
+ "id" => 9999998,
+ "name" => "Unittest Source Dienstleister",
+ "address" => null,
+ "geo" => [
+ "lat" => "48.12750898398659",
+ "lon" => "11.604317899956524"
+ ],
+ "scope" => [
+ "id" => 1,
+ "provider" => [
+ "id" => 9999998,
+ "name" => "Unittest Source Dienstleister",
+ "lat" => 48.12750898398659,
+ "lon" => 11.604317899956524,
+ "source" => "unittest",
+ "contact" => [
+ "city" => "Berlin",
+ "country" => "Germany",
+ "name" => "Unittest Source Dienstleister",
+ "postalCode" => "10178",
+ "region" => "Berlin",
+ "street" => "Alte Jakobstraße",
+ "streetNumber" => "105"
+ ]
+ ],
+ "shortName" => "Scope 1",
+ "telephoneActivated" => true,
+ "telephoneRequired" => false,
+ "customTextfieldActivated" => true,
+ "customTextfieldRequired" => false,
+ "customTextfieldLabel" => "Custom Label",
+ "captchaActivatedRequired" => true,
+ "displayInfo" => null
+ ]
+ ],
+ [
+ "id" => 9999999,
+ "name" => "Unittest Source Dienstleister 2",
+ "address" => null,
+ "geo" => [
+ "lat" => "48.12750898398659",
+ "lon" => "11.604317899956524"
+ ],
+ "scope" => [
+ "id" => 2,
+ "provider" => [
+ "id" => 9999999,
+ "name" => "Unittest Source Dienstleister 2",
+ "lat" => 48.12750898398659,
+ "lon" => 11.604317899956524,
+ "source" => "unittest",
+ "contact" => [
+ "city" => "Berlin",
+ "country" => "Germany",
+ "name" => "Unittest Source Dienstleister 2",
+ "postalCode" => "10178",
+ "region" => "Berlin",
+ "street" => "Alte Jakobstraße",
+ "streetNumber" => "106"
+ ]
+ ],
+ "shortName" => "Scope 2",
+ "telephoneActivated" => false,
+ "telephoneRequired" => true,
+ "customTextfieldActivated" => false,
+ "customTextfieldRequired" => true,
+ "customTextfieldLabel" => "",
+ "captchaActivatedRequired" => false,
+ "displayInfo" => null
+ ]
+ ]
+ ],
+ "services" => [
+ [
+ "id" => 1,
+ "name" => "Unittest Source Dienstleistung",
+ "maxQuantity" => 1,
+ "combinable" => []
+ ],
+ [
+ "id" => 2,
+ "name" => "Unittest Source Dienstleistung 2",
+ "maxQuantity" => 1,
+ "combinable" => [
+ "1" => [9999999],
+ "2" => [9999999]
+ ]
+ ]
+ ],
+ "relations" => [
+ [
+ "officeId" => 9999998,
+ "serviceId" => 1,
+ "slots" => 2
+ ],
+ [
+ "officeId" => 9999999,
+ "serviceId" => 1,
+ "slots" => 1
+ ],
+ [
+ "officeId" => 9999999,
+ "serviceId" => 2,
+ "slots" => 1
+ ]
+ ]
+ ];
+ $this->assertEquals(200, $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, $responseBody);
+ }
+
+}
diff --git a/zmscitizenapi/tests/Zmscitizenapi/Controllers/Scope/ScopeByIdTest.php b/zmscitizenapi/tests/Zmscitizenapi/Controllers/Scope/ScopeByIdTest.php
new file mode 100644
index 000000000..1ecc57366
--- /dev/null
+++ b/zmscitizenapi/tests/Zmscitizenapi/Controllers/Scope/ScopeByIdTest.php
@@ -0,0 +1,127 @@
+clear();
+ }
+ }
+
+ public function testRendering()
+ {
+ $this->setApiCalls([
+ [
+ 'function' => 'readGetResult',
+ 'url' => '/source/unittest/',
+ 'parameters' => [
+ 'resolveReferences' => 2,
+ ],
+ 'response' => $this->readFixture("GET_SourceGet_dldb.json"),
+ ]
+ ]);
+ $response = $this->render([], [
+ 'scopeId' => '1'
+ ], []);
+ $expectedResponse = [
+ "id" => 1,
+ "provider" => [
+ "id" => 9999998,
+ "name" => "Unittest Source Dienstleister",
+ "lat" => 48.12750898398659,
+ "lon" => 11.604317899956524,
+ "source" => "unittest",
+ "contact" => [
+ "city" => "Berlin",
+ "country" => "Germany",
+ "name" => "Unittest Source Dienstleister",
+ "postalCode" => "10178",
+ "region" => "Berlin",
+ "street" => "Alte Jakobstraße",
+ "streetNumber" => "105"
+ ]
+ ],
+ "shortName" => "Scope 1",
+ "telephoneActivated" => true,
+ "telephoneRequired" => false,
+ "customTextfieldActivated" => true,
+ "customTextfieldRequired" => false,
+ "customTextfieldLabel" => "Custom Label",
+ "captchaActivatedRequired" => true,
+ "displayInfo" => null
+ ];
+ $responseBody = json_decode((string)$response->getBody(), true);
+ $this->assertEquals(200, $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, $responseBody);
+ }
+
+ public function testScopeNotFound()
+ {
+ $this->setApiCalls([
+ [
+ 'function' => 'readGetResult',
+ 'url' => '/source/unittest/',
+ 'parameters' => [
+ 'resolveReferences' => 2,
+ ],
+ 'response' => $this->readFixture("GET_SourceGet_dldb.json"),
+ ]
+ ]);
+
+ $response = $this->render([], [
+ 'scopeId' => '99'
+ ], []);
+
+ $expectedResponse = [
+ 'errors' => [
+ ErrorMessages::get('scopesNotFound')
+ ]
+ ];
+
+ $this->assertEquals(ErrorMessages::get('scopesNotFound')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, json_decode((string)$response->getBody(), true));
+
+ }
+
+ public function testNoScopeIdProvided()
+ {
+ $response = $this->render([], [], []);
+ $expectedResponse = [
+ 'errors' => [
+ ErrorMessages::get('invalidScopeId')
+ ]
+ ];
+ $this->assertEquals(ErrorMessages::get('invalidScopeId')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, json_decode((string)$response->getBody(), true));
+
+ }
+
+ public function testInvalidScopeId()
+ {
+ $response = $this->render([], [
+ 'scopeId' => 'blahblahblah'
+ ], []);
+
+ $expectedResponse = [
+ 'errors' => [
+ ErrorMessages::get('invalidScopeId')
+ ]
+ ];
+ $this->assertEquals(ErrorMessages::get('invalidScopeId')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, json_decode((string)$response->getBody(), true));
+ }
+
+}
diff --git a/zmscitizenapi/tests/Zmscitizenapi/Controllers/Scope/ScopesListTest.php b/zmscitizenapi/tests/Zmscitizenapi/Controllers/Scope/ScopesListTest.php
new file mode 100644
index 000000000..6d6e6cbbe
--- /dev/null
+++ b/zmscitizenapi/tests/Zmscitizenapi/Controllers/Scope/ScopesListTest.php
@@ -0,0 +1,100 @@
+clear();
+ }
+ }
+
+ public function testRendering()
+ {
+ $this->setApiCalls([
+ [
+ 'function' => 'readGetResult',
+ 'url' => '/source/unittest/',
+ 'parameters' => [
+ 'resolveReferences' => 2,
+ ],
+ 'response' => $this->readFixture("GET_SourceGet_dldb.json")
+ ]
+ ]);
+
+ $response = $this->render();
+ $responseBody = json_decode((string)$response->getBody(), true);
+ $expectedResponse = [
+ "scopes" => [
+ [
+ "id" => 1,
+ "provider" => [
+ "id" => 9999998,
+ "name" => "Unittest Source Dienstleister",
+ "lat" => 48.12750898398659,
+ "lon" => 11.604317899956524,
+ "source" => "unittest",
+ "contact" => [
+ "city" => "Berlin",
+ "country" => "Germany",
+ "name" => "Unittest Source Dienstleister",
+ "postalCode" => "10178",
+ "region" => "Berlin",
+ "street" => "Alte Jakobstraße",
+ "streetNumber" => "105"
+ ]
+ ],
+ "shortName" => "Scope 1",
+ "telephoneActivated" => true,
+ "telephoneRequired" => false,
+ "customTextfieldActivated" => true,
+ "customTextfieldRequired" => false,
+ "customTextfieldLabel" => "Custom Label",
+ "captchaActivatedRequired" => true,
+ "displayInfo" => null
+ ],
+ [
+ "id" => 2,
+ "provider" => [
+ "id" => 9999999,
+ "name" => "Unittest Source Dienstleister 2",
+ "lat" => 48.12750898398659,
+ "lon" => 11.604317899956524,
+ "source" => "unittest",
+ "contact" => [
+ "city" => "Berlin",
+ "country" => "Germany",
+ "name" => "Unittest Source Dienstleister 2",
+ "postalCode" => "10178",
+ "region" => "Berlin",
+ "street" => "Alte Jakobstraße",
+ "streetNumber" => "106"
+ ]
+ ],
+ "shortName" => "Scope 2",
+ "telephoneActivated" => false,
+ "telephoneRequired" => true,
+ "customTextfieldActivated" => false,
+ "customTextfieldRequired" => true,
+ "customTextfieldLabel" => "",
+ "captchaActivatedRequired" => false,
+ "displayInfo" => null
+ ]
+ ]
+ ];
+
+ $this->assertEquals(200, $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, $responseBody);
+ }
+}
diff --git a/zmscitizenapi/tests/Zmscitizenapi/Controllers/Security/FriendlyCaptchaTest.php b/zmscitizenapi/tests/Zmscitizenapi/Controllers/Security/FriendlyCaptchaTest.php
new file mode 100644
index 000000000..cc22aba1a
--- /dev/null
+++ b/zmscitizenapi/tests/Zmscitizenapi/Controllers/Security/FriendlyCaptchaTest.php
@@ -0,0 +1,94 @@
+clear();
+ }
+
+ putenv('FRIENDLY_CAPTCHA_SITE_KEY=FAKE_SITE_KEY');
+ putenv('FRIENDLY_CAPTCHA_ENDPOINT=https://api.friendlycaptcha.com/api/v1/siteverify');
+ putenv('FRIENDLY_CAPTCHA_ENDPOINT_PUZZLE=https://api.friendlycaptcha.com/api/v1/puzzle');
+ putenv('CAPTCHA_ENABLED=1');
+
+ \App::initialize();
+ }
+
+ public function tearDown(): void
+ {
+ putenv('FRIENDLY_CAPTCHA_SITEKEY=');
+ putenv('FRIENDLY_CAPTCHA_ENDPOINT=');
+ putenv('FRIENDLY_CAPTCHA_ENDPOINT_PUZZLE=');
+ putenv('CAPTCHA_ENABLED=');
+
+ parent::tearDown();
+ }
+
+ public function testCaptchaDetails()
+ {
+ $captchaEnabled = filter_var(getenv('CAPTCHA_ENABLED'), FILTER_VALIDATE_BOOLEAN);
+ $parameters = [];
+ $response = $this->render([], $parameters, []);
+ $responseBody = json_decode((string)$response->getBody(), true);
+
+ $expectedResponse = [
+ 'siteKey' => 'FAKE_SITE_KEY',
+ 'captchaEndpoint' => 'https://api.friendlycaptcha.com/api/v1/siteverify',
+ 'puzzle' => 'https://api.friendlycaptcha.com/api/v1/puzzle',
+ 'captchaEnabled' => true
+ ];
+
+ $this->assertEquals(200, $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, $responseBody);
+ }
+
+ public function testVerifyCaptchaSuccess()
+ {
+ // Mock the HTTP client to return a successful response
+ $mockResponse = new \GuzzleHttp\Psr7\Response(200, [], json_encode(['success' => true]));
+ \App::$http = new \GuzzleHttp\Client(['handler' => \GuzzleHttp\HandlerStack::create(new \GuzzleHttp\Handler\MockHandler([$mockResponse]))]);
+
+ $captcha = new FriendlyCaptcha();
+ $result = $captcha->verifyCaptcha('valid_solution');
+ $this->assertTrue($result);
+ }
+
+ public function testVerifyCaptchaFailure()
+ {
+ // Mock the HTTP client to return a failure response
+ $mockResponse = new \GuzzleHttp\Psr7\Response(200, [], json_encode(['success' => false]));
+ \App::$http = new \GuzzleHttp\Client(['handler' => \GuzzleHttp\HandlerStack::create(new \GuzzleHttp\Handler\MockHandler([$mockResponse]))]);
+
+ $captcha = new FriendlyCaptcha();
+ $result = $captcha->verifyCaptcha('invalid_solution');
+ $this->assertFalse($result);
+ }
+
+ public function testVerifyCaptchaException()
+ {
+ // Mock the HTTP client to throw an exception
+ $mockHandler = new \GuzzleHttp\Handler\MockHandler([
+ new \GuzzleHttp\Exception\RequestException('Error Communicating with Server', new \GuzzleHttp\Psr7\Request('POST', 'test'))
+ ]);
+ \App::$http = new \GuzzleHttp\Client(['handler' => \GuzzleHttp\HandlerStack::create($mockHandler)]);
+
+ $captcha = new FriendlyCaptcha();
+ $result = $captcha->verifyCaptcha('exception_solution');
+ $this->assertFalse($result);
+ }
+
+}
diff --git a/zmscitizenapi/tests/Zmscitizenapi/Controllers/Service/ServiceListByOfficeTest.php b/zmscitizenapi/tests/Zmscitizenapi/Controllers/Service/ServiceListByOfficeTest.php
new file mode 100644
index 000000000..51d7b63c1
--- /dev/null
+++ b/zmscitizenapi/tests/Zmscitizenapi/Controllers/Service/ServiceListByOfficeTest.php
@@ -0,0 +1,96 @@
+clear();
+ }
+ }
+
+ public function testRendering()
+ {
+ $this->setApiCalls([
+ [
+ 'function' => 'readGetResult',
+ 'url' => '/source/unittest/',
+ 'parameters' => [
+ 'resolveReferences' => 2,
+ ],
+ 'response' => $this->readFixture("GET_SourceGet_dldb.json"),
+ ]
+ ]);
+ $response = $this->render([], [
+ 'officeId' => '9999998'
+ ], []);
+ $expectedResponse = [
+ 'services' => [
+ [
+ 'id' => '1',
+ 'name' => 'Unittest Source Dienstleistung',
+ 'maxQuantity' => 1,
+ "combinable" => null
+ ]
+ ]
+ ];
+ $this->assertEquals(200, $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, json_decode((string) $response->getBody(), true));
+ }
+
+
+ public function testServiceNotFound()
+ {
+ $this->setApiCalls([
+ [
+ 'function' => 'readGetResult',
+ 'url' => '/source/unittest/',
+ 'parameters' => [
+ 'resolveReferences' => 2,
+ ],
+ 'response' => $this->readFixture("GET_SourceGet_dldb.json"),
+ ]
+ ]);
+
+ $response = $this->render([], [
+ 'officeId' => '99999999'
+ ], []);
+
+ $expectedResponse = [
+ 'errors' => [
+ ErrorMessages::get('servicesNotFound')
+ ]
+ ];
+
+ $this->assertEquals(ErrorMessages::get('servicesNotFound')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, json_decode((string) $response->getBody(), true));
+
+ }
+
+ public function testNoOfficeIdProvided()
+ {
+ $response = $this->render([], [], []);
+ $expectedResponse = [
+ 'errors' => [
+ ErrorMessages::get('invalidOfficeId')
+ ]
+ ];
+
+ $this->assertEquals(ErrorMessages::get('invalidOfficeId')['statusCode'], $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, json_decode((string) $response->getBody(), true));
+
+ }
+
+}
diff --git a/zmscitizenapi/tests/Zmscitizenapi/Controllers/Service/ServicesListTest.php b/zmscitizenapi/tests/Zmscitizenapi/Controllers/Service/ServicesListTest.php
new file mode 100644
index 000000000..5e5054188
--- /dev/null
+++ b/zmscitizenapi/tests/Zmscitizenapi/Controllers/Service/ServicesListTest.php
@@ -0,0 +1,57 @@
+clear();
+ }
+ }
+
+ public function testRendering() {
+ $this->setApiCalls([
+ [
+ 'function' => 'readGetResult',
+ 'url' => '/source/unittest/',
+ 'parameters' => [
+ 'resolveReferences' => 2,
+ ],
+ 'response' => $this->readFixture("GET_SourceGet_dldb.json")
+ ]
+ ]);
+
+ $response = $this->render();
+ $responseBody = json_decode((string)$response->getBody(), true);
+ $expectedResponse = [
+ "services" => [
+ [
+ "id" => "1",
+ "name" => "Unittest Source Dienstleistung",
+ "maxQuantity" => 1,
+ "combinable" => null
+ ],
+ [
+ "id" => "2",
+ "name" => "Unittest Source Dienstleistung 2",
+ "maxQuantity" => 1,
+ "combinable" => null
+ ]
+ ]
+ ];
+
+ $this->assertEquals(200, $response->getStatusCode());
+ $this->assertEqualsCanonicalizing($expectedResponse, $responseBody);
+ }
+}
diff --git a/zmscitizenapi/tests/Zmscitizenapi/Middleware/CorsMiddlewareTest.php b/zmscitizenapi/tests/Zmscitizenapi/Middleware/CorsMiddlewareTest.php
new file mode 100644
index 000000000..6dd736566
--- /dev/null
+++ b/zmscitizenapi/tests/Zmscitizenapi/Middleware/CorsMiddlewareTest.php
@@ -0,0 +1,108 @@
+clear();
+ }
+ putenv('CORS=http://localhost:8080');
+ $this->middleware = new CorsMiddleware($this->logger);
+ }
+
+ protected function tearDown(): void
+ {
+ putenv('CORS'); // Clear environment variable
+ parent::tearDown();
+ }
+
+ public function testAllowsRequestWithoutOrigin(): void
+ {
+ $request = $this->createRequest();
+ $response = new Response();
+ $handler = $this->createHandler($response);
+
+ /*$this->logger->expectLogInfo('Direct browser request - no Origin header', [
+ 'uri' => 'http://localhost/test'
+ ]);*/
+
+ $result = $this->middleware->process($request, $handler);
+ $this->assertSame($response, $result);
+ }
+
+ public function testBlocksDisallowedOrigin(): void
+ {
+ $request = $this->createRequest(['Origin' => 'http://evil.com']);
+ $response = new Response();
+ $handler = $this->createHandler($response);
+
+ $this->logger->expectLogInfo(sprintf(
+ 'CORS blocked - Origin %s not allowed. URI: %s',
+ 'http://evil.com',
+ 'http://localhost/test'
+ ));
+
+ $result = $this->middleware->process($request, $handler);
+ $logBody = json_decode((string)$result->getBody(), true);
+
+ $this->assertEquals(ErrorMessages::get('corsOriginNotAllowed')['statusCode'], $result->getStatusCode());
+ $this->assertEquals(
+ ['errors' => [ErrorMessages::get('corsOriginNotAllowed')]],
+ $logBody
+ );
+ }
+
+ public function testAllowsWhitelistedOrigin(): void
+ {
+ $request = $this->createRequest(['Origin' => 'http://localhost:8080']);
+ $response = new Response();
+ $handler = $this->createHandler($response);
+
+ $result = $this->middleware->process($request, $handler);
+
+ $this->assertEquals('http://localhost:8080', $result->getHeaderLine('Access-Control-Allow-Origin'));
+ $this->assertNotEmpty($result->getHeaderLine('Access-Control-Allow-Methods'));
+ }
+
+ public function testHandlesPreflightRequest(): void
+ {
+ $headers = new \Slim\Psr7\Headers([
+ 'Origin' => 'http://localhost:8080',
+ 'Access-Control-Request-Method' => 'POST',
+ 'Access-Control-Request-Headers' => 'content-type'
+ ]);
+
+ $request = new \Slim\Psr7\Request(
+ 'OPTIONS',
+ new \Slim\Psr7\Uri('http', 'localhost/test'),
+ $headers,
+ [],
+ [],
+ new \Slim\Psr7\Stream(fopen('php://temp', 'r+'))
+ );
+
+ $response = new Response();
+ $handler = $this->createHandler($response);
+
+ $result = $this->middleware->process($request, $handler);
+
+ $this->assertEquals('http://localhost:8080', $result->getHeaderLine('Access-Control-Allow-Origin'));
+ $this->assertNotEmpty($result->getHeaderLine('Access-Control-Allow-Methods'));
+ $this->assertNotEmpty($result->getHeaderLine('Access-Control-Allow-Headers'));
+ }
+}
\ No newline at end of file
diff --git a/zmscitizenapi/tests/Zmscitizenapi/Middleware/CsrfMiddlewareTest.php b/zmscitizenapi/tests/Zmscitizenapi/Middleware/CsrfMiddlewareTest.php
new file mode 100644
index 000000000..aebe7c23e
--- /dev/null
+++ b/zmscitizenapi/tests/Zmscitizenapi/Middleware/CsrfMiddlewareTest.php
@@ -0,0 +1,105 @@
+clear();
+ }
+ $this->middleware = new CsrfMiddleware($this->logger);
+ }
+
+ public function testAllowsGetRequest(): void
+ {
+ /** @var ServerRequestInterface|MockObject $request */
+ $request = $this->createMock(ServerRequestInterface::class);
+ $request->expects($this->once())
+ ->method('getMethod')
+ ->willReturn('GET');
+ $request->expects($this->any())
+ ->method('getUri')
+ ->willReturn(new \BO\Zmsclient\Psr7\Uri('http://localhost/test'));
+
+ $response = new Response();
+ $handler = $this->createHandler($response);
+
+ $result = $this->middleware->process($request, $handler);
+ $this->assertSame($response, $result);
+ }
+
+ public function testBlocksPostWithoutToken(): void
+ {
+ /** @var ServerRequestInterface|MockObject $request */
+ $request = $this->createMock(ServerRequestInterface::class);
+ $request->expects($this->once())
+ ->method('getMethod')
+ ->willReturn('POST');
+ $request->expects($this->any())
+ ->method('getUri')
+ ->willReturn(new \BO\Zmsclient\Psr7\Uri('http://localhost/test'));
+
+ $response = new Response();
+ $handler = $this->createHandler($response);
+
+ $this->logger->expectLogInfo('CSRF token missing', [
+ 'uri' => 'http://localhost/test'
+ ]);
+
+ $result = $this->middleware->process($request, $handler);
+ $logBody = json_decode((string)$result->getBody(), true);
+
+ $this->assertEquals(ErrorMessages::get('csrfTokenMissing')['statusCode'], $result->getStatusCode());
+ $this->assertEquals(
+ ['errors' => [ErrorMessages::get('csrfTokenMissing')]],
+ $logBody
+ );
+ }
+
+ public function testBlocksInvalidToken(): void
+ {
+ /** @var ServerRequestInterface|MockObject $request */
+ $request = $this->createMock(ServerRequestInterface::class);
+ $request->expects($this->once())
+ ->method('getMethod')
+ ->willReturn('POST');
+ $request->method('getHeaderLine')
+ ->willReturnCallback(function(string $header) {
+ $this->assertEquals('X-CSRF-Token', $header);
+ return 'invalid';
+ });
+ $request->expects($this->any())
+ ->method('getUri')
+ ->willReturn(new \BO\Zmsclient\Psr7\Uri('http://localhost/test'));
+
+ $response = new Response();
+ $handler = $this->createHandler($response);
+
+ $this->logger->expectLogInfo('Invalid CSRF token');
+
+ $result = $this->middleware->process($request, $handler);
+ $logBody = json_decode((string)$result->getBody(), true);
+
+ $this->assertEquals(ErrorMessages::get('csrfTokenInvalid')['statusCode'], $result->getStatusCode());
+ $this->assertEquals(
+ ['errors' => [ErrorMessages::get('csrfTokenInvalid')]],
+ $logBody
+ );
+ }
+}
\ No newline at end of file
diff --git a/zmscitizenapi/tests/Zmscitizenapi/Middleware/IpFilterMiddlewareTest.php b/zmscitizenapi/tests/Zmscitizenapi/Middleware/IpFilterMiddlewareTest.php
new file mode 100644
index 000000000..bc2a4964b
--- /dev/null
+++ b/zmscitizenapi/tests/Zmscitizenapi/Middleware/IpFilterMiddlewareTest.php
@@ -0,0 +1,171 @@
+clear();
+ }
+ putenv('IP_BLACKLIST'); // Clear any existing blacklist
+ $_SERVER = []; // Reset server variables
+ }
+
+ protected function tearDown(): void
+ {
+ putenv('IP_BLACKLIST');
+ $_SERVER = []; // Reset server variables
+ parent::tearDown();
+ }
+
+ public function testAllowsRequestWithEmptyBlacklist(): void
+ {
+ $_SERVER['REMOTE_ADDR'] = '192.168.1.1';
+ $middleware = new IpFilterMiddleware($this->logger);
+
+ $request = $this->createRequest(['REMOTE_ADDR' => '192.168.1.1']);
+ $response = new Response();
+ $handler = $this->createHandler($response);
+
+ /*$this->logger->expectLogInfo('Request processed successfully', [
+ 'uri' => 'http://localhost/test'
+ ]);*/
+
+ $result = $middleware->process($request, $handler);
+ $this->assertSame($response, $result);
+ }
+
+ public function testAllowsNonBlacklistedIPv4(): void
+ {
+ putenv('IP_BLACKLIST=192.168.1.1,10.0.0.0/8');
+ $_SERVER['REMOTE_ADDR'] = '192.168.2.1';
+ $middleware = new IpFilterMiddleware($this->logger);
+
+ $request = $this->createRequest(['REMOTE_ADDR' => '192.168.2.1']);
+ $response = new Response();
+ $handler = $this->createHandler($response);
+
+ /*$this->logger->expectLogInfo('Request processed successfully', [
+ 'uri' => 'http://localhost/test'
+ ]);*/
+
+ $result = $middleware->process($request, $handler);
+ $this->assertSame($response, $result);
+ }
+
+ public function testAllowsNonBlacklistedIPv6(): void
+ {
+ putenv('IP_BLACKLIST=2001:db8::/32,::1');
+ $_SERVER['REMOTE_ADDR'] = '2001:db9::1';
+ $middleware = new IpFilterMiddleware($this->logger);
+
+ $request = $this->createRequest(['REMOTE_ADDR' => '2001:db9::1']);
+ $response = new Response();
+ $handler = $this->createHandler($response);
+
+ /*$this->logger->expectLogInfo('Request processed successfully', [
+ 'uri' => 'http://localhost/test'
+ ]);*/
+
+ $result = $middleware->process($request, $handler);
+ $this->assertSame($response, $result);
+ }
+
+ public function testBlocksBlacklistedIPv4(): void
+ {
+ putenv('IP_BLACKLIST=192.168.1.0/24');
+ $_SERVER['REMOTE_ADDR'] = '192.168.1.1';
+ $middleware = new IpFilterMiddleware($this->logger);
+
+ $request = $this->createRequest(['REMOTE_ADDR' => '192.168.1.1']);
+ $response = new Response();
+ $handler = $this->createHandler($response);
+
+ $this->logger->expectLogInfo('Access denied - IP blacklisted', [
+ 'ip' => '192.168.1.1',
+ 'uri' => 'http://localhost/test'
+ ]);
+
+ $result = $middleware->process($request, $handler);
+ $responseData = json_decode((string)$result->getBody(), true);
+
+ $this->assertEquals(ErrorMessages::get('ipBlacklisted')['statusCode'], $result->getStatusCode());
+ $this->assertEquals(
+ ['errors' => [ErrorMessages::get('ipBlacklisted')]],
+ $responseData
+ );
+ }
+
+ public function testBlocksBlacklistedIPv6(): void
+ {
+ putenv('IP_BLACKLIST=2001:db8::/32');
+ $_SERVER['REMOTE_ADDR'] = '2001:db8:1::1';
+ $middleware = new IpFilterMiddleware($this->logger);
+
+ $request = $this->createRequest(['REMOTE_ADDR' => '2001:db8:1::1']);
+ $response = new Response();
+ $handler = $this->createHandler($response);
+
+ $this->logger->expectLogInfo('Access denied - IP blacklisted', [
+ 'ip' => '2001:db8:1::1',
+ 'uri' => 'http://localhost/test'
+ ]);
+
+ $result = $middleware->process($request, $handler);
+ $responseData = json_decode((string)$result->getBody(), true);
+
+ $this->assertEquals(ErrorMessages::get('ipBlacklisted')['statusCode'], $result->getStatusCode());
+ $this->assertEquals(
+ ['errors' => [ErrorMessages::get('ipBlacklisted')]],
+ $responseData
+ );
+ }
+
+ public function testHandlesInvalidIP(): void
+ {
+ putenv('IP_BLACKLIST=192.168.1.1');
+ $_SERVER['REMOTE_ADDR'] = 'invalid-ip';
+ $middleware = new IpFilterMiddleware($this->logger);
+
+ $request = $this->createRequest(['REMOTE_ADDR' => 'invalid-ip']);
+ $response = new Response();
+ $handler = $this->createHandler($response);
+
+ $this->logger->expectLogInfo('Invalid IP address detected', [
+ 'ip' => 'invalid-ip',
+ 'uri' => 'http://localhost/test'
+ ]);
+
+ $result = $middleware->process($request, $handler);
+ $this->assertSame($response, $result);
+ }
+
+ public function testHandlesInvalidBlacklistEntry(): void
+ {
+ putenv('IP_BLACKLIST=invalid-ip,192.168.1.1');
+ $_SERVER['REMOTE_ADDR'] = '192.168.1.2';
+ $middleware = new IpFilterMiddleware($this->logger);
+
+ $request = $this->createRequest(['REMOTE_ADDR' => '192.168.1.2']);
+ $response = new Response();
+ $handler = $this->createHandler($response);
+
+ /*$this->logger->expectLogInfo('Request processed successfully', [
+ 'uri' => 'http://localhost/test'
+ ]);*/
+
+ $result = $middleware->process($request, $handler);
+ $this->assertSame($response, $result);
+ }
+}
\ No newline at end of file
diff --git a/zmscitizenapi/tests/Zmscitizenapi/Middleware/RateLimitingMiddlewareTest.php b/zmscitizenapi/tests/Zmscitizenapi/Middleware/RateLimitingMiddlewareTest.php
new file mode 100644
index 000000000..c539c9339
--- /dev/null
+++ b/zmscitizenapi/tests/Zmscitizenapi/Middleware/RateLimitingMiddlewareTest.php
@@ -0,0 +1,212 @@
+clear();
+ }
+ $this->cache = $this->createMock(CacheInterface::class);
+ $this->middleware = new RateLimitingMiddleware($this->cache, $this->logger);
+ }
+
+ protected function tearDown(): void
+ {
+ if (\App::$cache) {
+ \App::$cache->clear();
+ }
+ parent::tearDown();
+ }
+
+ public function testAllowsRequestUnderLimit(): void
+ {
+ $request = $this->createRequest();
+ $response = new Response();
+ $handler = $this->createHandler($response);
+
+ // Mock lock acquisition
+ $this->cache->expects($this->once())
+ ->method('has')
+ ->willReturn(false);
+
+ $this->cache->method('set')
+ ->willReturnCallback(function(string $key, $value, $ttl) {
+ if (str_contains($key, '_lock')) {
+ $this->assertTrue($value === true);
+ $this->assertEquals(1, $ttl);
+ } else {
+ $this->assertIsArray($value);
+ $this->assertArrayHasKey('count', $value);
+ $this->assertEquals(2, $value['count']);
+ $this->assertEquals(60, $ttl);
+ }
+ return true;
+ });
+
+ $currentTime = time();
+ $requestData = [
+ 'count' => 1,
+ 'timestamp' => $currentTime
+ ];
+
+ $this->cache->expects($this->any())
+ ->method('get')
+ ->willReturn($requestData);
+
+ $this->cache->expects($this->any())
+ ->method('delete')
+ ->willReturn(true);
+
+ $result = $this->middleware->process($request, $handler);
+
+ $this->assertSame($response->getStatusCode(), $result->getStatusCode());
+ $this->assertSame('58', $result->getHeaderLine('X-RateLimit-Remaining'));
+ $this->assertSame('60', $result->getHeaderLine('X-RateLimit-Limit'));
+ $this->assertNotEmpty($result->getHeaderLine('X-RateLimit-Reset'));
+ }
+
+ public function testBlocksRequestOverLimit(): void
+ {
+ $request = $this->createRequest();
+ $response = new Response();
+ $handler = $this->createHandler($response);
+
+ // Mock lock acquisition
+ $this->cache->expects($this->once())
+ ->method('has')
+ ->willReturn(false);
+
+ $this->cache->method('set')
+ ->willReturnCallback(function(string $key, $value, $ttl) {
+ $this->assertTrue(str_contains($key, '_lock'));
+ $this->assertTrue($value === true);
+ $this->assertEquals(1, $ttl);
+ return true;
+ });
+
+ $currentTime = time();
+ $requestData = [
+ 'count' => 60,
+ 'timestamp' => $currentTime
+ ];
+
+ $this->cache->expects($this->any())
+ ->method('get')
+ ->willReturn($requestData);
+
+ $this->cache->expects($this->any())
+ ->method('delete')
+ ->willReturn(true);
+
+ $this->logger->expectLogInfo(sprintf(
+ 'Rate limit exceeded for IP %s. URI: %s',
+ '127.0.0.1',
+ 'http://localhost/test'
+ ));
+
+ $result = $this->middleware->process($request, $handler);
+ $logBody = json_decode((string) $result->getBody(), true);
+
+ $this->assertEquals(ErrorMessages::get('rateLimitExceeded')['statusCode'], $result->getStatusCode());
+ $this->assertEquals(
+ ['errors' => [ErrorMessages::get('rateLimitExceeded')]],
+ $logBody
+ );
+ }
+
+ public function testHandlesFirstRequest(): void
+ {
+ $request = $this->createRequest();
+ $response = new Response();
+ $handler = $this->createHandler($response);
+
+ // Mock lock acquisition
+ $this->cache->expects($this->once())
+ ->method('has')
+ ->willReturn(false);
+
+ $this->cache->method('set')
+ ->willReturnCallback(function(string $key, $value, $ttl) {
+ if (str_contains($key, '_lock')) {
+ $this->assertTrue($value === true);
+ $this->assertEquals(1, $ttl);
+ } else {
+ $this->assertIsArray($value);
+ $this->assertArrayHasKey('count', $value);
+ $this->assertEquals(1, $value['count']);
+ $this->assertEquals(60, $ttl);
+ }
+ return true;
+ });
+
+ // For first request, always return null
+ $this->cache->expects($this->any())
+ ->method('get')
+ ->willReturn(null);
+
+ $this->cache->expects($this->any())
+ ->method('delete')
+ ->willReturn(true);
+
+ $result = $this->middleware->process($request, $handler);
+
+ $this->assertSame($response->getStatusCode(), $result->getStatusCode());
+ $this->assertSame('59', $result->getHeaderLine('X-RateLimit-Remaining'));
+ $this->assertSame('60', $result->getHeaderLine('X-RateLimit-Limit'));
+ $this->assertNotEmpty($result->getHeaderLine('X-RateLimit-Reset'));
+ }
+
+ public function testHandlesCorruptedCacheData(): void
+ {
+ $request = $this->createRequest();
+ $response = new Response();
+ $handler = $this->createHandler($response);
+
+ // Mock lock acquisition
+ $this->cache->expects($this->once())
+ ->method('has')
+ ->willReturn(false);
+
+ $this->cache->method('set')
+ ->willReturnCallback(function(string $key, $value, $ttl) {
+ $this->assertTrue(str_contains($key, '_lock'));
+ $this->assertTrue($value === true);
+ $this->assertEquals(1, $ttl);
+ return true;
+ });
+
+ // Return corrupted data for rate limit key
+ $this->cache->expects($this->any())
+ ->method('get')
+ ->willReturn('corrupted');
+
+ // Handle both lock release and corrupted data cleanup
+ $this->cache->expects($this->any())
+ ->method('delete')
+ ->willReturn(true);
+
+ $result = $this->middleware->process($request, $handler);
+
+ $this->assertSame($response->getStatusCode(), $result->getStatusCode());
+ $this->assertSame('59', $result->getHeaderLine('X-RateLimit-Remaining'));
+ $this->assertSame('60', $result->getHeaderLine('X-RateLimit-Limit'));
+ }
+}
\ No newline at end of file
diff --git a/zmscitizenapi/tests/Zmscitizenapi/Middleware/RequestSanitizerMiddlewareTest.php b/zmscitizenapi/tests/Zmscitizenapi/Middleware/RequestSanitizerMiddlewareTest.php
new file mode 100644
index 000000000..81e64b323
--- /dev/null
+++ b/zmscitizenapi/tests/Zmscitizenapi/Middleware/RequestSanitizerMiddlewareTest.php
@@ -0,0 +1,75 @@
+clear();
+ }
+ $this->middleware = new RequestSanitizerMiddleware($this->logger);
+ }
+
+ public function testSanitizesQueryParams(): void
+ {
+ /** @var ServerRequestInterface|MockObject $request */
+ $request = $this->createMock(ServerRequestInterface::class);
+ $request->expects($this->once())
+ ->method('getQueryParams')
+ ->willReturn(['test' => '']);
+ $request->expects($this->once())
+ ->method('withQueryParams')
+ ->willReturnSelf();
+ $request->expects($this->any())
+ ->method('getUri')
+ ->willReturn(new \BO\Zmsclient\Psr7\Uri('http://localhost/test'));
+
+ $response = new Response();
+ $handler = $this->createHandler($response);
+
+ /*$this->logger->expectLogInfo('Request sanitized', [
+ 'uri' => 'http://localhost/test'
+ ]);*/
+
+ $result = $this->middleware->process($request, $handler);
+ $this->assertSame($response, $result);
+ }
+
+ public function testHandlesSanitizationError(): void
+ {
+ /** @var ServerRequestInterface|MockObject $request */
+ $request = $this->createMock(ServerRequestInterface::class);
+ $request->expects($this->once())
+ ->method('getQueryParams')
+ ->willThrowException(new \RuntimeException('Sanitization error'));
+ $request->expects($this->any())
+ ->method('getUri')
+ ->willReturn(new \BO\Zmsclient\Psr7\Uri('http://localhost/test'));
+
+ $response = new Response();
+ $handler = $this->createHandler($response);
+
+ $this->logger->expectLogError(new \RuntimeException('Sanitization error'));
+
+ $this->expectException(\RuntimeException::class);
+ $this->expectExceptionMessage('Sanitization error');
+
+ $this->middleware->process($request, $handler);
+ }
+}
\ No newline at end of file
diff --git a/zmscitizenapi/tests/Zmscitizenapi/Middleware/RequestSizeLimitMiddlewareTest.php b/zmscitizenapi/tests/Zmscitizenapi/Middleware/RequestSizeLimitMiddlewareTest.php
new file mode 100644
index 000000000..e89852366
--- /dev/null
+++ b/zmscitizenapi/tests/Zmscitizenapi/Middleware/RequestSizeLimitMiddlewareTest.php
@@ -0,0 +1,58 @@
+clear();
+ }
+ $this->middleware = new RequestSizeLimitMiddleware($this->logger);
+ }
+
+ public function testAllowsRequestUnderLimit(): void
+ {
+ $request = $this->createRequest(['Content-Length' => '1024']);
+ $response = new Response();
+ $handler = $this->createHandler($response);
+
+ $result = $this->middleware->process($request, $handler);
+ $this->assertSame($response, $result);
+ }
+
+ public function testBlocksRequestOverLimit(): void
+ {
+ $request = $this->createRequest(['Content-Length' => '20000000']);
+ $response = new Response();
+ $handler = $this->createHandler($response);
+
+ $this->logger->expectLogInfo(sprintf(
+ 'Request too large: %d bytes. URI: %s',
+ 20000000,
+ 'http://localhost/test'
+ ));
+
+ $result = $this->middleware->process($request, $handler);
+ $logBody = json_decode((string)$result->getBody(), true);
+
+ $this->assertEquals(ErrorMessages::get('requestEntityTooLarge')['statusCode'], $result->getStatusCode());
+ $this->assertEquals(
+ ['errors' => [ErrorMessages::get('requestEntityTooLarge')]],
+ $logBody
+ );
+ }
+}
\ No newline at end of file
diff --git a/zmscitizenapi/tests/Zmscitizenapi/Middleware/SecurityHeadersMiddlewareTest.php b/zmscitizenapi/tests/Zmscitizenapi/Middleware/SecurityHeadersMiddlewareTest.php
new file mode 100644
index 000000000..ec29a8448
--- /dev/null
+++ b/zmscitizenapi/tests/Zmscitizenapi/Middleware/SecurityHeadersMiddlewareTest.php
@@ -0,0 +1,63 @@
+clear();
+ }
+ $this->middleware = new SecurityHeadersMiddleware($this->logger);
+ }
+
+ public function testAddsSecurityHeaders(): void
+ {
+ $request = $this->createRequest(['X-Test' => 'test']);
+ $response = new Response();
+ $handler = $this->createHandler($response);
+
+ /*$this->logger->expectLogInfo('Security headers added', [
+ 'uri' => 'http://localhost/test'
+ ]);*/
+
+ $result = $this->middleware->process($request, $handler);
+
+ $this->assertContainsEquals('DENY', $result->getHeader('X-Frame-Options'));
+ $this->assertContainsEquals('nosniff', $result->getHeader('X-Content-Type-Options'));
+ }
+
+ public function testHandlesHeaderException(): void
+ {
+ $request = $this->createRequest(['X-Test' => 'test']);
+ $response = $this->createMock(Response::class);
+ $response->method('withHeader')
+ ->willThrowException(new \RuntimeException('Header error'));
+ $handler = $this->createHandler($response);
+
+ $this->logger->expectLogError(new \RuntimeException('Header error'));
+
+ $result = $this->middleware->process($request, $handler);
+
+ $logBody = json_decode((string)$result->getBody(), true);
+
+ $this->assertEquals(ErrorMessages::get('securityHeaderViolation')['statusCode'], $result->getStatusCode());
+ $this->assertEquals(
+ ['errors' => [ErrorMessages::get('securityHeaderViolation')]],
+ $logBody
+ );
+ }
+}
\ No newline at end of file
diff --git a/zmscitizenapi/tests/Zmscitizenapi/MiddlewareTestCase.php b/zmscitizenapi/tests/Zmscitizenapi/MiddlewareTestCase.php
new file mode 100644
index 000000000..16c9ffeb4
--- /dev/null
+++ b/zmscitizenapi/tests/Zmscitizenapi/MiddlewareTestCase.php
@@ -0,0 +1,64 @@
+logger = new TestLogger(); // Create instance instead of using class name
+ $this->responseFactory = new ResponseFactory();
+ }
+
+ protected function tearDown(): void
+ {
+ TestLogger::verifyNoMoreLogs();
+ TestLogger::resetTest();
+ parent::tearDown();
+ }
+
+ protected function createRequest(array $headerValues = []): ServerRequestInterface
+ {
+ // Normalize headers to PSR-7 format: header name => array of values
+ $headers = [];
+ foreach ($headerValues as $name => $value) {
+ $headers[$name] = (array)$value;
+ }
+
+ $request = $this->createMock(ServerRequestInterface::class);
+ $request->method('getHeaders')->willReturn($headers);
+ $request->method('getHeaderLine')->willReturnCallback(
+ function ($name) use ($headers) {
+ if (isset($headers[$name])) {
+ return implode(', ', $headers[$name]);
+ } else {
+ return '';
+ }
+ }
+ );
+ $request->method('getUri')->willReturn(new Uri('http://localhost/test'));
+ return $request;
+ }
+
+ protected function createHandler(ResponseInterface $response): RequestHandlerInterface
+ {
+ $handler = $this->createMock(RequestHandlerInterface::class);
+ $handler->method('handle')->willReturn($response);
+ return $handler;
+ }
+}
\ No newline at end of file
diff --git a/zmscitizenapi/tests/Zmscitizenapi/TestLogger.php b/zmscitizenapi/tests/Zmscitizenapi/TestLogger.php
new file mode 100644
index 000000000..d1c741b79
--- /dev/null
+++ b/zmscitizenapi/tests/Zmscitizenapi/TestLogger.php
@@ -0,0 +1,91 @@
+ $context Additional context data
+ * @throws \RuntimeException When test case is not initialized
+ */
+ public static function logInfo(string $message, array $context = []): void
+ {
+ if (self::$testCase === null) {
+ throw new \RuntimeException('Test case not initialized. Call initTest() first.');
+ }
+
+ $expected = current(self::$expectedLogs);
+ if (!$expected) {
+ self::$testCase->fail('Unexpected log info: ' . $message);
+ }
+
+ array_shift(self::$expectedLogs);
+ self::$testCase->assertEquals('info', $expected[0]);
+ self::$testCase->assertStringContainsString($expected[1], $message);
+ if (!empty($expected[2])) {
+ foreach ($expected[2] as $key => $value) {
+ self::$testCase->assertArrayHasKey($key, $context);
+ self::$testCase->assertEquals($value, $context[$key]);
+ }
+ }
+ }
+
+ public static function logError(
+ \Throwable $exception,
+ ?RequestInterface $request = null,
+ ?ResponseInterface $response = null,
+ array $context = []
+ ): void {
+ $expected = array_shift(self::$expectedLogs);
+ if (!$expected) {
+ self::$testCase->fail('Unexpected log error: ' . $exception->getMessage());
+ }
+ self::$testCase->assertEquals('error', $expected[0]);
+ self::$testCase->assertInstanceOf(get_class($expected[1]), $exception);
+ }
+
+ public static function verifyNoMoreLogs(): void
+ {
+ self::$testCase->assertEmpty(self::$expectedLogs, 'Expected no more logs');
+ }
+
+ public static function resetTest(): void
+ {
+ self::$testCase = null;
+ self::$expectedLogs = [];
+ }
+}
\ No newline at end of file
diff --git a/zmscitizenapi/tests/Zmscitizenapi/bootstrap.php b/zmscitizenapi/tests/Zmscitizenapi/bootstrap.php
new file mode 100644
index 000000000..f3a67cbae
--- /dev/null
+++ b/zmscitizenapi/tests/Zmscitizenapi/bootstrap.php
@@ -0,0 +1,10 @@
+Es empfiehlt sich, die Adresse in sämtlichen Ausweisdokumenten gleichzeitig mit der Anmeldung zu ändern.
",
+ "links": [
+ {
+ "name": "Datenschutzgrundverordnung",
+ "link": "https://stadt.muenchen.de/infos/dsgvo-datenschutzgrundverordnung.html",
+ "description": false
+ }
+ ],
+ "forms": [
+ {
+ "name": "Datenschutzgrundverordnung",
+ "link": "https://stadt.muenchen.de/infos/dsgvo-datenschutzgrundverordnung.html",
+ "description": false
+ }
+ ],
+ "name": "Adressänderung Personalausweis, Reisepass, eAT",
+ "appointment": {
+ "link": "https://zms-demo.muenchen.de/buergeransicht/#/services/10242339"
+ },
+ "maxQuantity": 5,
+ "public": true,
+ "fees": "gebührenfrei",
+ "duration": 5
+ }
+ }
+ ]
+ }
+}
\ No newline at end of file
diff --git a/zmscitizenapi/tests/Zmscitizenapi/fixtures/POST_confirm_appointment.json b/zmscitizenapi/tests/Zmscitizenapi/fixtures/POST_confirm_appointment.json
new file mode 100644
index 000000000..b416a903d
--- /dev/null
+++ b/zmscitizenapi/tests/Zmscitizenapi/fixtures/POST_confirm_appointment.json
@@ -0,0 +1,165 @@
+{
+ "meta": {
+ "$schema": "https://schema.berlin.de/queuemanagement/metaresult.json",
+ "error": false,
+ "generated": "2024-09-24T16:48:37+02:00",
+ "server": "Zmsapi-ENV ()"
+ },
+ "data": {
+ "$schema": "https://schema.berlin.de/queuemanagement/process.json",
+ "amendment": "",
+ "customTextfield": "Some custom text",
+ "appointments": [
+ {
+ "date": 1727865900,
+ "scope": {
+ "id": "228",
+ "source": "dldb"
+ },
+ "availability": {
+ "id": "6712",
+ "weekday": {
+ "sunday": "0",
+ "monday": "0",
+ "tuesday": "0",
+ "wednesday": "8",
+ "thursday": "0",
+ "friday": "0",
+ "saturday": "0"
+ },
+ "repeat": {
+ "afterWeeks": "1",
+ "weekOfMonth": "0"
+ },
+ "bookable": {
+ "startInDays": "0",
+ "endInDays": "30"
+ },
+ "workstationCount": {
+ "public": "1",
+ "callcenter": "0",
+ "intern": "1"
+ },
+ "lastChange": 1727247741,
+ "multipleSlotsAllowed": "1",
+ "slotTimeInMinutes": "5",
+ "startDate": 1727820000,
+ "endDate": 1735599600,
+ "startTime": "07:30:00",
+ "endTime": "12:50:00",
+ "type": "appointment",
+ "scope": {
+ "id": "228"
+ },
+ "description": "Terminserie-Pass Mittwoch"
+ },
+ "slotCount": "1"
+ }
+ ],
+ "apiclient": {
+ "shortname": "default"
+ },
+ "authKey": "fb43",
+ "clients": [
+ {
+ "familyName": "TEST_USER",
+ "email": "test@muenchen.de",
+ "emailSendCount": "0",
+ "notificationsSendCount": "0",
+ "surveyAccepted": "0",
+ "telephone": "123456789"
+ }
+ ],
+ "createIP": "172.19.0.1",
+ "createTimestamp": "1727853590",
+ "id": "101002",
+ "archiveId": 0,
+ "queue": {
+ "$schema": "https://schema.berlin.de/queuemanagement/queue.json",
+ "arrivalTime": 1727865900,
+ "callCount": "0",
+ "callTime": 0,
+ "number": "101002",
+ "waitingTimeEstimate": 0,
+ "waitingTimeOptimistic": 0,
+ "waitingTime": null,
+ "wayTime": null,
+ "status": "confirmed",
+ "lastCallTime": 0,
+ "destination": null,
+ "destinationHint": null,
+ "withAppointment": "1"
+ },
+ "reminderTimestamp": "0",
+ "requests": [
+ {
+ "id": "10242339",
+ "link": "https://service.berlin.de/dienstleistung/10242339/",
+ "name": "Adressänderung Personalausweis, Reisepass, eAT",
+ "group": "Sonstiges",
+ "source": "dldb",
+ "data": {
+ "authorities": [
+ {
+ "id": "1",
+ "name": "X",
+ "webinfo": "https://www.berlin.de/sen/finanzen/"
+ }
+ ],
+ "locations": [
+ {
+ "location": "102522",
+ "authority": {
+ "id": "1",
+ "name": "X",
+ "webinfo": "https://www.berlin.de/sen/finanzen/"
+ },
+ "url": "https://zms-demo.muenchen.de/buergeransicht/#/services/10242339/locations/102522",
+ "appointment": {
+ "link": "https://zms-demo.muenchen.de/buergeransicht/#/services/10242339/locations/102522",
+ "slots": "0",
+ "external": false,
+ "multiple": "1",
+ "allowed": true
+ },
+ "hint": false
+ }
+ ],
+ "meta": {
+ "lastupdate": "2024-09-25T10:02:05",
+ "url": "https://zms-demo.muenchen.de/buergeransicht/#/services/10242339",
+ "locale": "de",
+ "keywords": "",
+ "translated": true,
+ "hash": "",
+ "id": "10242339"
+ },
+ "id": "10242339",
+ "description": "Es empfiehlt sich, die Adresse in sämtlichen Ausweisdokumenten gleichzeitig mit der Anmeldung zu ändern.
",
+ "links": [
+ {
+ "name": "Datenschutzgrundverordnung",
+ "link": "https://stadt.muenchen.de/infos/dsgvo-datenschutzgrundverordnung.html",
+ "description": false
+ }
+ ],
+ "forms": [
+ {
+ "name": "Datenschutzgrundverordnung",
+ "link": "https://stadt.muenchen.de/infos/dsgvo-datenschutzgrundverordnung.html",
+ "description": false
+ }
+ ],
+ "name": "Adressänderung Personalausweis, Reisepass, eAT",
+ "appointment": {
+ "link": "https://zms-demo.muenchen.de/buergeransicht/#/services/10242339"
+ },
+ "maxQuantity": 5,
+ "public": true,
+ "fees": "gebührenfrei",
+ "duration": 5
+ }
+ }
+ ]
+ }
+}
\ No newline at end of file
diff --git a/zmscitizenapi/tests/Zmscitizenapi/fixtures/POST_preconfirm_appointment.json b/zmscitizenapi/tests/Zmscitizenapi/fixtures/POST_preconfirm_appointment.json
new file mode 100644
index 000000000..c1ed47d82
--- /dev/null
+++ b/zmscitizenapi/tests/Zmscitizenapi/fixtures/POST_preconfirm_appointment.json
@@ -0,0 +1,165 @@
+{
+ "meta": {
+ "$schema": "https://schema.berlin.de/queuemanagement/metaresult.json",
+ "error": false,
+ "generated": "2024-09-24T16:48:37+02:00",
+ "server": "Zmsapi-ENV ()"
+ },
+ "data": {
+ "$schema": "https://schema.berlin.de/queuemanagement/process.json",
+ "amendment": "",
+ "customTextfield": "Some custom text",
+ "appointments": [
+ {
+ "date": 1727865900,
+ "scope": {
+ "id": "228",
+ "source": "dldb"
+ },
+ "availability": {
+ "id": "6712",
+ "weekday": {
+ "sunday": "0",
+ "monday": "0",
+ "tuesday": "0",
+ "wednesday": "8",
+ "thursday": "0",
+ "friday": "0",
+ "saturday": "0"
+ },
+ "repeat": {
+ "afterWeeks": "1",
+ "weekOfMonth": "0"
+ },
+ "bookable": {
+ "startInDays": "0",
+ "endInDays": "30"
+ },
+ "workstationCount": {
+ "public": "1",
+ "callcenter": "0",
+ "intern": "1"
+ },
+ "lastChange": 1727247741,
+ "multipleSlotsAllowed": "1",
+ "slotTimeInMinutes": "5",
+ "startDate": 1727820000,
+ "endDate": 1735599600,
+ "startTime": "07:30:00",
+ "endTime": "12:50:00",
+ "type": "appointment",
+ "scope": {
+ "id": "228"
+ },
+ "description": "Terminserie-Pass Mittwoch"
+ },
+ "slotCount": "1"
+ }
+ ],
+ "apiclient": {
+ "shortname": "default"
+ },
+ "authKey": "fb43",
+ "clients": [
+ {
+ "familyName": "TEST_USER",
+ "email": "test@muenchen.de",
+ "emailSendCount": "0",
+ "notificationsSendCount": "0",
+ "surveyAccepted": "0",
+ "telephone": "123456789"
+ }
+ ],
+ "createIP": "172.19.0.1",
+ "createTimestamp": "1727853590",
+ "id": "101002",
+ "archiveId": 0,
+ "queue": {
+ "$schema": "https://schema.berlin.de/queuemanagement/queue.json",
+ "arrivalTime": 1727865900,
+ "callCount": "0",
+ "callTime": 0,
+ "number": "101002",
+ "waitingTimeEstimate": 0,
+ "waitingTimeOptimistic": 0,
+ "waitingTime": null,
+ "wayTime": null,
+ "status": "preconfirmed",
+ "lastCallTime": 0,
+ "destination": null,
+ "destinationHint": null,
+ "withAppointment": "1"
+ },
+ "reminderTimestamp": "0",
+ "requests": [
+ {
+ "id": "10242339",
+ "link": "https://service.berlin.de/dienstleistung/10242339/",
+ "name": "Adressänderung Personalausweis, Reisepass, eAT",
+ "group": "Sonstiges",
+ "source": "dldb",
+ "data": {
+ "authorities": [
+ {
+ "id": "1",
+ "name": "X",
+ "webinfo": "https://www.berlin.de/sen/finanzen/"
+ }
+ ],
+ "locations": [
+ {
+ "location": "102522",
+ "authority": {
+ "id": "1",
+ "name": "X",
+ "webinfo": "https://www.berlin.de/sen/finanzen/"
+ },
+ "url": "https://zms-demo.muenchen.de/buergeransicht/#/services/10242339/locations/102522",
+ "appointment": {
+ "link": "https://zms-demo.muenchen.de/buergeransicht/#/services/10242339/locations/102522",
+ "slots": "0",
+ "external": false,
+ "multiple": "1",
+ "allowed": true
+ },
+ "hint": false
+ }
+ ],
+ "meta": {
+ "lastupdate": "2024-09-25T10:02:05",
+ "url": "https://zms-demo.muenchen.de/buergeransicht/#/services/10242339",
+ "locale": "de",
+ "keywords": "",
+ "translated": true,
+ "hash": "",
+ "id": "10242339"
+ },
+ "id": "10242339",
+ "description": "Es empfiehlt sich, die Adresse in sämtlichen Ausweisdokumenten gleichzeitig mit der Anmeldung zu ändern.
",
+ "links": [
+ {
+ "name": "Datenschutzgrundverordnung",
+ "link": "https://stadt.muenchen.de/infos/dsgvo-datenschutzgrundverordnung.html",
+ "description": false
+ }
+ ],
+ "forms": [
+ {
+ "name": "Datenschutzgrundverordnung",
+ "link": "https://stadt.muenchen.de/infos/dsgvo-datenschutzgrundverordnung.html",
+ "description": false
+ }
+ ],
+ "name": "Adressänderung Personalausweis, Reisepass, eAT",
+ "appointment": {
+ "link": "https://zms-demo.muenchen.de/buergeransicht/#/services/10242339"
+ },
+ "maxQuantity": 5,
+ "public": true,
+ "fees": "gebührenfrei",
+ "duration": 5
+ }
+ }
+ ]
+ }
+}
\ No newline at end of file
diff --git a/zmscitizenapi/tests/Zmscitizenapi/fixtures/POST_reserve_appointment.json b/zmscitizenapi/tests/Zmscitizenapi/fixtures/POST_reserve_appointment.json
new file mode 100644
index 000000000..9af5e3b9a
--- /dev/null
+++ b/zmscitizenapi/tests/Zmscitizenapi/fixtures/POST_reserve_appointment.json
@@ -0,0 +1,100 @@
+{
+ "$schema": "https://localhost/terminvereinbarung/api/2/",
+ "meta": {
+ "$schema": "https://schema.berlin.de/queuemanagement/metaresult.json",
+ "error": false,
+ "generated": "2024-09-24T16:48:37+02:00",
+ "server": "Zmsapi-ENV ()"
+ },
+ "data": {
+ "$schema": "https://schema.berlin.de/queuemanagement/process.json",
+ "amendment": "",
+ "customTextfield": "",
+ "appointments": [
+ {
+ "date": 32526616522,
+ "scope": {
+ "id": "58",
+ "source": "dldb"
+ },
+ "availability": {
+ "id": 0,
+ "weekday": {
+ "sunday": 0,
+ "monday": 0,
+ "tuesday": 0,
+ "wednesday": 0,
+ "thursday": 0,
+ "friday": 0,
+ "saturday": 0
+ },
+ "repeat": {
+ "afterWeeks": 1,
+ "weekOfMonth": 0
+ },
+ "bookable": {
+ "startInDays": 1,
+ "endInDays": 60
+ },
+ "workstationCount": {
+ "public": 0,
+ "callcenter": 0,
+ "intern": 0
+ },
+ "lastChange": 0,
+ "multipleSlotsAllowed": true,
+ "slotTimeInMinutes": 10,
+ "startDate": 0,
+ "endDate": 0,
+ "startTime": "0:00",
+ "endTime": "23:59",
+ "type": "appointment"
+ },
+ "slotCount": "4"
+ }
+ ],
+ "apiclient": {
+ "shortname": "default"
+ },
+ "authKey": "fb43",
+ "displayInfo": "Infos zum Standort.",
+ "clients": [
+ {
+ "familyName": "TEST_USER",
+ "email": "test@muenchen.de",
+ "emailSendCount": "0",
+ "notificationsSendCount": "0",
+ "surveyAccepted": "0",
+ "telephone": "123456789"
+ }
+ ],
+ "createIP": "",
+ "createTimestamp": 1727189317,
+ "id": "101002",
+ "serviceCount": 1,
+ "archiveId": 0,
+ "queue": {
+ "$schema": "https:\/\/schema.berlin.de\/queuemanagement\/queue.json",
+ "arrivalTime": 1727691300,
+ "callCount": "0",
+ "callTime": 0,
+ "number": "101002",
+ "waitingTimeEstimate": 0,
+ "waitingTimeOptimistic": 0,
+ "waitingTime": null,
+ "wayTime": null,
+ "status": "reserved",
+ "lastCallTime": 0,
+ "destination": null,
+ "destinationHint": null,
+ "withAppointment": "1"
+ },
+ "reminderTimestamp": "0",
+ "scope": {
+ "id": "58",
+ "source": "dldb"
+ },
+ "status": "reserved",
+ "lastChange": 1727189317
+ }
+}
\ No newline at end of file
diff --git a/zmscitizenapi/tests/Zmscitizenapi/fixtures/POST_update_appointment.json b/zmscitizenapi/tests/Zmscitizenapi/fixtures/POST_update_appointment.json
new file mode 100644
index 000000000..af7b65eb8
--- /dev/null
+++ b/zmscitizenapi/tests/Zmscitizenapi/fixtures/POST_update_appointment.json
@@ -0,0 +1,166 @@
+{
+ "$schema": "https://localhost/terminvereinbarung/api/2/",
+ "meta": {
+ "$schema": "https://schema.berlin.de/queuemanagement/metaresult.json",
+ "error": false,
+ "generated": "2024-09-24T16:48:37+02:00",
+ "server": "Zmsapi-ENV ()"
+ },
+ "data": {
+ "$schema": "https://schema.berlin.de/queuemanagement/process.json",
+ "amendment": "",
+ "customTextfield": "Some custom text",
+ "appointments": [
+ {
+ "date": 1727865900,
+ "scope": {
+ "id": "228",
+ "source": "dldb"
+ },
+ "availability": {
+ "id": "6712",
+ "weekday": {
+ "sunday": "0",
+ "monday": "0",
+ "tuesday": "0",
+ "wednesday": "8",
+ "thursday": "0",
+ "friday": "0",
+ "saturday": "0"
+ },
+ "repeat": {
+ "afterWeeks": "1",
+ "weekOfMonth": "0"
+ },
+ "bookable": {
+ "startInDays": "0",
+ "endInDays": "30"
+ },
+ "workstationCount": {
+ "public": "1",
+ "callcenter": "0",
+ "intern": "1"
+ },
+ "lastChange": 1727247741,
+ "multipleSlotsAllowed": "1",
+ "slotTimeInMinutes": "5",
+ "startDate": 1727820000,
+ "endDate": 1735599600,
+ "startTime": "07:30:00",
+ "endTime": "12:50:00",
+ "type": "appointment",
+ "scope": {
+ "id": "228"
+ },
+ "description": "Terminserie-Pass Mittwoch"
+ },
+ "slotCount": "1"
+ }
+ ],
+ "apiclient": {
+ "shortname": "default"
+ },
+ "authKey": "fb43",
+ "clients": [
+ {
+ "familyName": "TEST_USER",
+ "email": "test@muenchen.de",
+ "emailSendCount": "0",
+ "notificationsSendCount": "0",
+ "surveyAccepted": "0",
+ "telephone": "123456789"
+ }
+ ],
+ "createIP": "172.19.0.1",
+ "createTimestamp": "1727853590",
+ "id": "101002",
+ "archiveId": 0,
+ "queue": {
+ "$schema": "https://schema.berlin.de/queuemanagement/queue.json",
+ "arrivalTime": 1727865900,
+ "callCount": "0",
+ "callTime": 0,
+ "number": "101002",
+ "waitingTimeEstimate": 0,
+ "waitingTimeOptimistic": 0,
+ "waitingTime": null,
+ "wayTime": null,
+ "status": "reserved",
+ "lastCallTime": 0,
+ "destination": null,
+ "destinationHint": null,
+ "withAppointment": "1"
+ },
+ "reminderTimestamp": "0",
+ "requests": [
+ {
+ "id": "10242339",
+ "link": "https://service.berlin.de/dienstleistung/10242339/",
+ "name": "Adressänderung Personalausweis, Reisepass, eAT",
+ "group": "Sonstiges",
+ "source": "dldb",
+ "data": {
+ "authorities": [
+ {
+ "id": "1",
+ "name": "X",
+ "webinfo": "https://www.berlin.de/sen/finanzen/"
+ }
+ ],
+ "locations": [
+ {
+ "location": "102522",
+ "authority": {
+ "id": "1",
+ "name": "X",
+ "webinfo": "https://www.berlin.de/sen/finanzen/"
+ },
+ "url": "https://zms-demo.muenchen.de/buergeransicht/#/services/10242339/locations/102522",
+ "appointment": {
+ "link": "https://zms-demo.muenchen.de/buergeransicht/#/services/10242339/locations/102522",
+ "slots": "0",
+ "external": false,
+ "multiple": "1",
+ "allowed": true
+ },
+ "hint": false
+ }
+ ],
+ "meta": {
+ "lastupdate": "2024-09-25T10:02:05",
+ "url": "https://zms-demo.muenchen.de/buergeransicht/#/services/10242339",
+ "locale": "de",
+ "keywords": "",
+ "translated": true,
+ "hash": "",
+ "id": "10242339"
+ },
+ "id": "10242339",
+ "description": "Es empfiehlt sich, die Adresse in sämtlichen Ausweisdokumenten gleichzeitig mit der Anmeldung zu ändern.
",
+ "links": [
+ {
+ "name": "Datenschutzgrundverordnung",
+ "link": "https://stadt.muenchen.de/infos/dsgvo-datenschutzgrundverordnung.html",
+ "description": false
+ }
+ ],
+ "forms": [
+ {
+ "name": "Datenschutzgrundverordnung",
+ "link": "https://stadt.muenchen.de/infos/dsgvo-datenschutzgrundverordnung.html",
+ "description": false
+ }
+ ],
+ "name": "Adressänderung Personalausweis, Reisepass, eAT",
+ "appointment": {
+ "link": "https://zms-demo.muenchen.de/buergeransicht/#/services/10242339"
+ },
+ "maxQuantity": 5,
+ "public": true,
+ "fees": "gebührenfrei",
+ "duration": 5
+ }
+ }
+ ]
+ }
+}
\ No newline at end of file
diff --git a/zmsdb/paratest.xml b/zmsdb/paratest.xml
index 082572423..440f4821e 100644
--- a/zmsdb/paratest.xml
+++ b/zmsdb/paratest.xml
@@ -7,7 +7,7 @@
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
processIsolation="true"
- stopOnFailure="true"
+ stopOnFailure="false"
syntaxCheck="false"
bootstrap="tests/Zmsdb/bootstrap.php"
>
diff --git a/zmsdldb/phpunit.xml.dist b/zmsdldb/phpunit.xml.dist
index a025a9a30..a744641f1 100644
--- a/zmsdldb/phpunit.xml.dist
+++ b/zmsdldb/phpunit.xml.dist
@@ -7,7 +7,7 @@
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
processIsolation="false"
- stopOnFailure="true"
+ stopOnFailure="false"
bootstrap="tests/bootstrap.php"
>
diff --git a/zmsentities/schema/citizenapi/appointmentCancel.json b/zmsentities/schema/citizenapi/appointmentCancel.json
new file mode 100644
index 000000000..49ed26c25
--- /dev/null
+++ b/zmsentities/schema/citizenapi/appointmentCancel.json
@@ -0,0 +1,16 @@
+{
+ "type": "object",
+ "description": "Includes a message indicating the result of a canceled appointment request.",
+ "properties": {
+ "message": {
+ "type": "string",
+ "description": "A human-readable message describing the cancellation result",
+ "minLength": 1,
+ "maxLength": 1000,
+ "pattern": "^[\\p{L}\\p{N}\\s.,!?()-]+$"
+ }
+ },
+ "required": [
+ "message"
+ ]
+}
\ No newline at end of file
diff --git a/zmsentities/schema/citizenapi/appointmentConfirm.json b/zmsentities/schema/citizenapi/appointmentConfirm.json
new file mode 100644
index 000000000..f77648b3a
--- /dev/null
+++ b/zmsentities/schema/citizenapi/appointmentConfirm.json
@@ -0,0 +1,10 @@
+{
+ "type": "object",
+ "description": "Contains a message confirming the successful confirmation of an appointment.",
+ "properties": {
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": ["message"]
+}
diff --git a/zmsentities/schema/citizenapi/appointmentPreconfirm.json b/zmsentities/schema/citizenapi/appointmentPreconfirm.json
new file mode 100644
index 000000000..e2537a038
--- /dev/null
+++ b/zmsentities/schema/citizenapi/appointmentPreconfirm.json
@@ -0,0 +1,10 @@
+{
+ "type": "object",
+ "description": "Provides a message indicating the successful pre-confirmation of an appointment.",
+ "properties": {
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": ["message"]
+}
diff --git a/zmsentities/schema/citizenapi/appointmentReserve.json b/zmsentities/schema/citizenapi/appointmentReserve.json
new file mode 100644
index 000000000..001ee30b8
--- /dev/null
+++ b/zmsentities/schema/citizenapi/appointmentReserve.json
@@ -0,0 +1,13 @@
+{
+ "type": "object",
+ "description": "Includes a message confirming that an appointment has been successfully reserved.",
+ "properties": {
+ "message": {
+ "type": "string",
+ "description": "Confirmation message for the appointment reservation",
+ "minLength": 1,
+ "maxLength": 500
+ }
+ },
+ "required": ["message"]
+}
diff --git a/zmsentities/schema/citizenapi/appointmentUpdate.json b/zmsentities/schema/citizenapi/appointmentUpdate.json
new file mode 100644
index 000000000..2a68fad92
--- /dev/null
+++ b/zmsentities/schema/citizenapi/appointmentUpdate.json
@@ -0,0 +1,10 @@
+{
+ "type": "object",
+ "description": "Provides a message indicating the result of an appointment update operation.",
+ "properties": {
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": ["message"]
+}
diff --git a/zmsentities/schema/citizenapi/availableAppointments.json b/zmsentities/schema/citizenapi/availableAppointments.json
new file mode 100644
index 000000000..c67d8b0e2
--- /dev/null
+++ b/zmsentities/schema/citizenapi/availableAppointments.json
@@ -0,0 +1,16 @@
+{
+ "title": "AvailableAppointments",
+ "type": ["array", "object", "null"],
+ "properties": {
+ "appointmentTimestamps": {
+ "type": ["array", "null"],
+ "description": "Array of available appointment timestamps in seconds since epoch",
+ "items": {
+ "type": "integer",
+ "description": "Timestamp in seconds since epoch"
+ }
+ }
+ },
+ "required": ["appointmentTimestamps"],
+ "description": "Schema defining the available appointments for a specific date, office, and service"
+}
diff --git a/zmsentities/schema/citizenapi/availableDays.json b/zmsentities/schema/citizenapi/availableDays.json
new file mode 100644
index 000000000..d647ef7aa
--- /dev/null
+++ b/zmsentities/schema/citizenapi/availableDays.json
@@ -0,0 +1,17 @@
+{
+ "title": "AvailableDays",
+ "type": ["array", "object", "null"],
+ "properties": {
+ "availableDays": {
+ "type": ["array", "null"],
+ "description": "Array of dates on which appointments are available",
+ "items": {
+ "type": "string",
+ "format": "date",
+ "description": "Date in YYYY-MM-DD format"
+ }
+ }
+ },
+ "required": ["availableDays"],
+ "description": "Schema defining the available days for an office and service"
+}
diff --git a/zmsentities/schema/citizenapi/captcha/altchaCaptcha.json b/zmsentities/schema/citizenapi/captcha/altchaCaptcha.json
new file mode 100644
index 000000000..17fd13255
--- /dev/null
+++ b/zmsentities/schema/citizenapi/captcha/altchaCaptcha.json
@@ -0,0 +1,25 @@
+{
+ "title": "AltchaCaptcha",
+ "type": ["array", "object", "null"],
+ "properties": {
+ "siteKey": {
+ "type": ["string", "null"],
+ "description": "Site key for AltchaCaptcha integration"
+ },
+ "captchaEndpoint": {
+ "type": ["string", "null"],
+ "description": "Endpoint for captcha verification"
+ },
+ "puzzle": {
+ "type": ["string", "null"],
+ "description": "Puzzle endpoint URL for AltchaCaptcha"
+ },
+ "captchaEnabled": {
+ "type": ["boolean", "null"],
+ "description": "Indicates if captcha is enabled"
+ }
+ },
+ "required": ["siteKey", "captchaEndpoint", "puzzle", "captchaEnabled"],
+ "description": "Schema definition for AltchaCaptcha configuration"
+ }
+
\ No newline at end of file
diff --git a/zmsentities/schema/citizenapi/captcha/friendlyCaptcha.json b/zmsentities/schema/citizenapi/captcha/friendlyCaptcha.json
new file mode 100644
index 000000000..03abd9774
--- /dev/null
+++ b/zmsentities/schema/citizenapi/captcha/friendlyCaptcha.json
@@ -0,0 +1,25 @@
+{
+ "title": "FriendlyCaptcha",
+ "type": ["array", "object", "null"],
+ "properties": {
+ "siteKey": {
+ "type": ["string", "null"],
+ "description": "Site key for FriendlyCaptcha integration"
+ },
+ "captchaEndpoint": {
+ "type": ["string", "null"],
+ "description": "Endpoint for captcha verification"
+ },
+ "puzzle": {
+ "type": ["string", "null"],
+ "description": "Puzzle endpoint URL for FriendlyCaptcha"
+ },
+ "captchaEnabled": {
+ "type": ["boolean", "null"],
+ "description": "Indicates if captcha is enabled"
+ }
+ },
+ "required": ["siteKey", "captchaEndpoint", "puzzle", "captchaEnabled"],
+ "description": "Schema definition for FriendlyCaptcha configuration"
+ }
+
\ No newline at end of file
diff --git a/zmsentities/schema/citizenapi/collections/officeList.json b/zmsentities/schema/citizenapi/collections/officeList.json
new file mode 100644
index 000000000..e067acb43
--- /dev/null
+++ b/zmsentities/schema/citizenapi/collections/officeList.json
@@ -0,0 +1,95 @@
+{
+ "type": ["array", "object", "null"],
+ "properties": {
+ "offices": {
+ "type": "array",
+ "items": {
+ "title": "Office",
+ "type": [
+ "array",
+ "object"
+ ],
+ "properties": {
+ "id": {
+ "type": "integer",
+ "description": "Unique identifier for the office"
+ },
+ "name": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "The name of the office"
+ },
+ "address": {
+ "type": [
+ "array",
+ "object",
+ "null"
+ ],
+ "description": "The address of the office",
+ "items": {
+ "type": [
+ "array",
+ "object",
+ "null"
+ ],
+ "properties": {
+ "house_number": {
+ "type": "string",
+ "description": "House number of the address"
+ },
+ "city": {
+ "type": "string",
+ "description": "City of the address"
+ },
+ "postal_code": {
+ "type": "string",
+ "description": "Postal code of the address"
+ },
+ "street": {
+ "type": "string",
+ "description": "Street name of the address"
+ },
+ "hint": {
+ "type": "boolean",
+ "description": "Additional hint about the address"
+ }
+ }
+ }
+ },
+ "geo": {
+ "type": [
+ "array",
+ "object",
+ "null"
+ ],
+ "description": "Geographical coordinates of the office",
+ "properties": {
+ "lat": {
+ "type": "number",
+ "description": "Latitude of a geo coordinate as wgs84 or etrs89",
+ "minimum": -90,
+ "maximum": 90
+ },
+ "lon": {
+ "type": "number",
+ "description": "Longitude of a geo coordinate as wgs84 or etrs89",
+ "minimum": -180,
+ "maximum": 180
+ }
+ }
+ },
+ "scope": {}
+ },
+ "required": [
+ "id"
+ ],
+ "description": "Schema definition for the Office entity"
+ }
+ }
+ },
+ "required": [
+ "offices"
+ ]
+}
\ No newline at end of file
diff --git a/zmsentities/schema/citizenapi/collections/officeServiceAndRelationList.json b/zmsentities/schema/citizenapi/collections/officeServiceAndRelationList.json
new file mode 100644
index 000000000..03a52e30b
--- /dev/null
+++ b/zmsentities/schema/citizenapi/collections/officeServiceAndRelationList.json
@@ -0,0 +1,222 @@
+{
+ "type": [
+ "array",
+ "object",
+ "null"
+ ],
+ "description": "Contains information about offices, services, and their relations. Includes details about each office, available services, and how they are related.",
+ "properties": {
+ "offices": {
+ "type": "array",
+ "items": {
+ "title": "Office",
+ "type": [
+ "array",
+ "object"
+ ],
+ "properties": {
+ "id": {
+ "type": "integer",
+ "description": "Unique identifier for the office"
+ },
+ "name": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "The name of the office"
+ },
+ "address": {
+ "type": [
+ "array",
+ "object",
+ "null"
+ ],
+ "description": "The address of the office",
+ "items": {
+ "type": [
+ "array",
+ "object",
+ "null"
+ ],
+ "properties": {
+ "house_number": {
+ "type": "string",
+ "description": "House number of the address"
+ },
+ "city": {
+ "type": "string",
+ "description": "City of the address"
+ },
+ "postal_code": {
+ "type": "string",
+ "description": "Postal code of the address"
+ },
+ "street": {
+ "type": "string",
+ "description": "Street name of the address"
+ },
+ "hint": {
+ "type": "boolean",
+ "description": "Additional hint about the address"
+ }
+ }
+ }
+ },
+ "geo": {
+ "type": [
+ "array",
+ "object",
+ "null"
+ ],
+ "description": "Geographical coordinates of the office",
+ "properties": {
+ "lat": {
+ "type": "number",
+ "description": "Latitude of a geo coordinate as wgs84 or etrs89",
+ "minimum": -90,
+ "maximum": 90
+ },
+ "lon": {
+ "type": "number",
+ "description": "Longitude of a geo coordinate as wgs84 or etrs89",
+ "minimum": -180,
+ "maximum": 180
+ }
+ }
+ },
+ "scope": {}
+ },
+ "required": [
+ "id"
+ ],
+ "description": "Schema definition for the Office entity"
+ }
+ },
+ "services": {
+ "type": "array",
+ "items": {
+ "type": [
+ "array",
+ "object",
+ "null"
+ ],
+ "properties": {
+ "services": {
+ "type": "array",
+ "items": {
+ "title": "Service",
+ "type": [
+ "array",
+ "object",
+ "null"
+ ],
+ "properties": {
+ "id": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "description": "Unique identifier for the service"
+ },
+ "name": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "Name of the service"
+ },
+ "maxQuantity": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "description": "Maximum quantity of the service"
+ },
+ "combinable": {
+ "type": [
+ "object",
+ "array",
+ "null"
+ ],
+ "description": "Combinable services",
+ "additionalProperties": {
+ "type": [
+ "array",
+ "null"
+ ],
+ "items": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ }
+ },
+ "items": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ }
+ }
+ },
+ "required": [
+ "id",
+ "name"
+ ],
+ "description": "Schema definition for the Service entity"
+ }
+ }
+ },
+ "required": [
+ "services"
+ ]
+ }
+ },
+ "relations": {
+ "type": "array",
+ "items": {
+ "title": "OfficeServiceRelation",
+ "type": [
+ "array",
+ "object",
+ "null"
+ ],
+ "properties": {
+ "officeId": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "description": "Unique identifier for the office"
+ },
+ "serviceId": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "description": "Unique identifier for the service"
+ },
+ "slots": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "description": "Number of slots available for the relation"
+ }
+ },
+ "required": [
+ "officeId",
+ "serviceId",
+ "slots"
+ ],
+ "description": "Schema definition for the Office-Service relation"
+ }
+ }
+ },
+ "required": [
+ "offices",
+ "services",
+ "relations"
+ ]
+}
\ No newline at end of file
diff --git a/zmsentities/schema/citizenapi/collections/officeServiceRelationList.json b/zmsentities/schema/citizenapi/collections/officeServiceRelationList.json
new file mode 100644
index 000000000..af5537aba
--- /dev/null
+++ b/zmsentities/schema/citizenapi/collections/officeServiceRelationList.json
@@ -0,0 +1,31 @@
+{
+ "type": ["array", "object", "null"],
+ "properties": {
+ "relations": {
+ "type": "array",
+ "items": {
+ "title": "OfficeServiceRelation",
+ "type": ["array", "object", "null"],
+ "properties": {
+ "officeId": {
+ "type": ["integer", "null"],
+ "description": "Unique identifier for the office"
+ },
+ "serviceId": {
+ "type": ["integer", "null"],
+ "description": "Unique identifier for the service"
+ },
+ "slots": {
+ "type": ["integer", "null"],
+ "description": "Number of slots available for the relation"
+ }
+ },
+ "required": ["officeId", "serviceId", "slots"],
+ "description": "Schema definition for the Office-Service relation"
+ }
+ }
+ },
+ "required": [
+ "relations"
+ ]
+}
\ No newline at end of file
diff --git a/zmsentities/schema/citizenapi/collections/serviceList.json b/zmsentities/schema/citizenapi/collections/serviceList.json
new file mode 100644
index 000000000..dd2861c01
--- /dev/null
+++ b/zmsentities/schema/citizenapi/collections/serviceList.json
@@ -0,0 +1,73 @@
+{
+ "type": ["array", "object", "null"],
+ "properties": {
+ "services": {
+ "type": "array",
+ "items": {
+ "title": "Service",
+ "type": [
+ "array",
+ "object",
+ "null"
+ ],
+ "properties": {
+ "id": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "description": "Unique identifier for the service"
+ },
+ "name": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "Name of the service"
+ },
+ "maxQuantity": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "description": "Maximum quantity of the service"
+ },
+ "combinable": {
+ "type": [
+ "object",
+ "array",
+ "null"
+ ],
+ "description": "Combinable services",
+ "additionalProperties": {
+ "type": [
+ "array",
+ "null"
+ ],
+ "items": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ }
+ },
+ "items": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ }
+ }
+ },
+ "required": [
+ "id",
+ "name"
+ ],
+ "description": "Schema definition for the Service entity"
+ }
+ }
+ },
+ "required": [
+ "services"
+ ]
+}
\ No newline at end of file
diff --git a/zmsentities/schema/citizenapi/collections/thinnedScopeList.json b/zmsentities/schema/citizenapi/collections/thinnedScopeList.json
new file mode 100644
index 000000000..09793ca99
--- /dev/null
+++ b/zmsentities/schema/citizenapi/collections/thinnedScopeList.json
@@ -0,0 +1,73 @@
+{
+ "type": ["array", "object", "null"],
+ "properties": {
+ "scopes": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "number"
+ },
+ "provider": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "number"
+ },
+ "name": {},
+ "source": {
+ "type": "string"
+ },
+ "contact": {}
+ },
+ "required": [
+ "id",
+ "name",
+ "source",
+ "contact"
+ ]
+ },
+ "shortName": {
+ "type": "string"
+ },
+ "telephoneActivated": {
+ "type": "boolean"
+ },
+ "telephoneRequired": {
+ "type": "boolean"
+ },
+ "customTextfieldActivated": {
+ "type": "boolean"
+ },
+ "customTextfieldRequired": {
+ "type": "boolean"
+ },
+ "customTextfieldLabel": {
+ "type": "string"
+ },
+ "captchaActivatedRequired": {
+ "type": "boolean"
+ },
+ "displayInfo": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "id",
+ "provider",
+ "shortName",
+ "telephoneActivated",
+ "telephoneRequired",
+ "customTextfieldActivated",
+ "customTextfieldRequired",
+ "customTextfieldLabel",
+ "captchaActivatedRequired"
+ ]
+ }
+ }
+ },
+ "required": [
+ "scopes"
+ ]
+}
\ No newline at end of file
diff --git a/zmsentities/schema/citizenapi/combinable.json b/zmsentities/schema/citizenapi/combinable.json
new file mode 100644
index 000000000..09f571aaf
--- /dev/null
+++ b/zmsentities/schema/citizenapi/combinable.json
@@ -0,0 +1,13 @@
+{
+ "type": ["object", "array", "null"],
+ "description": "Combinable services",
+ "additionalProperties": {
+ "type": ["array", "null"],
+ "items": {
+ "type": ["integer", "null"]
+ }
+ },
+ "items": {
+ "type": ["integer", "null"]
+ }
+}
diff --git a/zmsentities/schema/citizenapi/office.json b/zmsentities/schema/citizenapi/office.json
new file mode 100644
index 000000000..ce42c4360
--- /dev/null
+++ b/zmsentities/schema/citizenapi/office.json
@@ -0,0 +1,66 @@
+{
+ "title": "Office",
+ "type": ["array", "object"],
+ "properties": {
+ "id": {
+ "type": "integer",
+ "description": "Unique identifier for the office"
+ },
+ "name": {
+ "type": ["string", "null"],
+ "description": "The name of the office"
+ },
+ "address": {
+ "type": ["array", "object", "null"],
+ "description": "The address of the office",
+ "items": {
+ "type": ["array", "object", "null"],
+ "properties": {
+ "house_number": {
+ "type": "string",
+ "description": "House number of the address"
+ },
+ "city": {
+ "type": "string",
+ "description": "City of the address"
+ },
+ "postal_code": {
+ "type": "string",
+ "description": "Postal code of the address"
+ },
+ "street": {
+ "type": "string",
+ "description": "Street name of the address"
+ },
+ "hint": {
+ "type": "boolean",
+ "description": "Additional hint about the address"
+ }
+ }
+ }
+ },
+ "geo": {
+ "type": ["array", "object", "null"],
+ "description": "Geographical coordinates of the office",
+ "properties": {
+ "lat": {
+ "type": "number",
+ "description": "Latitude of a geo coordinate as wgs84 or etrs89",
+ "minimum": -90,
+ "maximum": 90
+ },
+ "lon": {
+ "type": "number",
+ "description": "Longitude of a geo coordinate as wgs84 or etrs89",
+ "minimum": -180,
+ "maximum": 180
+ }
+ }
+ },
+ "scope": {
+
+ }
+ },
+ "required": ["id"],
+ "description": "Schema definition for the Office entity"
+}
diff --git a/zmsentities/schema/citizenapi/officeServiceRelation.json b/zmsentities/schema/citizenapi/officeServiceRelation.json
new file mode 100644
index 000000000..3c819a5b0
--- /dev/null
+++ b/zmsentities/schema/citizenapi/officeServiceRelation.json
@@ -0,0 +1,21 @@
+{
+ "title": "OfficeServiceRelation",
+ "type": ["array", "object", "null"],
+ "properties": {
+ "officeId": {
+ "type": ["integer", "null"],
+ "description": "Unique identifier for the office"
+ },
+ "serviceId": {
+ "type": ["integer", "null"],
+ "description": "Unique identifier for the service"
+ },
+ "slots": {
+ "type": ["integer", "null"],
+ "description": "Number of slots available for the relation"
+ }
+ },
+ "required": ["officeId", "serviceId", "slots"],
+ "description": "Schema definition for the Office-Service relation"
+ }
+
\ No newline at end of file
diff --git a/zmsentities/schema/citizenapi/processFreeSlots.json b/zmsentities/schema/citizenapi/processFreeSlots.json
new file mode 100644
index 000000000..60a3eaae8
--- /dev/null
+++ b/zmsentities/schema/citizenapi/processFreeSlots.json
@@ -0,0 +1,15 @@
+{
+ "title": "ProcessFreeSlots",
+ "description": "Defines a list of free appointment timestamps returned by processFreeSlots method.",
+ "type": ["array", "object"],
+ "properties": {
+ "appointmentTimestamps": {
+ "type": ["array", "null"],
+ "description": "Numeric timestamps (seconds) for each available slot.",
+ "items": {
+ "type": "integer"
+ }
+ }
+ },
+ "required": ["appointmentTimestamps"]
+ }
\ No newline at end of file
diff --git a/zmsentities/schema/citizenapi/service.json b/zmsentities/schema/citizenapi/service.json
new file mode 100644
index 000000000..528e49905
--- /dev/null
+++ b/zmsentities/schema/citizenapi/service.json
@@ -0,0 +1,34 @@
+{
+ "title": "Service",
+ "type": ["array", "object", "null"],
+ "properties": {
+ "id": {
+ "type": ["integer", "null"],
+ "description": "Unique identifier for the service"
+ },
+ "name": {
+ "type": ["string", "null"],
+ "description": "Name of the service"
+ },
+ "maxQuantity": {
+ "type": ["integer", "null"],
+ "description": "Maximum quantity of the service"
+ },
+ "combinable": {
+ "type": ["object", "array", "null"],
+ "description": "Combinable services",
+ "additionalProperties": {
+ "type": ["array", "null"],
+ "items": {
+ "type": ["integer", "null"]
+ }
+ },
+ "items": {
+ "type": ["integer", "null"]
+ }
+ }
+ },
+ "required": ["id", "name"],
+ "description": "Schema definition for the Service entity"
+ }
+
\ No newline at end of file
diff --git a/zmsentities/schema/citizenapi/thinnedContact.json b/zmsentities/schema/citizenapi/thinnedContact.json
new file mode 100644
index 000000000..d2b9b772f
--- /dev/null
+++ b/zmsentities/schema/citizenapi/thinnedContact.json
@@ -0,0 +1,44 @@
+{
+ "title": "ThinnedContact",
+ "description": "Represents a simplified contact object for the citizen API.",
+ "type": ["array", "object", "null"],
+ "properties": {
+ "city": {
+ "type": ["string", "null"],
+ "description": "The city name."
+ },
+ "country": {
+ "type": ["string", "null"],
+ "description": "The country name."
+ },
+ "name": {
+ "type": ["string", "null"],
+ "description": "Optional name field displayed for contact."
+ },
+ "postalCode": {
+ "type": ["string", "null"],
+ "description": "The postal (ZIP) code."
+ },
+ "region": {
+ "type": ["string", "null"],
+ "description": "The region or state name."
+ },
+ "street": {
+ "type": ["string", "null"],
+ "description": "The street name."
+ },
+ "streetNumber": {
+ "type": ["string", "null"],
+ "description": "The house/street number."
+ }
+ },
+ "required": [
+ "city",
+ "country",
+ "name",
+ "postalCode",
+ "region",
+ "street",
+ "streetNumber"
+ ]
+ }
\ No newline at end of file
diff --git a/zmsentities/schema/citizenapi/thinnedProcess.json b/zmsentities/schema/citizenapi/thinnedProcess.json
new file mode 100644
index 000000000..5c578b79a
--- /dev/null
+++ b/zmsentities/schema/citizenapi/thinnedProcess.json
@@ -0,0 +1,174 @@
+{
+ "title": "ThinnedProcess",
+ "type": ["array", "object", "null"],
+ "properties": {
+ "processId": {
+ "type": ["integer", "null"],
+ "description": "Unique identifier for the process"
+ },
+ "authKey": {
+ "type": ["string", "null"],
+ "description": "Authentication key for the process"
+ },
+ "timestamp": {
+ "type": ["string", "null"],
+ "description": "Timestamp of the process in seconds since epoch"
+ },
+ "familyName": {
+ "type": ["string", "null"],
+ "description": "Family name associated with the process"
+ },
+ "customTextfield": {
+ "type": ["string", "null"],
+ "description": "Custom text field for the process"
+ },
+ "email": {
+ "type": ["string", "null"],
+ "description": "Email associated with the process"
+ },
+ "telephone": {
+ "type": ["string", "null"],
+ "description": "Telephone number associated with the process"
+ },
+ "officeName": {
+ "type": ["string", "null"],
+ "description": "Name of the office handling the process"
+ },
+ "officeId": {
+ "type": ["integer", "null"],
+ "description": "Unique identifier for the office"
+ },
+ "scope": {
+ "type": ["object", "null"],
+ "description": "Scope of the process",
+ "properties": {
+ "id": {
+ "type": ["integer", "null"],
+ "description": "Unique identifier of the scope"
+ },
+ "provider": {
+ "type": ["object", "null"],
+ "properties": {
+ "id": {
+ "type": ["integer", "null"],
+ "description": "Provider ID"
+ },
+ "name": {
+ "type": ["string", "null"],
+ "description": "Provider name"
+ },
+ "source": {
+ "type": ["string", "null"],
+ "description": "Data source of the provider"
+ },
+ "contact": {
+ "type": ["object", "null"],
+ "properties": {
+ "city": {
+ "type": ["string", "null"],
+ "description": "City of the contact"
+ },
+ "country": {
+ "type": ["string", "null"],
+ "description": "Country of the contact"
+ },
+ "name": {
+ "type": ["string", "null"],
+ "description": "Name of the contact"
+ },
+ "postalCode": {
+ "type": ["string", "null"],
+ "description": "Postal code of the contact"
+ },
+ "region": {
+ "type": ["string", "null"],
+ "description": "Region of the contact"
+ },
+ "street": {
+ "type": ["string", "null"],
+ "description": "Street of the contact"
+ },
+ "streetNumber": {
+ "type": ["string", "null"],
+ "description": "Street number of the contact"
+ }
+ }
+ }
+ }
+ },
+ "shortName": {
+ "type": ["string", "null"],
+ "description": "Short name of the scope"
+ },
+ "telephoneActivated": {
+ "type": ["boolean", "null"],
+ "description": "Whether telephone is activated"
+ },
+ "telephoneRequired": {
+ "type": ["boolean", "null"],
+ "description": "Whether telephone is required"
+ },
+ "customTextfieldActivated": {
+ "type": ["boolean", "null"],
+ "description": "Whether custom textfield is activated"
+ },
+ "customTextfieldRequired": {
+ "type": ["boolean", "null"],
+ "description": "Whether custom textfield is required"
+ },
+ "customTextfieldLabel": {
+ "type": ["string", "null"],
+ "description": "Label for the custom textfield"
+ },
+ "captchaActivatedRequired": {
+ "type": ["boolean", "null"],
+ "description": "Whether captcha is activated and required"
+ },
+ "displayInfo": {
+ "type": ["string", "null"],
+ "description": "Additional display information"
+ }
+ }
+ },
+ "status": {
+ "type": ["string", "null"],
+ "enum": [
+ "free",
+ "reserved",
+ "preconfirmed",
+ "confirmed",
+ "queued",
+ "called",
+ "processing",
+ "pending",
+ "pickup",
+ "finished",
+ "missed",
+ "parked",
+ "archived",
+ "deleted",
+ "anonymized",
+ "blocked",
+ "conflict"
+ ],
+ "description": "Status of the process"
+ },
+ "subRequestCounts": {
+ "type": ["array", "null"],
+ "description": "Counts of sub-requests in the process",
+ "items": {
+ "type": ["integer", "null"]
+ }
+ },
+ "serviceId": {
+ "type": ["integer", "null"],
+ "description": "Service ID associated with the process"
+ },
+ "serviceCount": {
+ "type": ["integer", "null"],
+ "description": "Count of services in the process"
+ }
+ },
+ "required": ["processId", "authKey"],
+ "description": "Schema definition for the process entity"
+}
diff --git a/zmsentities/schema/citizenapi/thinnedProvider.json b/zmsentities/schema/citizenapi/thinnedProvider.json
new file mode 100644
index 000000000..7d140e42d
--- /dev/null
+++ b/zmsentities/schema/citizenapi/thinnedProvider.json
@@ -0,0 +1,99 @@
+{
+ "title": "ThinnedProvider",
+ "type": [
+ "array",
+ "object",
+ "null"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "description": "Provider ID"
+ },
+ "name": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "Provider name"
+ },
+ "lat": {
+ "type": [
+ "number",
+ "null"
+ ],
+ "description": "Latitude in decimal degrees."
+ },
+ "lon": {
+ "type": [
+ "number",
+ "null"
+ ],
+ "description": "Longitude in decimal degrees."
+ },
+ "source": {
+ "type": "string",
+ "description": "Data source"
+ },
+ "contact": {
+ "title": "ThinnedContact",
+ "description": "Represents a simplified contact object for the citizen API.",
+ "type": [
+ "array",
+ "object",
+ "null"
+ ],
+ "properties": {
+ "city": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "The city name."
+ },
+ "country": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "The country name."
+ },
+ "name": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "Optional name field displayed for contact."
+ },
+ "postalCode": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "The postal (ZIP) code."
+ },
+ "region": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "The region or state name."
+ },
+ "street": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "The street name."
+ },
+ "streetNumber": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "The house/street number."
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/zmsentities/schema/citizenapi/thinnedScope.json b/zmsentities/schema/citizenapi/thinnedScope.json
new file mode 100644
index 000000000..3c1227bbc
--- /dev/null
+++ b/zmsentities/schema/citizenapi/thinnedScope.json
@@ -0,0 +1,134 @@
+{
+ "title": "ThinnedScope",
+ "type": [
+ "array",
+ "object",
+ "null"
+ ],
+ "description": "The scope of the office's operations",
+ "properties": {
+ "id": {
+ "type": [
+ "string",
+ "integer"
+ ],
+ "description": "Unique identifier of the scope"
+ },
+ "provider": {
+ "type": [
+ "array",
+ "object",
+ "null"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "description": "Provider ID"
+ },
+ "name": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "Provider name"
+ },
+ "source": {
+ "type": "string",
+ "description": "Data source"
+ },
+ "contact": {
+ "type": [
+ "array",
+ "object",
+ "null"
+ ],
+ "properties": {
+ "city": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "country": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "name": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "postalCode": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "region": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "street": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "streetNumber": {
+ "type": [
+ "string",
+ "null"
+ ]
+ }
+ }
+ }
+ }
+ },
+ "shortName": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "Short name of the scope"
+ },
+ "telephoneActivated": {
+ "type": "boolean",
+ "description": "Whether telephone is activated"
+ },
+ "telephoneRequired": {
+ "type": "boolean",
+ "description": "Whether telephone is required"
+ },
+ "customTextfieldActivated": {
+ "type": "boolean",
+ "description": "Whether a custom textfield is activated"
+ },
+ "customTextfieldRequired": {
+ "type": "boolean",
+ "description": "Whether a custom textfield is required"
+ },
+ "customTextfieldLabel": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "Label for the custom textfield"
+ },
+ "captchaActivatedRequired": {
+ "type": "boolean",
+ "description": "Whether captcha is activated and required"
+ },
+ "displayInfo": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "Additional display information"
+ }
+ }
+}
\ No newline at end of file
diff --git a/zmsentities/src/Zmsentities/Process.php b/zmsentities/src/Zmsentities/Process.php
index ad187fc94..0692bca58 100644
--- a/zmsentities/src/Zmsentities/Process.php
+++ b/zmsentities/src/Zmsentities/Process.php
@@ -13,23 +13,23 @@ class Process extends Schema\Entity
{
const PRIMARY = 'id';
- public const STATUS_FREE = 'free';
- public const STATUS_RESERVED = 'reserved';
- public const STATUS_CONFIRMED = 'confirmed';
- public const STATUS_PRECONFIRMED = 'preconfirmed';
- public const STATUS_QUEUED = 'queued';
- public const STATUS_CALLED = 'called';
+ public const STATUS_FREE = 'free';
+ public const STATUS_RESERVED = 'reserved';
+ public const STATUS_CONFIRMED = 'confirmed';
+ public const STATUS_PRECONFIRMED = 'preconfirmed';
+ public const STATUS_QUEUED = 'queued';
+ public const STATUS_CALLED = 'called';
public const STATUS_PROCESSING = 'processing';
- public const STATUS_PENDING = 'pending';
- public const STATUS_PICKUP = 'pickup';
- public const STATUS_FINISHED = 'finished';
- public const STATUS_MISSED = 'missed';
- public const STATUS_PARKED = 'parked';
- public const STATUS_ARCHIVED = 'archived';
- public const STATUS_DELETED = 'deleted';
+ public const STATUS_PENDING = 'pending';
+ public const STATUS_PICKUP = 'pickup';
+ public const STATUS_FINISHED = 'finished';
+ public const STATUS_MISSED = 'missed';
+ public const STATUS_PARKED = 'parked';
+ public const STATUS_ARCHIVED = 'archived';
+ public const STATUS_DELETED = 'deleted';
public const STATUS_ANONYMIZED = 'anonymized';
- public const STATUS_BLOCKED = 'blocked';
- public const STATUS_CONFLICT = 'conflict';
+ public const STATUS_BLOCKED = 'blocked';
+ public const STATUS_CONFLICT = 'conflict';
public static $schema = "process.json";
@@ -114,7 +114,7 @@ public function addRequests($source, $requestCSV)
{
$requestList = $this->getRequests();
foreach (explode(',', $requestCSV) as $id) {
- if (! $requestList->hasRequests($id)) {
+ if (!$requestList->hasRequests($id)) {
$this->requests[] = new Request(array(
'source' => $source,
'id' => $id
@@ -149,22 +149,22 @@ public function hasScopeAdmin()
public function sendAdminMailOnConfirmation()
{
- return (bool)((int)$this->toProperty()->scope->preferences->client->adminMailOnAppointment->get());
+ return (bool) ((int) $this->toProperty()->scope->preferences->client->adminMailOnAppointment->get());
}
-
+
public function sendAdminMailOnDeleted()
{
- return (bool)((int)$this->toProperty()->scope->preferences->client->adminMailOnDeleted->get());
+ return (bool) ((int) $this->toProperty()->scope->preferences->client->adminMailOnDeleted->get());
}
public function sendAdminMailOnUpdated()
{
- return (bool)((int)$this->toProperty()->scope->preferences->client->adminMailOnUpdated->get());
+ return (bool) ((int) $this->toProperty()->scope->preferences->client->adminMailOnUpdated->get());
}
public function shouldSendAdminMailOnClerkMail()
{
- return (bool)((int)$this->toProperty()->scope->preferences->client->adminMailOnMailSent->get());
+ return (bool) ((int) $this->toProperty()->scope->preferences->client->adminMailOnMailSent->get());
}
public function withUpdatedData($requestData, \DateTimeInterface $dateTime, $scope = null, $notice = '')
@@ -248,7 +248,7 @@ public function getAppointments()
if (!$this['appointments'] instanceof Collection\AppointmentList) {
$this['appointments'] = new Collection\AppointmentList($this['appointments']);
foreach ($this['appointments'] as $index => $appointment) {
- if (! $appointment instanceof Appointment) {
+ if (!$appointment instanceof Appointment) {
$this['appointments'][$index] = new Appointment($appointment);
}
}
@@ -265,7 +265,7 @@ public function getClients()
if (!$this['clients'] instanceof Collection\ClientList) {
$this['clients'] = new Collection\ClientList($this['clients']);
foreach ($this['clients'] as $index => $client) {
- if (! $client instanceof Client) {
+ if (!$client instanceof Client) {
$this['clients'][$index] = new Client($client);
}
}
@@ -488,7 +488,7 @@ public function withLessData(array $keepArray = [])
{
$entity = clone $this;
- if (! in_array('availability', $keepArray)) {
+ if (!in_array('availability', $keepArray)) {
foreach ($entity['appointments'] as $appointment) {
if ($appointment->toProperty()->scope->isAvailable()) {
$scopeId = $appointment['scope']['id'];
@@ -513,36 +513,26 @@ public function withLessData(array $keepArray = [])
if ($entity->status == 'free') {
// delete keys
- foreach ([
- 'authKey',
- 'queue',
- 'requests',
- ] as $key) {
- if (! in_array($key, $keepArray) && $entity->toProperty()->$key->isAvailable()) {
+ foreach (['authKey', 'queue', 'requests',] as $key) {
+ if (!in_array($key, $keepArray) && $entity->toProperty()->$key->isAvailable()) {
unset($entity[$key]);
}
}
// delete if empty
- foreach ([
- 'amendment',
- 'id',
- 'authKey',
- 'archiveId',
- 'reminderTimestamp',
- ] as $key) {
- if (! in_array($key, $keepArray) && $entity->toProperty()->$key->isAvailable() && !$entity[$key]) {
+ foreach (['amendment', 'id', 'authKey', 'archiveId', 'reminderTimestamp',] as $key) {
+ if (!in_array($key, $keepArray) && $entity->toProperty()->$key->isAvailable() && !$entity[$key]) {
unset($entity[$key]);
}
}
- if (! in_array('provider', $keepArray) && $entity->toProperty()->scope->provider->data->isAvailable()) {
+ if (!in_array('provider', $keepArray) && $entity->toProperty()->scope->provider->data->isAvailable()) {
unset($entity['scope']['provider']['data']);
}
}
- if (! in_array('dayoff', $keepArray) && $entity->toProperty()->scope->dayoff->isAvailable()) {
+ if (!in_array('dayoff', $keepArray) && $entity->toProperty()->scope->dayoff->isAvailable()) {
unset($entity['scope']['dayoff']);
}
- if (! in_array('scope', $keepArray) && $entity->toProperty()->scope->preferences->isAvailable()) {
+ if (!in_array('scope', $keepArray) && $entity->toProperty()->scope->preferences->isAvailable()) {
unset($entity['scope']['preferences']);
}
return $entity;
@@ -659,11 +649,11 @@ public function __toString()
{
$string = "process#";
$string .= $this->id ?: $this->archiveId;
- $string .= ":".$this->authKey;
+ $string .= ":" . $this->authKey;
$string .= " (" . $this->status . ")";
$string .= " " . $this->getFirstAppointment()->toDateTime()->format('c');
$string .= " " . ($this->isWithAppointment() ? "appoint" : "arrival:" . $this->getArrivalTime()->format('c'));
- $string .= " " . $this->getFirstAppointment()->slotCount."slots";
+ $string .= " " . $this->getFirstAppointment()->slotCount . "slots";
$string .= "*" . count($this->appointments);
foreach ($this->getRequests() as $request) {
$string .= " " . $request['source'] . "." . $request['id'];
diff --git a/zmsentities/src/Zmsentities/Schema/Entity.php b/zmsentities/src/Zmsentities/Schema/Entity.php
index ed695687c..4afbbf8af 100644
--- a/zmsentities/src/Zmsentities/Schema/Entity.php
+++ b/zmsentities/src/Zmsentities/Schema/Entity.php
@@ -87,7 +87,7 @@ public function getDefaults()
/**
* This method is private, because the used library should not be used outside of this class!
*/
- private function getValidator($locale = 'de_DE', $resolveLevel = 0)
+ public function getValidator($locale = 'de_DE', $resolveLevel = 0)
{
$jsonSchema = self::readJsonSchema()->withResolvedReferences($resolveLevel);
$data = (new Schema($this))->withoutRefs();
@@ -101,9 +101,9 @@ private function getValidator($locale = 'de_DE', $resolveLevel = 0)
/**
* Check if the given data validates against the given jsonSchema
*
- * @return Boolean
+ * @return bool
*/
- public function isValid($resolveLevel = 0)
+ public function isValid($resolveLevel = 0): bool
{
$validator = $this->getValidator('de_DE', $resolveLevel = 0);
return $validator->isValid();
@@ -113,9 +113,9 @@ public function isValid($resolveLevel = 0)
* Check if the given data validates against the given jsonSchema
*
* @throws \BO\Zmsentities\Expcetion\SchemaValidation
- * @return Boolean
+ * @return bool
*/
- public function testValid($locale = 'de_DE', $resolveLevel = 0)
+ public function testValid($locale = 'de_DE', $resolveLevel = 0): bool
{
$validator = $this->getValidator($locale, $resolveLevel);
$validator = $this->registerExtensions($validator);
diff --git a/zmsentities/src/Zmsentities/Scope.php b/zmsentities/src/Zmsentities/Scope.php
index 3238f9769..60d50494b 100644
--- a/zmsentities/src/Zmsentities/Scope.php
+++ b/zmsentities/src/Zmsentities/Scope.php
@@ -25,6 +25,46 @@ public function getSource()
return $this->toProperty()->source->get();
}
+ public function getShortName()
+ {
+ return $this->toProperty()->shortName->get();
+ }
+
+ public function getTelephoneActivated()
+ {
+ return $this->getPreference('client', 'telephoneActivated', null);
+ }
+
+ public function getTelephoneRequired()
+ {
+ return $this->getPreference('client', 'telephoneRequired', null);
+ }
+
+ public function getCustomTextfieldActivated()
+ {
+ return $this->getPreference('client', 'customTextfieldActivated', null);
+ }
+
+ public function getCustomTextfieldRequired()
+ {
+ return $this->getPreference('client', 'customTextfieldRequired', null);
+ }
+
+ public function getCustomTextfieldLabel()
+ {
+ return $this->getPreference('client', 'customTextfieldLabel', '');
+ }
+
+ public function getCaptchaActivatedRequired()
+ {
+ return $this->getPreference('client', 'captchaActivatedRequired', null);
+ }
+
+ public function getDisplayInfo()
+ {
+ return $this->getPreference('appointment', 'infoForAppointment', null);
+ }
+
public function getProvider()
{
if (!$this->provider instanceof Provider) {
diff --git a/zmsentities/src/Zmsentities/Source.php b/zmsentities/src/Zmsentities/Source.php
index b0cfaa397..fe6a8fb30 100644
--- a/zmsentities/src/Zmsentities/Source.php
+++ b/zmsentities/src/Zmsentities/Source.php
@@ -51,6 +51,18 @@ public function getProviderList()
return $providerList;
}
+ public function getScopeList()
+ {
+ $scopeList = new Collection\ScopeList();
+ foreach ($this->toProperty()->scopes->get() as $scope) {
+ if (!$scope instanceof Scope) {
+ $scope = new Scope($scope);
+ }
+ $scopeList->addEntity($scope);
+ }
+ return $scopeList;
+ }
+
public function getRequestList()
{
$requestList = new Collection\RequestList();
diff --git a/zmsentities/tests/Zmsentities/ValidationTest.php b/zmsentities/tests/Zmsentities/ValidationTest.php
index c49a221b3..820972f7c 100644
--- a/zmsentities/tests/Zmsentities/ValidationTest.php
+++ b/zmsentities/tests/Zmsentities/ValidationTest.php
@@ -26,7 +26,7 @@ public function testTestValidObject()
$this->fail("Expected exception SchemaValidation not thrown");
} catch (\BO\Zmsentities\Exception\SchemaValidation $exception) {
foreach ($exception->data as $error) {
- $this->assertContains(
+ $this->assertContainsEquals(
'Die E-Mail Adresse muss eine valide E-Mail im Format max@mustermann.de sein',
$error['messages']
);
@@ -44,7 +44,7 @@ public function testTestValidObjectReference()
$this->fail("Expected exception SchemaValidation not thrown");
} catch (\BO\Zmsentities\Exception\SchemaValidation $exception) {
foreach ($exception->data as $error) {
- $this->assertContains(
+ $this->assertContainsEquals(
'Die E-Mail Adresse muss eine valide E-Mail im Format max@mustermann.de sein',
$error['messages']
);
diff --git a/zmsslim/phpunit.xml.dist b/zmsslim/phpunit.xml.dist
index 62e86e02a..cacae0417 100644
--- a/zmsslim/phpunit.xml.dist
+++ b/zmsslim/phpunit.xml.dist
@@ -10,7 +10,7 @@
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
forceCoversAnnotation="false"
- stopOnFailure="true"
+ stopOnFailure="false"
verbose="true"
processIsolation="true"
>
diff --git a/zmsslim/tests/Slim/Middleware/TrailingSlashTest.php b/zmsslim/tests/Slim/Middleware/TrailingSlashTest.php
index 1092dfcb8..ec9b76f2c 100644
--- a/zmsslim/tests/Slim/Middleware/TrailingSlashTest.php
+++ b/zmsslim/tests/Slim/Middleware/TrailingSlashTest.php
@@ -29,6 +29,6 @@ public function testInvoke()
self::assertTrue($response->hasHeader('Location'));
self::assertSame(StatusCodeInterface::STATUS_MOVED_PERMANENTLY, $response->getStatusCode());
- self::assertContains('//localhost/admin/', $response->getHeader('Location'));
+ self::assertContainsEquals('//localhost/admin/', $response->getHeader('Location'));
}
}
diff --git a/zmsslim/tests/Slim/TranslatorTest.php b/zmsslim/tests/Slim/TranslatorTest.php
index cc4198aed..ec4869555 100644
--- a/zmsslim/tests/Slim/TranslatorTest.php
+++ b/zmsslim/tests/Slim/TranslatorTest.php
@@ -23,7 +23,7 @@ public function testTranslatorPoLoader()
);
$translator = new \BO\Slim\LanguageTranslator('de_DE', 'en_GB', 'de');
$this->assertEquals('en_GB', $translator->getInstance()->getLocale());
- $this->assertContains('de_DE', $translator->getInstance()->getFallbackLocales());
+ $this->assertContainsEquals('de_DE', $translator->getInstance()->getFallbackLocales());
// does not work because the default language is not accepted when loading the languages in zmsslim language translator
/*
@@ -55,7 +55,7 @@ public function testTranslatorJsonLoader()
);
$translator = new \BO\Slim\LanguageTranslator('de_DE', 'en_GB', 'de');
$this->assertEquals('en_GB', $translator->getInstance()->getLocale());
- $this->assertContains('de_DE', $translator->getInstance()->getFallbackLocales());
+ $this->assertContainsEquals('de_DE', $translator->getInstance()->getFallbackLocales());
$this->assertEquals(
'das ist ein json Test',
$translator->getInstance()->getCatalogue('de_DE')->get('unittest')