Compare commits
292 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cd7d5f5fe9 | ||
|
|
0e05779f89 | ||
|
|
c8f895c759 | ||
|
|
c40016bba1 | ||
|
|
71e0d65f31 | ||
|
|
da0c3afc14 | ||
|
|
e857a58d00 | ||
|
|
cfac370e2e | ||
|
|
08354b44b6 | ||
|
|
ed593c25b0 | ||
|
|
c52b13cc51 | ||
|
|
7b910c5af6 | ||
|
|
961829a0c7 | ||
|
|
36faed679e | ||
|
|
cef7a4e4b0 | ||
|
|
a87f5756b2 | ||
|
|
0874259eba | ||
|
|
3ac3cbc7ff | ||
|
|
16dfc32f73 | ||
|
|
65df9aa04e | ||
|
|
584fee7e69 | ||
|
|
5d36639705 | ||
|
|
8e42639134 | ||
|
|
a94d8a4b49 | ||
|
|
b9999caf8d | ||
|
|
4d6ff9b726 | ||
|
|
82697ed416 | ||
|
|
0d5958aae6 | ||
|
|
0edef37e50 | ||
|
|
0ea946a2f6 | ||
|
|
f882b431b7 | ||
|
|
889cc66c07 | ||
|
|
9d28162ec4 | ||
|
|
db684cf35f | ||
|
|
7a5c91e55e | ||
|
|
e2263911f3 | ||
|
|
aef658645b | ||
|
|
1e1172b757 | ||
|
|
a15cc29a61 | ||
|
|
b31602b040 | ||
|
|
020e2f8526 | ||
|
|
c5fe5987da | ||
|
|
4b0775b5ea | ||
|
|
7a6c154e5f | ||
|
|
769949b3c3 | ||
|
|
5a0cc6c519 | ||
|
|
99586179d7 | ||
|
|
87e66c1d13 | ||
|
|
b3dc0ff3c4 | ||
|
|
14ceb300cf | ||
|
|
44ffc4d34e | ||
|
|
f9ded6d01c | ||
|
|
4ff3daecab | ||
|
|
1899fd1f0e | ||
|
|
8e213e2e33 | ||
|
|
c7535d3d04 | ||
|
|
b8a0f16efa | ||
|
|
57e12e06b6 | ||
|
|
afdd6cd0a7 | ||
|
|
aeb8addbb8 | ||
|
|
dd05d9166c | ||
|
|
1cf15950c6 | ||
|
|
713c937890 | ||
|
|
91a82af969 | ||
|
|
d8f419cd2c | ||
|
|
c1ab6ef066 | ||
|
|
0a1bfc9aed | ||
|
|
fee9609907 | ||
|
|
cecd808a80 | ||
|
|
bd0cb02389 | ||
|
|
146bbf57bf | ||
|
|
c8eba4a932 | ||
|
|
639a8459d2 | ||
|
|
abde0bd5b9 | ||
|
|
0f6dc42bf2 | ||
|
|
49666d5364 | ||
|
|
9f99e06b7d | ||
|
|
09a506ccca | ||
|
|
5201aa3a5a | ||
|
|
240885577d | ||
|
|
80716ab7f7 | ||
|
|
7d5912e396 | ||
|
|
2831ad972b | ||
|
|
fa5abe7470 | ||
|
|
85f3896337 | ||
|
|
bf087c3bff | ||
|
|
450399fd74 | ||
|
|
46e9c58ad2 | ||
|
|
232561cb83 | ||
|
|
a7354cb051 | ||
|
|
3e4732bcb4 | ||
|
|
f67a8c88c9 | ||
|
|
255590a4d0 | ||
|
|
eacb445b55 | ||
|
|
b51d409a88 | ||
|
|
169fe52a26 | ||
|
|
bd4efb4587 | ||
|
|
5229999365 | ||
|
|
48443e4f82 | ||
|
|
c3f4c29c1a | ||
|
|
353b899c3f | ||
|
|
8cbbefb20b | ||
|
|
87661b2a60 | ||
|
|
17f898e3f9 | ||
|
|
4165bfecb5 | ||
|
|
8a9210123f | ||
|
|
14647576df | ||
|
|
50df658d58 | ||
|
|
3de48c14fd | ||
|
|
ba99113776 | ||
|
|
62104ebe28 | ||
|
|
f0667b7d3e | ||
|
|
5a9480ab93 | ||
|
|
5492ffe217 | ||
|
|
bae633e312 | ||
|
|
ff831beb01 | ||
|
|
d29561fe64 | ||
|
|
ae520d543d | ||
|
|
9e4da0e287 | ||
|
|
15e729cbe2 | ||
|
|
da0b32c7ee | ||
|
|
9a334b5ed1 | ||
|
|
b31ea9cad0 | ||
|
|
e8ed520a38 | ||
|
|
d4db0cffc6 | ||
|
|
3c309dc356 | ||
|
|
bf3e7a93fd | ||
|
|
52588ae999 | ||
|
|
f84a1a7b8c | ||
|
|
30c4483ce9 | ||
|
|
e0c4b34ed8 | ||
|
|
8b96ce90be | ||
|
|
55ea4b55f3 | ||
|
|
3398aacc86 | ||
|
|
6b649a8e7d | ||
|
|
3b3997d547 | ||
|
|
e5df954cf8 | ||
|
|
9ebf712170 | ||
|
|
aad9586ca9 | ||
|
|
35feddfb3d | ||
|
|
d912d4b096 | ||
|
|
8dd54f8824 | ||
|
|
2ae317aad6 | ||
|
|
70600ea623 | ||
|
|
668151fdaf | ||
|
|
9aab392350 | ||
|
|
274432a472 | ||
|
|
0fef2cfc98 | ||
|
|
6dd9ef90ae | ||
|
|
55ae6553d8 | ||
|
|
f79f1e1494 | ||
|
|
85dc683ed9 | ||
|
|
26992f52ff | ||
|
|
9f1b4242b9 | ||
|
|
b2c1b6eb1d | ||
|
|
c997b37047 | ||
|
|
8430458882 | ||
|
|
8a5246b389 | ||
|
|
b171b4a280 | ||
|
|
ea2885115d | ||
|
|
e0bc673f41 | ||
|
|
ecc03eee72 | ||
|
|
dafb8bfdf5 | ||
|
|
16dbdbdad1 | ||
|
|
46b5413f07 | ||
|
|
b36d070db1 | ||
|
|
5037154108 | ||
|
|
e89856966d | ||
|
|
2aa4fa8aed | ||
|
|
42bdae01cc | ||
|
|
31d7a789c4 | ||
|
|
e2e2a357f7 | ||
|
|
1fd2a3a1d8 | ||
|
|
8f6006e209 | ||
|
|
797d1da1b2 | ||
|
|
3ac95cc6f8 | ||
|
|
c2dd9e79c5 | ||
|
|
6e10308f53 | ||
|
|
054d22cc0c | ||
|
|
98e580d327 | ||
|
|
209b7e0193 | ||
|
|
41ce20a86e | ||
|
|
e34218eb27 | ||
|
|
cf5db041db | ||
|
|
a9ef1b910a | ||
|
|
c116a202ab | ||
|
|
bc25ee6f3c | ||
|
|
a1ad396a0e | ||
|
|
418b4e1941 | ||
|
|
23682def0d | ||
|
|
30077e68ad | ||
|
|
4ce5488bae | ||
|
|
668aaba1ca | ||
|
|
29fa8a1b7d | ||
|
|
ad36fd4596 | ||
|
|
4676d3de67 | ||
|
|
9c9d67ad37 | ||
|
|
5a0b68496e | ||
|
|
18e2baa1a8 | ||
|
|
507078363b | ||
|
|
a32e410314 | ||
|
|
8db44cdd53 | ||
|
|
1da093ebd2 | ||
|
|
f0ff70eb0c | ||
|
|
205865f524 | ||
|
|
acdb3ad13e | ||
|
|
284f69ec97 | ||
|
|
e2ae396154 | ||
|
|
8ab2702129 | ||
|
|
d8d745dba9 | ||
|
|
6adab479e0 | ||
|
|
e6a54a8acb | ||
|
|
b66fc01013 | ||
|
|
1f97e35046 | ||
|
|
0da6a85ff8 | ||
|
|
ce81ccd7ec | ||
|
|
c5dcba92eb | ||
|
|
477b01247e | ||
|
|
c359f8d68c | ||
|
|
54752d5d05 | ||
|
|
ec14a31f72 | ||
|
|
7e28b8cd94 | ||
|
|
e79a62b6f3 | ||
|
|
3d9bb99381 | ||
|
|
1f6f2a47f2 | ||
|
|
05357b16ec | ||
|
|
ed5de2f115 | ||
|
|
2da418dd01 | ||
|
|
149edff185 | ||
|
|
d7308fbc36 | ||
|
|
bc32abb4f1 | ||
|
|
ae50117098 | ||
|
|
07715c0097 | ||
|
|
924e435ff4 | ||
|
|
58af50b5c5 | ||
|
|
90af1eb37e | ||
|
|
10d4e9f08b | ||
|
|
a5f99507c2 | ||
|
|
c9d1b56c2a | ||
|
|
b5c79ca5c1 | ||
|
|
695b141053 | ||
|
|
f7e5296a36 | ||
|
|
242eaa60c5 | ||
|
|
a5dbb9ebc3 | ||
|
|
0692ec70c5 | ||
|
|
caa157adaf | ||
|
|
7dffb917e2 | ||
|
|
1a8b3cff7c | ||
|
|
7c1c15460e | ||
|
|
f91a7d0eae | ||
|
|
ab9fce56f5 | ||
|
|
06785a3cb8 | ||
|
|
f9e0448fb6 | ||
|
|
6e7bbe0681 | ||
|
|
b23bbf8334 | ||
|
|
2d9c55ef9c | ||
|
|
7150dc1c7a | ||
|
|
3a332fc1c3 | ||
|
|
6328623474 | ||
|
|
c3e37f8dec | ||
|
|
440f5b4db3 | ||
|
|
3feaa4cddf | ||
|
|
e7db2d4523 | ||
|
|
0048fcf0ab | ||
|
|
fcd0a79251 | ||
|
|
d84914974d | ||
|
|
0ac773f523 | ||
|
|
319e3f9706 | ||
|
|
ec4e0ef178 | ||
|
|
159a21658f | ||
|
|
ae54ee559b | ||
|
|
b3d4d6945e | ||
|
|
79b9b9b808 | ||
|
|
dc49a08128 | ||
|
|
f1bb958609 | ||
|
|
4485340355 | ||
|
|
e83b77d985 | ||
|
|
ae9ab52f4d | ||
|
|
dc8fa4da0b | ||
|
|
3ede971217 | ||
|
|
8118d5a3f9 | ||
|
|
268ce12f07 | ||
|
|
3656e33e64 | ||
|
|
2bd16742dd | ||
|
|
f27ed8f354 | ||
|
|
fa3b38ac98 | ||
|
|
7d1f60cfae | ||
|
|
c07ff0dd57 | ||
|
|
de4b49495a | ||
|
|
5c91bb5c0f | ||
|
|
49ab623187 | ||
|
|
a3b4a4bbca |
1801 changed files with 107171 additions and 60014 deletions
6
.browserslistrc
Normal file
6
.browserslistrc
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
# Browsers that we support
|
||||
|
||||
last 3 version
|
||||
> 0.3%
|
||||
not dead # no browsers without security updates
|
||||
not Firefox < 100
|
||||
|
|
@ -1,2 +1,11 @@
|
|||
.git
|
||||
.git/
|
||||
.gitlab/
|
||||
.gitlab-ci.yml
|
||||
*.md
|
||||
.vscode/
|
||||
auth.json
|
||||
Dockerfile
|
||||
manifest.yaml
|
||||
node_modules/
|
||||
production-resources.yaml
|
||||
.env.example
|
||||
|
|
|
|||
|
|
@ -3,13 +3,33 @@ root = true
|
|||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
indent_size = 2
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[*.{php,json}]
|
||||
indent_size = 4
|
||||
|
||||
[*.blade.php]
|
||||
indent_size = 2
|
||||
|
||||
[*.{yml,yaml}]
|
||||
indent_size = 2
|
||||
|
||||
[{compose,docker-compose}.{yml,yaml}]
|
||||
indent_size = 2
|
||||
|
||||
[*.scss]
|
||||
indent_size = 2
|
||||
quote_type = single
|
||||
|
||||
[*.css]
|
||||
indent_size = 2
|
||||
quote_type = single
|
||||
|
||||
[*.vue]
|
||||
indent_size = 2
|
||||
|
|
|
|||
39
.env.example
39
.env.example
|
|
@ -3,6 +3,7 @@ APP_ENV=local
|
|||
APP_KEY=
|
||||
APP_DEBUG=true
|
||||
APP_URL=http://localhost
|
||||
APP_CURRENCY=EUR
|
||||
|
||||
LOG_CHANNEL=stack
|
||||
LOG_LEVEL=debug
|
||||
|
|
@ -15,22 +16,24 @@ DB_USERNAME=root
|
|||
DB_PASSWORD=
|
||||
|
||||
BROADCAST_DRIVER=log
|
||||
CACHE_DRIVER=file
|
||||
CACHE_STORE=file
|
||||
QUEUE_CONNECTION=sync
|
||||
SESSION_DRIVER=file
|
||||
SESSION_LIFETIME=120
|
||||
|
||||
MAIL_MAILER=smtp
|
||||
MAIL_HOST=smtp.mailtrap.io
|
||||
FILESYSTEM_DISK=local
|
||||
|
||||
MAIL_MAILER=log
|
||||
MAIL_SCHEME=null
|
||||
MAIL_HOST=127.0.0.1
|
||||
MAIL_PORT=2525
|
||||
MAIL_USERNAME=
|
||||
MAIL_PASSWORD=
|
||||
MAIL_ENCRYPTION=
|
||||
MAIL_FROM_ADDRESS=
|
||||
MAIL_USERNAME=null
|
||||
MAIL_PASSWORD=null
|
||||
MAIL_FROM_ADDRESS="hello@example.com"
|
||||
MAIL_FROM_NAME="${APP_NAME}"
|
||||
|
||||
MAIL_LIST_SERVICEWEB=
|
||||
MAIL_LIST_INVALIDINCO=
|
||||
MAIL_TO_CONTACT=
|
||||
MAIL_TO_REGATE=
|
||||
|
||||
# CRM
|
||||
CRM_ENDPOINT=
|
||||
|
|
@ -42,3 +45,21 @@ SENTRY_LARAVEL_DSN=
|
|||
|
||||
# Matomo
|
||||
MATOMO_SITE_ID=
|
||||
|
||||
# UWIL ADMIN SECURE
|
||||
# - LIST SEPARATED WITH ',' (Required)
|
||||
UWIL_ADMIN_SECURE_CIDR_LIST=""
|
||||
# - LIST SEPARATED WITH ',' (Optional)
|
||||
UWIL_ADMIN_SECURE_CIDR_PROJECT_ALLOWED_LIST=""
|
||||
|
||||
# Arcadia API
|
||||
ARCADIA_API_ENDPOINT=
|
||||
ARCADIA_API_APIKEY=
|
||||
ARCADIA_API_SECRET=
|
||||
ARCADIA_API_IGNORE_SELF_SIGNED_CERT=false
|
||||
|
||||
# Serial Caster
|
||||
SERIAL_CASTER_SEED=12345
|
||||
|
||||
# LDC
|
||||
LDC_HEADQUARTERS_PHONE_RECEPTION="02 43 62 70 00"
|
||||
|
|
|
|||
14
.gitattributes
vendored
14
.gitattributes
vendored
|
|
@ -1,5 +1,11 @@
|
|||
* text=auto
|
||||
*.css linguist-vendored
|
||||
*.scss linguist-vendored
|
||||
*.js linguist-vendored
|
||||
* text=auto eol=lf
|
||||
|
||||
*.blade.php diff=html
|
||||
*.css diff=css
|
||||
*.html diff=html
|
||||
*.md diff=markdown
|
||||
*.php diff=php
|
||||
|
||||
/.github export-ignore
|
||||
CHANGELOG.md export-ignore
|
||||
.styleci.yml export-ignore
|
||||
|
|
|
|||
53
.gitignore
vendored
53
.gitignore
vendored
|
|
@ -1,22 +1,51 @@
|
|||
.env
|
||||
.env.testing
|
||||
.env.backup
|
||||
.env.production
|
||||
.phpunit.result.cache
|
||||
.stylelintcache
|
||||
/.fleet
|
||||
/.idea
|
||||
/.phpunit.cache
|
||||
/.vscode
|
||||
/node_modules
|
||||
/bootstrap/cache
|
||||
/public/build
|
||||
/public/cache
|
||||
/public/hot
|
||||
/public/sitemap.xml
|
||||
/public/storage
|
||||
/public/images/content
|
||||
/public/images/gammes
|
||||
/public/images/produits
|
||||
/public/images/slides
|
||||
/public/b-o/
|
||||
/public/mix-manifest.json
|
||||
/storage/*.key
|
||||
/vendor
|
||||
.env
|
||||
.env.backup
|
||||
.env.dev
|
||||
.env.prod
|
||||
.phpunit.result.cache
|
||||
coverage/
|
||||
auth.json
|
||||
docker-compose.override.yml
|
||||
Homestead.json
|
||||
Homestead.yaml
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
auth.json
|
||||
.npmrc
|
||||
js.save
|
||||
phpstan-report.xml
|
||||
|
||||
# Ignore public favicon
|
||||
/public/android-chrome-192x192.png
|
||||
/public/android-chrome-512x512.png
|
||||
/public/apple-touch-icon.png
|
||||
/public/browserconfig.xml
|
||||
/public/browserconfig.xml
|
||||
/public/favicon-16x16.png
|
||||
/public/favicon-32x32.png
|
||||
/public/favicon.ico
|
||||
/public/mstile-150x150.png
|
||||
/public/safari-pinned-tab.svg
|
||||
/public/site.webmanifest
|
||||
/public/files
|
||||
|
||||
develop
|
||||
_ide_helper.php
|
||||
.phpstorm.meta.php
|
||||
phpstan-report.xml
|
||||
|
||||
// Ignore PHPUnit cache files
|
||||
cache
|
||||
|
|
|
|||
349
.gitlab-ci.yml
349
.gitlab-ci.yml
|
|
@ -1,23 +1,328 @@
|
|||
include:
|
||||
- project: "ldc-web/shared-ci"
|
||||
ref: "main"
|
||||
file: "lint/lint7.3.yml"
|
||||
- project: "ldc-web/shared-ci"
|
||||
ref: "main"
|
||||
file: "security/security7.3.yml"
|
||||
- project: "ldc-web/shared-ci"
|
||||
ref: "main"
|
||||
file: "build/build7.3.yml"
|
||||
- project: "ldc-web/shared-ci"
|
||||
ref: "main"
|
||||
file: "test/test.yml"
|
||||
- project: "ldc-web/shared-ci"
|
||||
ref: "main"
|
||||
file: "deploy/deploy.yml"
|
||||
|
||||
stages:
|
||||
- lint
|
||||
- security
|
||||
- build
|
||||
- test
|
||||
- deploy
|
||||
- install
|
||||
- lint
|
||||
- test
|
||||
- security
|
||||
- build
|
||||
- deploy
|
||||
|
||||
include:
|
||||
- project: "ldc-web/shared-ci"
|
||||
ref: "main"
|
||||
file: "security/composer.yml"
|
||||
- project: "ldc-web/shared-ci"
|
||||
ref: "main"
|
||||
file: "build/composer.yml"
|
||||
- project: "ldc-web/shared-ci"
|
||||
ref: "main"
|
||||
file: "build/docker-multistage.yml"
|
||||
- project: "ldc-web/shared-ci"
|
||||
ref: "main"
|
||||
file: "test/test.yml"
|
||||
- project: "ldc-web/shared-ci"
|
||||
ref: "main"
|
||||
file: "deploy/deploy.yml"
|
||||
|
||||
# * LINT --------------------------------------------------------------------
|
||||
|
||||
.php-lint-base: &php-lint-base
|
||||
image: $CI_REGISTRY/ldc-web/projets/ldc-web-images-docker/uwil-ci-8.5:latest
|
||||
variables:
|
||||
COMPOSER_CACHE_DIR: ".composer-cache"
|
||||
cache:
|
||||
key:
|
||||
files:
|
||||
- composer.lock
|
||||
prefix: "composer"
|
||||
policy: pull
|
||||
paths:
|
||||
- .composer-cache/
|
||||
- vendor/
|
||||
before_script:
|
||||
- composer config gitlab-token.lab.s-web.fr $CI_DEPLOY_USER $CI_DEPLOY_PASSWORD
|
||||
- composer install --no-scripts --no-interaction --no-ansi --no-progress --prefer-dist
|
||||
rules:
|
||||
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
||||
- if: $CI_COMMIT_BRANCH == "develop"
|
||||
|
||||
parallel-lint:
|
||||
<<: *php-lint-base
|
||||
stage: lint
|
||||
script:
|
||||
- vendor/bin/parallel-lint --exclude vendor --exclude storage --exclude bootstrap --exclude node_modules .
|
||||
allow_failure: false
|
||||
|
||||
pint:
|
||||
<<: *php-lint-base
|
||||
stage: lint
|
||||
script:
|
||||
- vendor/bin/pint --test
|
||||
allow_failure: false
|
||||
|
||||
phpcs:
|
||||
<<: *php-lint-base
|
||||
stage: lint
|
||||
script:
|
||||
- vendor/bin/phpcs --standard=phpcs.xml --extensions=php --ignore=vendor/*,storage/*,bootstrap/*,node_modules/* --report=junit --report-file=phpcs-report.xml
|
||||
artifacts:
|
||||
when: on_failure
|
||||
expose_as: 'phpcs'
|
||||
paths:
|
||||
- phpcs-report.xml
|
||||
reports:
|
||||
junit: phpcs-report.xml
|
||||
allow_failure: false
|
||||
|
||||
phpstan:
|
||||
<<: *php-lint-base
|
||||
stage: lint
|
||||
variables:
|
||||
PHP_MEMORY_LIMIT: "512M"
|
||||
COMPOSER_CACHE_DIR: ".composer-cache"
|
||||
script:
|
||||
- php -d memory_limit=512M vendor/bin/phpstan --error-format=junit > phpstan-report.xml
|
||||
artifacts:
|
||||
when: on_failure
|
||||
expose_as: 'phpstan'
|
||||
paths:
|
||||
- phpstan-report.xml
|
||||
reports:
|
||||
junit: phpstan-report.xml
|
||||
allow_failure: false
|
||||
|
||||
eslint:
|
||||
stage: lint
|
||||
image: oven/bun:1
|
||||
variables:
|
||||
BUN_INSTALL_CACHE_DIR: ".bun-cache"
|
||||
cache:
|
||||
key:
|
||||
files:
|
||||
- bun.lock
|
||||
prefix: "bun"
|
||||
policy: pull
|
||||
paths:
|
||||
- .bun-cache/
|
||||
before_script:
|
||||
- echo "@ldc-web:registry=https://${CI_SERVER_HOST}/api/v4/packages/npm/" >> .npmrc
|
||||
- echo "//${CI_SERVER_HOST}/api/v4/packages/npm/:_authToken=${CI_DEPLOY_PASSWORD}" >> .npmrc
|
||||
- echo "@uwil:registry=https://lab.s-web.fr/api/v4/groups/ldc-web/-/packages/npm/" >> .npmrc
|
||||
- echo "//lab.s-web.fr/api/v4/groups/ldc-web/-/packages/npm/:_authToken=${CI_DEPLOY_PASSWORD}" >> .npmrc
|
||||
- bun install --no-save
|
||||
script:
|
||||
- bun run eslint-ci --quiet --output-file=eslint-junit.xml
|
||||
artifacts:
|
||||
when: on_failure
|
||||
expose_as: 'eslint'
|
||||
paths:
|
||||
- eslint-junit.xml
|
||||
reports:
|
||||
junit: eslint-junit.xml
|
||||
allow_failure: false
|
||||
rules:
|
||||
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
||||
- if: $CI_COMMIT_BRANCH == "develop"
|
||||
|
||||
stylelint:
|
||||
stage: lint
|
||||
image: oven/bun:1
|
||||
variables:
|
||||
BUN_INSTALL_CACHE_DIR: ".bun-cache"
|
||||
cache:
|
||||
key:
|
||||
files:
|
||||
- bun.lock
|
||||
prefix: "bun"
|
||||
policy: pull
|
||||
paths:
|
||||
- .bun-cache/
|
||||
before_script:
|
||||
- echo "@ldc-web:registry=https://${CI_SERVER_HOST}/api/v4/packages/npm/" >> .npmrc
|
||||
- echo "//${CI_SERVER_HOST}/api/v4/packages/npm/:_authToken=${CI_DEPLOY_PASSWORD}" >> .npmrc
|
||||
- echo "@uwil:registry=https://lab.s-web.fr/api/v4/groups/ldc-web/-/packages/npm/" >> .npmrc
|
||||
- echo "//lab.s-web.fr/api/v4/groups/ldc-web/-/packages/npm/:_authToken=${CI_DEPLOY_PASSWORD}" >> .npmrc
|
||||
- bun install --no-save
|
||||
script:
|
||||
- bun run stylelint-ci --quiet --output-file=stylelint-junit.xml
|
||||
artifacts:
|
||||
when: on_failure
|
||||
expose_as: 'stylelint'
|
||||
paths:
|
||||
- stylelint-junit.xml
|
||||
reports:
|
||||
junit: stylelint-junit.xml
|
||||
allow_failure: false
|
||||
rules:
|
||||
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
||||
- if: $CI_COMMIT_BRANCH == "develop"
|
||||
|
||||
# * TESTS -------------------------------------------------------------------
|
||||
|
||||
bun-build-test:
|
||||
stage: test
|
||||
image: oven/bun:1
|
||||
variables:
|
||||
BUN_INSTALL_CACHE_DIR: ".bun-cache"
|
||||
cache:
|
||||
key:
|
||||
files:
|
||||
- bun.lock
|
||||
prefix: "bun"
|
||||
policy: pull
|
||||
paths:
|
||||
- .bun-cache/
|
||||
before_script:
|
||||
- echo "@ldc-web:registry=https://${CI_SERVER_HOST}/api/v4/packages/npm/" >> .npmrc
|
||||
- echo "//${CI_SERVER_HOST}/api/v4/packages/npm/:_authToken=${CI_DEPLOY_PASSWORD}" >> .npmrc
|
||||
- echo "@uwil:registry=https://lab.s-web.fr/api/v4/groups/ldc-web/-/packages/npm/" >> .npmrc
|
||||
- echo "//lab.s-web.fr/api/v4/groups/ldc-web/-/packages/npm/:_authToken=${CI_DEPLOY_PASSWORD}" >> .npmrc
|
||||
- bun install --no-save
|
||||
script:
|
||||
- timeout 300 bun run prod
|
||||
artifacts:
|
||||
expose_as: 'front-build-test'
|
||||
paths:
|
||||
- public/build/manifest.json
|
||||
allow_failure: false
|
||||
rules:
|
||||
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
||||
- if: $CI_COMMIT_BRANCH == "trunk" || $CI_COMMIT_BRANCH == "master" || $CI_COMMIT_BRANCH == "main"
|
||||
- if: $CI_COMMIT_TAG != null
|
||||
|
||||
tests_front:
|
||||
stage: test
|
||||
image: oven/bun:1
|
||||
variables:
|
||||
BUN_INSTALL_CACHE_DIR: ".bun-cache"
|
||||
cache:
|
||||
key:
|
||||
files:
|
||||
- bun.lock
|
||||
prefix: "bun"
|
||||
policy: pull
|
||||
paths:
|
||||
- .bun-cache/
|
||||
before_script:
|
||||
- echo "@ldc-web:registry=https://${CI_SERVER_HOST}/api/v4/packages/npm/" >> .npmrc
|
||||
- echo "//${CI_SERVER_HOST}/api/v4/packages/npm/:_authToken=${CI_DEPLOY_PASSWORD}" >> .npmrc
|
||||
- echo "@uwil:registry=https://lab.s-web.fr/api/v4/groups/ldc-web/-/packages/npm/" >> .npmrc
|
||||
- echo "//lab.s-web.fr/api/v4/groups/ldc-web/-/packages/npm/:_authToken=${CI_DEPLOY_PASSWORD}" >> .npmrc
|
||||
- bun install --no-save
|
||||
script:
|
||||
- bun run test:unit
|
||||
artifacts:
|
||||
when: always
|
||||
expose_as: 'vitest'
|
||||
paths:
|
||||
- coverage/junit-vitest.xml
|
||||
reports:
|
||||
junit: coverage/junit-vitest.xml
|
||||
allow_failure: false
|
||||
rules:
|
||||
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
||||
- if: $CI_COMMIT_BRANCH == "trunk" || $CI_COMMIT_BRANCH == "master" || $CI_COMMIT_BRANCH == "main"
|
||||
- if: $CI_COMMIT_TAG != null
|
||||
|
||||
tests_laravel:
|
||||
stage: test
|
||||
image: $CI_REGISTRY/ldc-web/projets/ldc-web-images-docker/uwil-ci-8.5:latest
|
||||
allow_failure: false
|
||||
variables:
|
||||
COMPOSER_CACHE_DIR: ".composer-cache"
|
||||
# MYSQL SERVICE VARS
|
||||
MYSQL_DATABASE: testing_db
|
||||
MYSQL_ROOT_PASSWORD: password
|
||||
|
||||
# LARAVEL VARS
|
||||
DB_CONNECTION: mysql
|
||||
DB_HOST: mysql
|
||||
DB_PORT: 3306
|
||||
DB_DATABASE: testing_db
|
||||
DB_USERNAME: root
|
||||
DB_PASSWORD: password
|
||||
cache:
|
||||
- key:
|
||||
files:
|
||||
- composer.lock
|
||||
prefix: "composer"
|
||||
policy: pull
|
||||
paths:
|
||||
- .composer-cache/
|
||||
- vendor/
|
||||
services:
|
||||
- name: mysql:8.0
|
||||
alias: mysql
|
||||
before_script:
|
||||
- composer config gitlab-token.lab.s-web.fr $CI_DEPLOY_USER $CI_DEPLOY_PASSWORD
|
||||
- composer install --no-scripts --no-interaction --no-ansi --no-progress --prefer-dist
|
||||
script:
|
||||
- cp .env.example .env
|
||||
- php artisan key:generate
|
||||
- php -r "while(true){try{new PDO('mysql:host=mysql;dbname=testing_db','root','password');break;}catch(Exception){sleep(1);}}"
|
||||
- php artisan config:clear
|
||||
- php artisan test --coverage --colors=never --log-junit=coverage/junit.xml --coverage-cobertura=coverage/cobertura.xml --coverage-html=coverage/html
|
||||
after_script:
|
||||
- if [ "$CI_JOB_STATUS" == "failed" ]; then cat storage/logs/laravel.log; fi
|
||||
artifacts:
|
||||
when: always
|
||||
expose_as: 'coverage'
|
||||
paths:
|
||||
- coverage/html/
|
||||
reports:
|
||||
junit: coverage/junit.xml
|
||||
coverage_report:
|
||||
coverage_format: cobertura
|
||||
path: coverage/cobertura.xml
|
||||
coverage: '/^\s*Total:\s*([\d\.]+)%/'
|
||||
needs:
|
||||
- job: bun-build-test
|
||||
optional: false
|
||||
artifacts: true
|
||||
rules:
|
||||
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
||||
- if: $CI_COMMIT_BRANCH == "trunk" || $CI_COMMIT_BRANCH == "master" || $CI_COMMIT_BRANCH == "main"
|
||||
- if: $CI_COMMIT_TAG != null
|
||||
|
||||
# * SECURITY ----------------------------------------------------------------
|
||||
|
||||
bun-security:
|
||||
stage: security
|
||||
image: oven/bun:1
|
||||
before_script:
|
||||
- echo "@ldc-web:registry=https://${CI_SERVER_HOST}/api/v4/packages/npm/" >> .npmrc
|
||||
- echo "//${CI_SERVER_HOST}/api/v4/packages/npm/:_authToken=${CI_DEPLOY_PASSWORD}" >> .npmrc
|
||||
- echo "@uwil:registry=https://lab.s-web.fr/api/v4/groups/ldc-web/-/packages/npm/" >> .npmrc
|
||||
- echo "//lab.s-web.fr/api/v4/groups/ldc-web/-/packages/npm/:_authToken=${CI_DEPLOY_PASSWORD}" >> .npmrc
|
||||
script:
|
||||
- bun audit --json > bun-security-checker-report.json
|
||||
artifacts:
|
||||
when: on_failure
|
||||
expose_as: 'bun-security-checker'
|
||||
paths:
|
||||
- bun-security-checker-report.json
|
||||
allow_failure: true
|
||||
rules:
|
||||
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
||||
- if: $CI_COMMIT_BRANCH == "trunk" || $CI_COMMIT_BRANCH == "master" || $CI_COMMIT_BRANCH == "main"
|
||||
- if: $CI_COMMIT_TAG != null
|
||||
|
||||
# * BUILD -------------------------------------------------------------------
|
||||
|
||||
npm-build:
|
||||
stage: build
|
||||
script:
|
||||
- echo "Frontend is built directly within Docker multi-stage build"
|
||||
|
||||
npm-security:
|
||||
stage: security
|
||||
script:
|
||||
- echo "Npm security would run bun security"
|
||||
dependencies:
|
||||
- bun-security
|
||||
needs:
|
||||
- job: bun-security
|
||||
optional: true
|
||||
rules:
|
||||
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
||||
- if: $CI_COMMIT_BRANCH == "trunk" || $CI_COMMIT_BRANCH == "master" || $CI_COMMIT_BRANCH == "main"
|
||||
- if: $CI_COMMIT_TAG != null
|
||||
|
|
|
|||
|
|
@ -8,4 +8,4 @@
|
|||
|
||||
|
||||
## Étapes pour la reproduction du bug
|
||||
*
|
||||
*
|
||||
|
|
|
|||
1058
.intellipest/_pest-helper.php
Normal file
1058
.intellipest/_pest-helper.php
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -5,9 +5,5 @@ php:
|
|||
finder:
|
||||
not-name:
|
||||
- index.php
|
||||
- server.php
|
||||
js:
|
||||
finder:
|
||||
not-name:
|
||||
- webpack.mix.js
|
||||
js: true
|
||||
css: true
|
||||
|
|
|
|||
43
.stylelintrc.json
Normal file
43
.stylelintrc.json
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
{
|
||||
"extends": [
|
||||
"stylelint-config-standard-scss",
|
||||
"stylelint-config-sass-guidelines",
|
||||
"stylelint-config-idiomatic-order"
|
||||
],
|
||||
"plugins": [
|
||||
"stylelint-scss"
|
||||
],
|
||||
"files": [
|
||||
"**/*.css",
|
||||
"**/*.scss"
|
||||
],
|
||||
"ignoreFiles": [
|
||||
"**/vendor/*.css",
|
||||
"**/vendor/**/*.css",
|
||||
"**/vendor/*.scss",
|
||||
"**/vendor/**/*.scss",
|
||||
"**/vendor/*.sass",
|
||||
"**/vendor/**/*.sass",
|
||||
"**/vendor/*.less",
|
||||
"**/vendor/**/*.less"
|
||||
],
|
||||
"customSyntax": "postcss-scss",
|
||||
"rules": {
|
||||
"declaration-property-value-no-unknown": true,
|
||||
"selector-max-id": null,
|
||||
"color-no-invalid-hex": true,
|
||||
"order/properties-alphabetical-order": null,
|
||||
"max-nesting-depth": 5,
|
||||
"selector-max-compound-selectors": 5,
|
||||
"scss/double-slash-comment-empty-line-before": [
|
||||
"always",
|
||||
{
|
||||
"ignore": [
|
||||
"between-comments",
|
||||
"stylelint-commands",
|
||||
"inside-block"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
95
.vscode/settings.json
vendored
95
.vscode/settings.json
vendored
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"workbench.colorCustomizations": {
|
||||
"activityBar.activeBackground": "#920c7f",
|
||||
"activityBar.activeBorder": "#1a1d02",
|
||||
"activityBar.activeBorder": "#e7e7e7",
|
||||
"activityBar.background": "#920c7f",
|
||||
"activityBar.foreground": "#e7e7e7",
|
||||
"activityBar.inactiveForeground": "#e7e7e799",
|
||||
|
|
@ -11,13 +11,100 @@
|
|||
"statusBar.background": "#630856",
|
||||
"statusBar.foreground": "#e7e7e7",
|
||||
"statusBarItem.hoverBackground": "#920c7f",
|
||||
"statusBarItem.remoteBackground": "#630856",
|
||||
"statusBarItem.remoteBackground": "#66760a",
|
||||
"statusBarItem.remoteForeground": "#e7e7e7",
|
||||
"titleBar.activeBackground": "#630856",
|
||||
"titleBar.activeForeground": "#e7e7e7",
|
||||
"titleBar.inactiveBackground": "#63085699",
|
||||
"titleBar.inactiveForeground": "#e7e7e799",
|
||||
"commandCenter.border": "#e7e7e799"
|
||||
"commandCenter.border": "#e7e7e799",
|
||||
"activityBarTop.activeBackground": "#920c7f",
|
||||
"activityBarTop.activeBorder": "#e7e7e7",
|
||||
"activityBarTop.background": "#920c7f",
|
||||
"activityBarTop.foreground": "#e7e7e7",
|
||||
"activityBarTop.inactiveForeground": "#e7e7e799",
|
||||
"commandCenter.foreground": "#e7e7e7",
|
||||
"statusBar.debuggingBackground": "#630856",
|
||||
"statusBar.debuggingForeground": "#e7e7e7",
|
||||
"statusBar.inactiveBackground": "#630856"
|
||||
},
|
||||
"peacock.remoteColor": "#630856"
|
||||
"peacock.color": "#630856",
|
||||
"peacock.remoteColor": "#630856",
|
||||
// "php.validate.executablePath": "/usr/local/bin/php85",
|
||||
// "php.executablePath": "/usr/local/bin/php85",
|
||||
"intelephense.environment.phpVersion": "8.5.0",
|
||||
"intelephense.environment.includePaths": [
|
||||
"./.intellipest",
|
||||
"./vendor/pestphp",
|
||||
"./vendor/phpunit/phpunit",
|
||||
"./vendor/laravel"
|
||||
],
|
||||
"phpstan.binCommand": [
|
||||
"${workspaceFolder}/vendor/bin/phpstan"
|
||||
],
|
||||
"laravel-pint.enable": true,
|
||||
"editor.formatOnSave": true,
|
||||
"eslint.validate": ["javascript", "typescript", "html", "vue"],
|
||||
"[html]": {
|
||||
"editor.formatOnSave": true,
|
||||
"editor.formatOnPaste": true,
|
||||
"editor.defaultFormatter": "rvest.vs-code-prettier-eslint",
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": "always"
|
||||
}
|
||||
},
|
||||
"[blade]": {
|
||||
"editor.formatOnSave": true,
|
||||
"editor.formatOnType": false
|
||||
},
|
||||
"[javascript]": {
|
||||
"editor.autoIndent": "full",
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": "always"
|
||||
},
|
||||
"editor.defaultFormatter": "dbaeumer.vscode-eslint",
|
||||
"editor.detectIndentation": false,
|
||||
"editor.formatOnSave": true
|
||||
},
|
||||
"[scss]": {
|
||||
"editor.defaultFormatter": "stylelint.vscode-stylelint",
|
||||
"editor.formatOnSave": true,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.stylelint": "always"
|
||||
}
|
||||
},
|
||||
"[typescript]": {
|
||||
"editor.autoIndent": "full",
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": "always"
|
||||
},
|
||||
"editor.defaultFormatter": "dbaeumer.vscode-eslint",
|
||||
"editor.detectIndentation": false,
|
||||
"editor.formatOnSave": true,
|
||||
"editor.formatOnSaveMode": "file"
|
||||
},
|
||||
"[php]": {
|
||||
"editor.formatOnSave": true,
|
||||
"editor.formatOnSaveMode": "file"
|
||||
},
|
||||
"[vue]": {
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": "always"
|
||||
}
|
||||
},
|
||||
"conventionalCommits.scopes": [
|
||||
"migration"
|
||||
],
|
||||
"emeraldwalk.runonsave": {
|
||||
"autoClearConsole": false,
|
||||
"commands": [
|
||||
{
|
||||
"match": "\\.php$",
|
||||
"isAsync": true,
|
||||
"cmd": "${workspaceFolder}/vendor/bin/pint ${file}"
|
||||
}
|
||||
]
|
||||
},
|
||||
// "LaravelExtraIntellisense.phpCommand": "docker exec -w /app/srcs -t boutique-cnc-sable-$(whoami)-apache-php-1 php -r \"{code}\"",
|
||||
// "LaravelExtraIntellisense.basePathForCode": "/app/srcs"
|
||||
}
|
||||
|
|
|
|||
68
CHANGELOG.md
68
CHANGELOG.md
|
|
@ -1,3 +1,71 @@
|
|||
# (2026-04-23)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* :bug: delete ldcweb:statuschecker from the kernel 898104d
|
||||
* :bug: error on removeFromCart dc8fa4d
|
||||
* :bug: hide brands for unpublished products a5dbb9e
|
||||
* :bug: set blade directives function in public #88 e7db2d4, closes #88
|
||||
* :bug: update promotion filtering system 0692ec7
|
||||
* :bug: use blade directives helper to prevent blade files from being cached #76 #77 ae54ee5, closes #76 #77
|
||||
* :bug: user->id null on SalarieExportJob + add public holiday with cron + condition on public holiday's calendar 79d9061
|
||||
* :chart_with_upwards_trend: update dashboard statistics #52 f454d35, closes #52
|
||||
* addToCart policy was broken in product #86 + homepage #85 7150dc1, closes #86 #85
|
||||
* Clean and fix cookie notice implementation 683e7fd
|
||||
* Display the correct count of employee in the dashboard c07ff0d
|
||||
* feedback on user basket a3b4a4b
|
||||
* Fix check on salarie and customer relation 0366644
|
||||
* fixed variable name for previous commit - #96 f91a7d0, closes #96
|
||||
* hide prices in products.index <hen the user isn't logged in - #96 ab9fce5, closes #96
|
||||
* prevent division by zero in membership rate calculation f27ed8f
|
||||
* Remove piece of :poop: 2687f58
|
||||
* rename route in sitemap 6328623
|
||||
* reset view cache when updating settings f1bb958
|
||||
* RM console.log e5e44bb
|
||||
* Update matricule format 8ee5a94
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* :poop: Update Matricule format on employees import and order export f156dd6
|
||||
* :poop: Update SalarieSite Enum and Salarie Magasin model method 895843e
|
||||
* :sparkles: add new product filter on food products no art in back-end #53 e900b85, closes #53
|
||||
* :sparkles: adding multiple brands 79b9b9b
|
||||
* :sparkles: Closes #46 3a3d993, closes #46
|
||||
* :sparkles: re-add the stat before the date result on the dashbaord 8118d5a
|
||||
* :sparkles: send an order reminder email b3d4d69
|
||||
* 💬 add wording 31dac9b
|
||||
* 🗃 add new field to show slide only for connected customers #87 fcd0a79, closes #87
|
||||
* Add Laravel K8S 4fb9363
|
||||
* Add missing newsletters folder ed5b2e0
|
||||
* added admin secure fa3b38a
|
||||
* added boolean to show old price on discounts - #95 06785a3, closes #95
|
||||
* Authorize all BO roles for user import - ts-2165 e7baad6
|
||||
* Clean and add CICD for K8S migration 9333185
|
||||
* export file optimization e83b77d
|
||||
* Issue #49 feedback ba6380f, closes #49
|
||||
* Issue #52 feedback f6e06ec, closes #52
|
||||
* Issue #55 feedback bab3bef, closes #55
|
||||
* Make the fu**ing Salarie import works again c332696
|
||||
* Make the fu**ing Salarie import works again 7f53f2a
|
||||
* move uploads file in storage/app/public instead of public/ be69807
|
||||
* re-order products with a select system like support-web - #94 f9e0448, closes #94
|
||||
* Replace Regate EAI order transfert by email 184da02
|
||||
* show only brands with min 1 product - #91 6e7bbe0, closes #91
|
||||
* Update queue connection in manifest a93d191
|
||||
* Update rules c49ca99
|
||||
* update store link in fo navbar #92 2d9c55e, closes #92
|
||||
* Update uploads files folder in controllers 43fc273
|
||||
* updated slider to 25/75 ratio - #83 7c1c154, closes #83
|
||||
|
||||
|
||||
### Reverts
|
||||
|
||||
* Revert "npm audit fix --force to solve all vulnerabilities" 02cfc19
|
||||
* Revert "PHPCS correction 1" 3e9428d
|
||||
|
||||
|
||||
## 07-10-2022
|
||||
* Nouvelle UX (mise à jour et ajout de composants vue, inplémentation de l'intégration dans des blades)
|
||||
|
||||
|
|
|
|||
53
Dockerfile
53
Dockerfile
|
|
@ -1,5 +1,52 @@
|
|||
FROM registry.s-web.fr/ldc-web/projets/ldc-web-images-docker/ldcweb-production-7.3:latest
|
||||
COPY --chown=www-data:www-data . /app/srcs
|
||||
ARG PHP_8_5_TAG=latest
|
||||
|
||||
FROM composer:2.9 AS vendor
|
||||
|
||||
ARG GITLAB_USER
|
||||
ARG GITLAB_PASSWORD
|
||||
|
||||
COPY database/ database/
|
||||
|
||||
COPY composer.json composer.json
|
||||
COPY composer.lock composer.lock
|
||||
|
||||
RUN composer config gitlab-token.lab.s-web.fr "${GITLAB_USER}" "${GITLAB_PASSWORD}"
|
||||
RUN composer install \
|
||||
--ignore-platform-reqs \
|
||||
--no-interaction \
|
||||
--no-plugins \
|
||||
--no-scripts \
|
||||
--no-dev \
|
||||
--prefer-dist
|
||||
|
||||
FROM oven/bun:1.4.4-alpine AS frontend
|
||||
|
||||
ARG GITLAB_PASSWORD
|
||||
|
||||
RUN mkdir -p /app/public/build
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json bun.lock* tsconfig.json vite.config.ts vite.config.js .stylelintrc.json /app/
|
||||
COPY resources/ /app/resources/
|
||||
COPY --from=vendor /app/vendor/ /app/vendor/
|
||||
|
||||
RUN echo "@ldc-web:registry=https://lab.s-web.fr/api/v4/packages/npm/" >> .npmrc && \
|
||||
echo "//lab.s-web.fr/api/v4/packages/npm/:_authToken=${GITLAB_PASSWORD}" >> .npmrc && \
|
||||
echo "@uwil:registry=https://lab.s-web.fr/api/v4/groups/ldc-web/-/packages/npm/" >> .npmrc && \
|
||||
echo "//lab.s-web.fr/api/v4/groups/ldc-web/-/packages/npm/:_authToken=${GITLAB_PASSWORD}" >> .npmrc
|
||||
|
||||
RUN bun install --frozen-lockfile && bun run prod
|
||||
|
||||
ARG PHP_8_5_TAG
|
||||
FROM registry.s-web.fr/ldc-web/projets/ldc-web-images-docker/uwil-production-8.5:${PHP_8_5_TAG}
|
||||
|
||||
ARG USER=www-data
|
||||
|
||||
COPY --chown=${USER}:${USER} . /app/srcs
|
||||
COPY --chown=${USER}:${USER} ./resources/favicon/ /app/srcs/public/
|
||||
COPY --from=vendor --chown=${USER}:${USER} /app/vendor/ /app/srcs/vendor/
|
||||
COPY --from=frontend --chown=${USER}:${USER} /app/public/build/ /app/srcs/public/build/
|
||||
|
||||
USER root
|
||||
RUN chmod -R 755 /app/srcs/storage
|
||||
USER www-data
|
||||
USER ${USER}
|
||||
|
|
|
|||
80
README.md
80
README.md
|
|
@ -1,26 +1,80 @@
|
|||
## Boutique Click N Collect Sablé
|
||||
# Boutique Click N Collect Sablé
|
||||
|
||||
## Table of contents
|
||||
|
||||
- [Boutique Click N Collect Sablé](#boutique-click-n-collect-sablé)
|
||||
- [Table of contents](#table-of-contents)
|
||||
- [Frameworks, Platforms and Libraries](#frameworks-platforms-and-libraries)
|
||||
- [Installation](#installation)
|
||||
- [Tests](#tests)
|
||||
- [Deployment](#deployment)
|
||||
- [Available Commands](#available-commands)
|
||||
- [Changelog](#changelog)
|
||||
- [Architecture](#architecture)
|
||||
- [Règles d'Or (Verrouillées par les tests)](#règles-dor-verrouillées-par-les-tests)
|
||||
|
||||
## Frameworks, Platforms and Libraries
|
||||
<!-- https://github.com/Ileriayo/markdown-badges -->
|
||||
[](https://sass-lang.com/)
|
||||
[](https://www.typescriptlang.org/)
|
||||
[](https://vite.dev/)
|
||||
[](https://www.php.net/)
|
||||
[](https://laravel.com/docs/master)
|
||||
[](https://vuejs.org/)
|
||||
|
||||
## Installation
|
||||
|
||||
```
|
||||
```bash
|
||||
cp .env.example .env
|
||||
vi .env
|
||||
composer install
|
||||
php artisan storage:link
|
||||
cd ./ressources/views/front/
|
||||
mkdir <theme-folder>
|
||||
cd <theme-folder>
|
||||
git clone <theme-repo> .
|
||||
cd ./../../../../
|
||||
npm install && npm run prod
|
||||
|
||||
npm i
|
||||
npm run prod
|
||||
```
|
||||
|
||||
## Back-Office
|
||||
|
||||
Création de compte
|
||||
## Tests
|
||||
|
||||
```bash
|
||||
cp .env.example .env.testing
|
||||
vi .env.testing
|
||||
composer install
|
||||
php artisan storage:link
|
||||
composer run test
|
||||
```
|
||||
|
||||
## Deployment
|
||||
|
||||
```bash
|
||||
php artisan schedule:run
|
||||
```
|
||||
|
||||
## Available Commands
|
||||
|
||||
```bash
|
||||
php artisan ldcweb:admin-secure-sync
|
||||
```
|
||||
|
||||
**Créer un utilisateur :**
|
||||
|
||||
```bash
|
||||
php artisan create:admin
|
||||
```
|
||||
|
||||
URL : /bo
|
||||
## Changelog
|
||||
|
||||
See the [CHANGELOG.md](CHANGELOG.md) file for details.
|
||||
|
||||
## Architecture
|
||||
|
||||
Ce projet suit les principes de **l'Architecture Hexagonale (Ports et Adapteurs)** et du **Domain-Driven Design (DDD)**.
|
||||
L'objectif est d'isoler au maximum la logique métier de l'infrastructure (framework Laravel, base de données, requêtes réseau) pour avoir un code robuste, pérenne et facile à tester.
|
||||
|
||||
### Règles d'Or (Verrouillées par les tests)
|
||||
|
||||
Afin de faciliter l'onboarding et d'assurer que l'architecture ne dérive pas, **Pest Arch** et **PHPStan (Level 9)** garantissent les principes suivants à chaque commit :
|
||||
|
||||
1. **Isolation totale du Domaine (Le Cœur)** : Le code dans `domain/` ne doit **jamais** dépendre du Framework (`Illuminate\`), de `App\`, ou de `Infrastructure\`. Il ne dépend que de PHP natif.
|
||||
2. **Inversion des Dépendances** : L'Application et l'Infrastructure dépendent du Domaine, et jamais l'inverse. Le domaine expose des `Ports` (Interfaces) que l'infrastructure implémente via ses `Adapters`.
|
||||
3. **Entités et Objets-Valeurs Purs** : Le domaine manipule ses propres Entités et `ValueObjects` validés et typés fortement. L'utilisation directe des modèles Eloquent est strictement interdite dans le domaine.
|
||||
4. **Conventions de Nommage** : Chaque composant a une place et un suffixe stricts : `*Service`, `*Repository`, `*Exception`, `*Dto`. Les tests échoueront si la convention n'est pas respectée.
|
||||
|
|
|
|||
33
REGLES METIERS.md
Normal file
33
REGLES METIERS.md
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
# Règles Métiers du projet
|
||||
|
||||
Les règles reposent sur le code de la partie **Domaine**.
|
||||
|
||||
## 1. Planification des Retraits
|
||||
|
||||
La sélection du retrait se fait désormais sur une journée complète et non plus par tranches horaires via `PickupSchedulingService`.
|
||||
|
||||
- **Jour de retrait standard** : Uniquement le **Vendredi** (`PICKUP_DAY = WeekDay::Friday`).
|
||||
- **Heure de clôture (Cutoff)** : **Lundi à 14h00** de la même semaine (`CUTOFF_DAY = Monday`, `CUTOFF_HOUR = 14`).
|
||||
- *Comportement* : Une commande validée avant Lundi 14h00 est préparée pour le vendredi en cours. Passé ce délai, le premier jour disponible bascule automatiquement sur le vendredi de la semaine suivante.
|
||||
- **Gestion des Exceptions** :
|
||||
- Les fermetures et jours fériés sont gérés via des `ShopPickupException`.
|
||||
- Une exception peut **fermer le retrait**, ou proposer un **jour de remplacement**. Ce jour de remplacement peut posséder sa propre limite de validation (`orderDeadlineAt`).
|
||||
- Une tâche planifiée `UpsertShopsPickupExceptionsUsingApiForYear` est lancée pour provisionner les jours fériés de l'année pour toutes les boutiques
|
||||
|
||||
## 2. Limites de Montants
|
||||
|
||||
Les montants pouvant être commandés sont gérés par un service dédié `CustomerOrderLimitPolicy`
|
||||
|
||||
- **Plafond par commande** : **200€ max** (`maxOrderAmountCents = 20000`). Lève une `OrderLimitExceededException`.
|
||||
- **Plafond hebdomadaire** : **200€ max par client** sur l'ensemble de la semaine (`maxWeeklyAmountCents = 20000`). Lève une `WeeklyLimitExceededException`.
|
||||
|
||||
## 3. Disponibilité des Produits
|
||||
|
||||
Elle est géré par le service `ProductAvailabilityService`.
|
||||
|
||||
- **Conditions d'ajout au panier** :
|
||||
|
||||
- Le produit est actif (`status = true`).
|
||||
- Il est éligible INCO (`hasInco = true`).
|
||||
- La date actuelle est supérieure ou égale à `orderable_start` (si défini).
|
||||
- La date actuelle est inférieure à `orderable_end` (si défini).
|
||||
61
RGPD.md
Normal file
61
RGPD.md
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
# RGPD
|
||||
|
||||
## Site
|
||||
|
||||
|
||||
### En base de données
|
||||
|
||||
| Table | Colonne | Contenu | Utilisation |
|
||||
| ----------- | -------------------------- | ----------------------------------------------- | -------------------------------------------------------- |
|
||||
| customers | email | Adresse email | Identifiant du client |
|
||||
| customers | hashcode | Code unique de l'utilisateur en CRM | Identifiant du client en CRM |
|
||||
| customers | created_at | Date | Suivi d'activité |
|
||||
| customers | updated_at | Date | Suivi d'activité |
|
||||
| customers | last_login_at | Date | Suivi de la dernière connexion au compte client |
|
||||
| --- | --- | --- | --- |
|
||||
| orders | employee.matricule | Matricule de l'employé | Identifiant unique de l'employé lié à la commande |
|
||||
| orders | employee.firstname | Prénom de l'employé | Affichage et personnalisation de la commande |
|
||||
| orders | employee.lastname | Nom de l'employé | Affichage et personnalisation de la commande |
|
||||
| orders | employee.facility | Établissement de l'employé | Localisation de l'employé pour la gestion de la commande |
|
||||
| orders | employee.site | Site de l'employé | Localisation du site de l'employé |
|
||||
| orders | employee.service | Service de l'employé | Service d'appartenance de l'employé |
|
||||
| orders | delivery_address.name | Nom du lieu de livraison | Identification du contact ou lieu de livraison |
|
||||
| orders | delivery_address.address | Adresse de livraison | Expédition et livraison de la commande |
|
||||
| orders | delivery_address.postcode | Code postal de livraison | Routage de la livraison |
|
||||
| orders | delivery_address.city | Ville de livraison | Routage de la livraison |
|
||||
| orders | delivery_address.phone | Téléphone de contact pour la livraison | Contact en cas de problème de livraison |
|
||||
| orders | delivery_address.latitude | Latitude de l'adresse de livraison | Géolocalisation pour optimiser la livraison |
|
||||
| orders | delivery_address.longitude | Longitude de l'adresse de livraison | Géolocalisation pour optimiser la livraison |
|
||||
| orders | employee_matricule | Matricule de l'employé (colonne virtuelle) | Indexation et recherche rapide des commandes par employé |
|
||||
| --- | --- | --- | --- |
|
||||
| order_lines | product_id | Identifiant du produit présent dans la commande | Détail de la commande |
|
||||
| order_lines | quantité | Quantité du produit présent dans la commande | Détail de la commande |
|
||||
| --- | --- | --- | --- |
|
||||
| employees | customer_id | Identifiant client | Permet de relier un salarié à un compte client du site |
|
||||
| employees | matricule | Matricule | Matricule du salarié |
|
||||
| employees | nom | Nom | Nom du salarié |
|
||||
| employees | prenom | Prénom | Prénom du salarié |
|
||||
| employees | start_date | Date de début du contrat | Activation du compte |
|
||||
| employees | end_date | Date de fin du contrat | Désactivation du compte |
|
||||
| employees | etablissement | Identifiant établissement | Localiser l'employé pour le dépôt de la commande |
|
||||
| employees | site | Identifiant site | Localiser l'employé pour le dépôt de la commande |
|
||||
| employees | service | Identifiant service | Localiser l'employé pour le dépôt de la commande |
|
||||
| --- | --- | --- | --- |
|
||||
| users | first_name | Prénom | Affichage à différents endroits du BO |
|
||||
| users | last_name | Nom | Affichage à différents endroits du BO |
|
||||
| users | email | Adresse mail | Identifiant, sert à la connexion au BO |
|
||||
|
||||
### Formulaire de contact (marketing)
|
||||
|
||||
| Donnée | Contenu |
|
||||
| -------------- | -------------------------------- |
|
||||
| Nom | Nom de la personne |
|
||||
| Prénom | Prénom de la personne |
|
||||
| Adresse E-mail | Adresse email de la personne |
|
||||
| Pièce jointe | Photo renseignée par la personne |
|
||||
|
||||
## Cookies
|
||||
|
||||
| Service | Utilité |
|
||||
| ------- | ----------------- |
|
||||
| Matomo | Mesure d'audience |
|
||||
32
app/Actions/Basket/AddToBasket.php
Normal file
32
app/Actions/Basket/AddToBasket.php
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Basket;
|
||||
|
||||
use App\Contracts\Action;
|
||||
use App\Contracts\ActionInput;
|
||||
use Domain\Services\BasketService;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
/**
|
||||
* @implements \App\Contracts\Action<\App\Actions\Basket\AddToBasketInput, void>
|
||||
*/
|
||||
final class AddToBasket implements Action
|
||||
{
|
||||
public function __construct(
|
||||
private readonly BasketService $basketService,
|
||||
) {
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function __invoke(ActionInput $input): void
|
||||
{
|
||||
Assert::isInstanceOf($input, AddToBasketInput::class);
|
||||
|
||||
$this->basketService->add(
|
||||
$input->productId,
|
||||
$input->qty
|
||||
);
|
||||
}
|
||||
}
|
||||
19
app/Actions/Basket/AddToBasketInput.php
Normal file
19
app/Actions/Basket/AddToBasketInput.php
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Basket;
|
||||
|
||||
use App\Contracts\ActionInput;
|
||||
use Domain\Models\ValueObjects\Entities\ProductId;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
final readonly class AddToBasketInput implements ActionInput
|
||||
{
|
||||
public function __construct(
|
||||
public ProductId $productId,
|
||||
public int $qty,
|
||||
) {
|
||||
Assert::greaterThan($qty, 0);
|
||||
}
|
||||
}
|
||||
33
app/Actions/Basket/EmptyBasket.php
Normal file
33
app/Actions/Basket/EmptyBasket.php
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Basket;
|
||||
|
||||
use App\Contracts\Action;
|
||||
use App\Contracts\ActionInput;
|
||||
use App\Contracts\ActionInputEmpty;
|
||||
use Domain\Services\BasketService;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
/**
|
||||
* @implements \App\Contracts\Action<\App\Contracts\ActionInputEmpty, void>
|
||||
*/
|
||||
final class EmptyBasket implements Action
|
||||
{
|
||||
public function __construct(
|
||||
private readonly BasketService $basketService,
|
||||
) {
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function __invoke(ActionInput $input): void
|
||||
{
|
||||
Assert::isInstanceOf(
|
||||
$input,
|
||||
ActionInputEmpty::class
|
||||
);
|
||||
|
||||
$this->basketService->empty();
|
||||
}
|
||||
}
|
||||
44
app/Actions/Basket/RemoveFromBasket.php
Normal file
44
app/Actions/Basket/RemoveFromBasket.php
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Basket;
|
||||
|
||||
use App\Contracts\Action;
|
||||
use App\Contracts\ActionInput;
|
||||
use Domain\Services\BasketService;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
/**
|
||||
* @implements \App\Contracts\Action<\App\Actions\Basket\RemoveQuantityFromBasketInput|\App\Actions\Basket\RemoveProductFromBasketInput, void>
|
||||
*/
|
||||
final class RemoveFromBasket implements Action
|
||||
{
|
||||
public function __construct(
|
||||
private readonly BasketService $basketService,
|
||||
) {
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function __invoke(ActionInput $input): void
|
||||
{
|
||||
Assert::isInstanceOfAny(
|
||||
$input,
|
||||
[
|
||||
RemoveQuantityFromBasketInput::class,
|
||||
RemoveProductFromBasketInput::class,
|
||||
]
|
||||
);
|
||||
|
||||
if ($input instanceof RemoveQuantityFromBasketInput) {
|
||||
$this->basketService->remove(
|
||||
$input->productId,
|
||||
$input->qty
|
||||
);
|
||||
} elseif ($input instanceof RemoveProductFromBasketInput) {
|
||||
$this->basketService->removeLine(
|
||||
$input->productId
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
16
app/Actions/Basket/RemoveProductFromBasketInput.php
Normal file
16
app/Actions/Basket/RemoveProductFromBasketInput.php
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Basket;
|
||||
|
||||
use App\Contracts\ActionInput;
|
||||
use Domain\Models\ValueObjects\Entities\ProductId;
|
||||
|
||||
final readonly class RemoveProductFromBasketInput implements ActionInput
|
||||
{
|
||||
public function __construct(
|
||||
public ProductId $productId,
|
||||
) {
|
||||
}
|
||||
}
|
||||
19
app/Actions/Basket/RemoveQuantityFromBasketInput.php
Normal file
19
app/Actions/Basket/RemoveQuantityFromBasketInput.php
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Basket;
|
||||
|
||||
use App\Contracts\ActionInput;
|
||||
use Domain\Models\ValueObjects\Entities\ProductId;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
final readonly class RemoveQuantityFromBasketInput implements ActionInput
|
||||
{
|
||||
public function __construct(
|
||||
public ProductId $productId,
|
||||
public int $qty,
|
||||
) {
|
||||
Assert::greaterThan($qty, 0);
|
||||
}
|
||||
}
|
||||
77
app/Actions/Common/ChangeModelOrder.php
Normal file
77
app/Actions/Common/ChangeModelOrder.php
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Common;
|
||||
|
||||
use App\Contracts\Action;
|
||||
use App\Contracts\ActionInput;
|
||||
use App\Interfaces\ModelWithOrder;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use SortDirection;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
/**
|
||||
* @implements \App\Contracts\Action<\App\Actions\Common\ChangeModelOrderInput, bool>
|
||||
*/
|
||||
final class ChangeModelOrder implements Action
|
||||
{
|
||||
private const int MAX_ORDER_VALUE = 4294967295;
|
||||
|
||||
/**
|
||||
* @param \App\Actions\Common\ChangeModelOrderInput $input
|
||||
*/
|
||||
#[\Override]
|
||||
public function __invoke(ActionInput $input): bool
|
||||
{
|
||||
Assert::isInstanceOf($input, ChangeModelOrderInput::class);
|
||||
|
||||
$model = $input->model;
|
||||
$goUp = $input->direction === SortDirection::Ascending;
|
||||
$currentOrder = $model->getOrder();
|
||||
|
||||
$usesSoftDeletes = in_array(SoftDeletes::class, class_uses_recursive($model), true)
|
||||
|| method_exists($model, 'trashed');
|
||||
|
||||
/** @var Builder<Model&ModelWithOrder> $baseQuery */
|
||||
$baseQuery = $usesSoftDeletes
|
||||
// @phpstan-ignore method.notFound (withTrashed)
|
||||
? $model::query()->withTrashed()
|
||||
: $model::query();
|
||||
|
||||
/** @var (Model&ModelWithOrder)|null $adjacentModel */
|
||||
$adjacentModel = $baseQuery
|
||||
->where('order', $goUp ? '<' : '>', $currentOrder)
|
||||
->orderBy('order', $goUp ? SortDirection::Descending : SortDirection::Ascending)
|
||||
->first();
|
||||
|
||||
if ($adjacentModel === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->permuteModelOrders($model, $adjacentModel);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private function permuteModelOrders(Model & ModelWithOrder $model1, Model & ModelWithOrder $model2): void
|
||||
{
|
||||
$order1 = $model1->getOrder();
|
||||
$order2 = $model2->getOrder();
|
||||
|
||||
// Deactivate timestamps to avoid updating updated_at
|
||||
$model1->timestamps = false;
|
||||
$model2->timestamps = false;
|
||||
|
||||
$model1->setOrder(self::MAX_ORDER_VALUE);
|
||||
$model1->saveQuietly();
|
||||
|
||||
$model2->setOrder($order1);
|
||||
$model2->saveQuietly();
|
||||
|
||||
$model1->setOrder($order2);
|
||||
$model1->saveQuietly();
|
||||
}
|
||||
}
|
||||
22
app/Actions/Common/ChangeModelOrderInput.php
Normal file
22
app/Actions/Common/ChangeModelOrderInput.php
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Common;
|
||||
|
||||
use App\Contracts\ActionInput;
|
||||
use App\Interfaces\ModelWithOrder;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use SortDirection;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
final readonly class ChangeModelOrderInput implements ActionInput
|
||||
{
|
||||
public function __construct(
|
||||
public Model & ModelWithOrder $model,
|
||||
public SortDirection $direction,
|
||||
) {
|
||||
Assert::isInstanceOf($this->model, Model::class);
|
||||
Assert::isInstanceOf($this->model, ModelWithOrder::class);
|
||||
}
|
||||
}
|
||||
102
app/Actions/Common/ChangeModelOrderPosition.php
Normal file
102
app/Actions/Common/ChangeModelOrderPosition.php
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Common;
|
||||
|
||||
use App\Contracts\Action;
|
||||
use App\Contracts\ActionInput;
|
||||
use App\Interfaces\ModelWithOrder;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
/**
|
||||
* @implements \App\Contracts\Action<\App\Actions\Common\ChangeModelOrderPositionInput, bool>
|
||||
*/
|
||||
final class ChangeModelOrderPosition implements Action
|
||||
{
|
||||
private const int MAX_ORDER_VALUE = 4294967295;
|
||||
|
||||
/**
|
||||
* @param \App\Actions\Common\ChangeModelOrderPositionInput $input
|
||||
*/
|
||||
#[\Override]
|
||||
public function __invoke(ActionInput $input): bool
|
||||
{
|
||||
Assert::isInstanceOf($input, ChangeModelOrderPositionInput::class);
|
||||
|
||||
$model = $input->model;
|
||||
$currentOrder = $model->getOrder();
|
||||
$targetOrder = $input->targetOrder;
|
||||
|
||||
if ($currentOrder === $targetOrder) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$usesSoftDeletes = in_array(SoftDeletes::class, class_uses_recursive($model), true)
|
||||
|| method_exists($model, 'trashed');
|
||||
|
||||
/** @var Builder<Model&ModelWithOrder> $baseQuery */
|
||||
$baseQuery = $usesSoftDeletes
|
||||
// @phpstan-ignore method.notFound (withTrashed)
|
||||
? $model::query()->withTrashed()
|
||||
: $model::query();
|
||||
|
||||
/** @var int|null $maxOrder */
|
||||
$maxOrder = (clone $baseQuery)->max('order');
|
||||
if ($maxOrder !== null && $targetOrder > $maxOrder) {
|
||||
$targetOrder = (int) $maxOrder;
|
||||
}
|
||||
|
||||
if ($currentOrder === $targetOrder) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Deactivate timestamps
|
||||
$model->timestamps = false;
|
||||
|
||||
// Step 1: Temporarily park the moving model at MAX_ORDER_VALUE
|
||||
$model->setOrder(self::MAX_ORDER_VALUE);
|
||||
$model->saveQuietly();
|
||||
|
||||
if ($targetOrder < $currentOrder) {
|
||||
// Moving UP
|
||||
/** @var \Illuminate\Database\Eloquent\Collection<int, Model&ModelWithOrder> $itemsToShift */
|
||||
$itemsToShift = (clone $baseQuery)
|
||||
->where('order', '>=', $targetOrder)
|
||||
->where('order', '<', $currentOrder)
|
||||
->whereKeyNot($model->getKey())
|
||||
->orderBy('order', 'desc')
|
||||
->get();
|
||||
|
||||
foreach ($itemsToShift as $item) {
|
||||
$item->timestamps = false;
|
||||
$item->setOrder($item->getOrder() + 1);
|
||||
$item->saveQuietly();
|
||||
}
|
||||
} else {
|
||||
// Moving DOWN
|
||||
/** @var \Illuminate\Database\Eloquent\Collection<int, Model&ModelWithOrder> $itemsToShift */
|
||||
$itemsToShift = (clone $baseQuery)
|
||||
->where('order', '>', $currentOrder)
|
||||
->where('order', '<=', $targetOrder)
|
||||
->whereKeyNot($model->getKey())
|
||||
->orderBy('order', 'asc')
|
||||
->get();
|
||||
|
||||
foreach ($itemsToShift as $item) {
|
||||
$item->timestamps = false;
|
||||
$item->setOrder($item->getOrder() - 1);
|
||||
$item->saveQuietly();
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2: Set the moving model to the target order
|
||||
$model->setOrder($targetOrder);
|
||||
$model->saveQuietly();
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
22
app/Actions/Common/ChangeModelOrderPositionInput.php
Normal file
22
app/Actions/Common/ChangeModelOrderPositionInput.php
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Common;
|
||||
|
||||
use App\Contracts\ActionInput;
|
||||
use App\Interfaces\ModelWithOrder;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
final readonly class ChangeModelOrderPositionInput implements ActionInput
|
||||
{
|
||||
public function __construct(
|
||||
public Model & ModelWithOrder $model,
|
||||
public int $targetOrder,
|
||||
) {
|
||||
Assert::isInstanceOf($this->model, Model::class);
|
||||
Assert::isInstanceOf($this->model, ModelWithOrder::class);
|
||||
Assert::greaterThanEq($this->targetOrder, 1);
|
||||
}
|
||||
}
|
||||
100
app/Actions/Common/ExecuteMassAction.php
Normal file
100
app/Actions/Common/ExecuteMassAction.php
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Common;
|
||||
|
||||
use App\Contracts\Action;
|
||||
use App\Contracts\ActionInput;
|
||||
use App\Enums\MassActionType;
|
||||
use App\Interfaces\ModelWithStar;
|
||||
use App\Interfaces\ModelWithStatus;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
/**
|
||||
* @implements \App\Contracts\Action<\App\Actions\Common\ExecuteMassActionInput, int>
|
||||
*/
|
||||
final class ExecuteMassAction implements Action
|
||||
{
|
||||
/**
|
||||
* @param \App\Actions\Common\ExecuteMassActionInput $input
|
||||
*/
|
||||
#[\Override]
|
||||
public function __invoke(ActionInput $input): int
|
||||
{
|
||||
Assert::isInstanceOf($input, ExecuteMassActionInput::class);
|
||||
|
||||
$this->assertModelMatchesInput($input);
|
||||
|
||||
return $this->updateModel($input);
|
||||
}
|
||||
|
||||
private function assertModelMatchesInput(ExecuteMassActionInput $input): void
|
||||
{
|
||||
$modelClass = $input->modelClass;
|
||||
switch ($input->action) {
|
||||
case MassActionType::Star:
|
||||
case MassActionType::Unstar:
|
||||
Assert::isAOf($modelClass, ModelWithStar::class);
|
||||
break;
|
||||
|
||||
case MassActionType::Publish:
|
||||
case MassActionType::Unpublish:
|
||||
Assert::isAOf($modelClass, ModelWithStatus::class);
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new \UnexpectedValueException('Unsupported mass action type: ' . $input->action->value);
|
||||
}
|
||||
}
|
||||
|
||||
private function updateModel(ExecuteMassActionInput $input): int
|
||||
{
|
||||
$modelClass = $input->modelClass;
|
||||
/** @var class-string<Model> $modelClass */
|
||||
$model = new $modelClass();
|
||||
|
||||
$models = $modelClass::query()
|
||||
->whereIn($model->getKeyName(), $input->modelKeyList)
|
||||
->get();
|
||||
|
||||
$updated = 0;
|
||||
|
||||
foreach ($models as $foundModel) {
|
||||
if (! $foundModel instanceof Model) {
|
||||
continue;
|
||||
}
|
||||
|
||||
switch ($input->action) {
|
||||
case MassActionType::Star:
|
||||
/** @var Model&ModelWithStar $foundModel */
|
||||
$foundModel->setStar(true);
|
||||
break;
|
||||
|
||||
case MassActionType::Unstar:
|
||||
/** @var Model&ModelWithStar $foundModel */
|
||||
$foundModel->setStar(false);
|
||||
break;
|
||||
|
||||
case MassActionType::Publish:
|
||||
/** @var Model&ModelWithStatus $foundModel */
|
||||
$foundModel->setStatus(true);
|
||||
break;
|
||||
|
||||
case MassActionType::Unpublish:
|
||||
/** @var Model&ModelWithStatus $foundModel */
|
||||
$foundModel->setStatus(false);
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new \UnexpectedValueException('Unsupported mass action type: ' . $input->action->value);
|
||||
}
|
||||
|
||||
$foundModel->saveOrFail();
|
||||
$updated++;
|
||||
}
|
||||
|
||||
return $updated;
|
||||
}
|
||||
}
|
||||
32
app/Actions/Common/ExecuteMassActionInput.php
Normal file
32
app/Actions/Common/ExecuteMassActionInput.php
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Common;
|
||||
|
||||
use App\Contracts\ActionInput;
|
||||
use App\Enums\MassActionType;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
final readonly class ExecuteMassActionInput implements ActionInput
|
||||
{
|
||||
/**
|
||||
* @param class-string<Model> $modelClass
|
||||
* @param array<int|string> $modelKeyList
|
||||
*/
|
||||
public function __construct(
|
||||
public string $modelClass,
|
||||
public MassActionType $action,
|
||||
public array $modelKeyList,
|
||||
) {
|
||||
Assert::classExists($this->modelClass);
|
||||
Assert::isAOf($this->modelClass, Model::class);
|
||||
Assert::notEmpty($this->modelKeyList);
|
||||
Assert::allScalar($this->modelKeyList);
|
||||
|
||||
foreach ($this->modelKeyList as $modelKey) {
|
||||
Assert::stringNotEmpty((string) $modelKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
32
app/Actions/Common/ToggleModelStar.php
Normal file
32
app/Actions/Common/ToggleModelStar.php
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Common;
|
||||
|
||||
use App\Contracts\Action;
|
||||
use App\Contracts\ActionInput;
|
||||
use App\Interfaces\ModelWithStar;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
/**
|
||||
* @implements \App\Contracts\Action<\App\Actions\Common\ToggleModelStarInput, \Illuminate\Database\Eloquent\Model&\App\Interfaces\ModelWithStar>
|
||||
*/
|
||||
final class ToggleModelStar implements Action
|
||||
{
|
||||
/**
|
||||
* @param \App\Actions\Common\ToggleModelStarInput $input
|
||||
*/
|
||||
#[\Override]
|
||||
public function __invoke(ActionInput $input): Model & ModelWithStar
|
||||
{
|
||||
Assert::isInstanceOf($input, ToggleModelStarInput::class);
|
||||
|
||||
$model = $input->model;
|
||||
$model->toggleStar();
|
||||
$model->saveOrFail();
|
||||
|
||||
return $model;
|
||||
}
|
||||
}
|
||||
20
app/Actions/Common/ToggleModelStarInput.php
Normal file
20
app/Actions/Common/ToggleModelStarInput.php
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Common;
|
||||
|
||||
use App\Contracts\ActionInput;
|
||||
use App\Interfaces\ModelWithStar;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
final readonly class ToggleModelStarInput implements ActionInput
|
||||
{
|
||||
public function __construct(
|
||||
public Model & ModelWithStar $model,
|
||||
) {
|
||||
Assert::isInstanceOf($this->model, Model::class);
|
||||
Assert::isInstanceOf($this->model, ModelWithStar::class);
|
||||
}
|
||||
}
|
||||
32
app/Actions/Common/ToggleModelStatus.php
Normal file
32
app/Actions/Common/ToggleModelStatus.php
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Common;
|
||||
|
||||
use App\Contracts\Action;
|
||||
use App\Contracts\ActionInput;
|
||||
use App\Interfaces\ModelWithStatus;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
/**
|
||||
* @implements \App\Contracts\Action<\App\Actions\Common\ToggleModelStatusInput, \Illuminate\Database\Eloquent\Model&\App\Interfaces\ModelWithStatus>
|
||||
*/
|
||||
final class ToggleModelStatus implements Action
|
||||
{
|
||||
/**
|
||||
* @param \App\Actions\Common\ToggleModelStatusInput $input
|
||||
*/
|
||||
#[\Override]
|
||||
public function __invoke(ActionInput $input): Model & ModelWithStatus
|
||||
{
|
||||
Assert::isInstanceOf($input, ToggleModelStatusInput::class);
|
||||
|
||||
$model = $input->model;
|
||||
$model->toggleStatus();
|
||||
$model->saveOrFail();
|
||||
|
||||
return $model;
|
||||
}
|
||||
}
|
||||
20
app/Actions/Common/ToggleModelStatusInput.php
Normal file
20
app/Actions/Common/ToggleModelStatusInput.php
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Common;
|
||||
|
||||
use App\Contracts\ActionInput;
|
||||
use App\Interfaces\ModelWithStatus;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
final readonly class ToggleModelStatusInput implements ActionInput
|
||||
{
|
||||
public function __construct(
|
||||
public Model & ModelWithStatus $model,
|
||||
) {
|
||||
Assert::isInstanceOf($this->model, Model::class);
|
||||
Assert::isInstanceOf($this->model, ModelWithStatus::class);
|
||||
}
|
||||
}
|
||||
39
app/Actions/Crm/UnsubscribeFromNewsletter.php
Normal file
39
app/Actions/Crm/UnsubscribeFromNewsletter.php
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Crm;
|
||||
|
||||
use App\Contracts\Action;
|
||||
use App\Contracts\ActionInput;
|
||||
use App\Services\CrmService;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
/**
|
||||
* @implements \App\Contracts\Action<\App\Actions\Crm\UnsubscribeFromNewsletterInput, \App\Actions\Crm\UnsubscribeFromNewsletterOutput>
|
||||
*/
|
||||
final class UnsubscribeFromNewsletter implements Action
|
||||
{
|
||||
public function __construct(
|
||||
private readonly CrmService $crmService
|
||||
) {
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function __invoke(ActionInput $input): UnsubscribeFromNewsletterOutput
|
||||
{
|
||||
Assert::isInstanceOf($input, UnsubscribeFromNewsletterInput::class);
|
||||
|
||||
try {
|
||||
$isUnsubscribed = $this->crmService->unsubscribeCustomer(
|
||||
$input->hashcode
|
||||
);
|
||||
|
||||
return $isUnsubscribed ? UnsubscribeFromNewsletterOutput::SUCCESS : UnsubscribeFromNewsletterOutput::FAILURE;
|
||||
} catch (\Throwable $e) {
|
||||
report($e);
|
||||
|
||||
return UnsubscribeFromNewsletterOutput::FAILURE;
|
||||
}
|
||||
}
|
||||
}
|
||||
16
app/Actions/Crm/UnsubscribeFromNewsletterInput.php
Normal file
16
app/Actions/Crm/UnsubscribeFromNewsletterInput.php
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Crm;
|
||||
|
||||
use App\Contracts\ActionInput;
|
||||
use Shared\ValueObjects\CrmHashcode;
|
||||
|
||||
final readonly class UnsubscribeFromNewsletterInput implements ActionInput
|
||||
{
|
||||
public function __construct(
|
||||
public CrmHashcode $hashcode
|
||||
) {
|
||||
}
|
||||
}
|
||||
12
app/Actions/Crm/UnsubscribeFromNewsletterOutput.php
Normal file
12
app/Actions/Crm/UnsubscribeFromNewsletterOutput.php
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Crm;
|
||||
|
||||
enum UnsubscribeFromNewsletterOutput
|
||||
{
|
||||
case SUCCESS;
|
||||
case INVALIDLINK;
|
||||
case FAILURE;
|
||||
}
|
||||
62
app/Actions/Crm/UpdateCustomer.php
Normal file
62
app/Actions/Crm/UpdateCustomer.php
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Crm;
|
||||
|
||||
use App\Contracts\Action;
|
||||
use App\Contracts\ActionInput;
|
||||
use App\Services\CrmService;
|
||||
use Infrastructure\Adapters\Services\AppCustomerAuthService;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
/**
|
||||
* @implements \App\Contracts\Action<\App\Actions\Crm\UpdateCustomerInput, \App\Actions\Crm\UpdateCustomerOutput>
|
||||
*/
|
||||
final class UpdateCustomer implements Action
|
||||
{
|
||||
public function __construct(
|
||||
private readonly AppCustomerAuthService $authService,
|
||||
private readonly CrmService $crmService
|
||||
) {
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function __invoke(ActionInput $input): UpdateCustomerOutput
|
||||
{
|
||||
Assert::isInstanceOf($input, UpdateCustomerInput::class);
|
||||
|
||||
$customer = $this->authService->getCustomerAsMandatory();
|
||||
|
||||
try {
|
||||
/** @var array{firstname:string,lastname:string,birth_date:string,phone?:string,address?:string,postcode:string,city:string,opt_in?:bool} $data */
|
||||
$data = [
|
||||
'firstname' => $input->firstname,
|
||||
'lastname' => $input->lastname,
|
||||
'birth_date' => $input->birthDate,
|
||||
'postcode' => $input->postcode,
|
||||
'city' => $input->city,
|
||||
'opt_in' => $input->optIn,
|
||||
];
|
||||
|
||||
if ($input->phone !== null) {
|
||||
$data['phone'] = $input->phone;
|
||||
}
|
||||
|
||||
if ($input->address !== null) {
|
||||
$data['address'] = $input->address;
|
||||
}
|
||||
|
||||
$result = $this->crmService->updateCustomerInfos(
|
||||
$customer,
|
||||
$data
|
||||
);
|
||||
|
||||
return new UpdateCustomerOutput($result);
|
||||
} catch (\Throwable $e) {
|
||||
report($e);
|
||||
|
||||
return new UpdateCustomerOutput(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
22
app/Actions/Crm/UpdateCustomerInput.php
Normal file
22
app/Actions/Crm/UpdateCustomerInput.php
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Crm;
|
||||
|
||||
use App\Contracts\ActionInput;
|
||||
|
||||
final readonly class UpdateCustomerInput implements ActionInput
|
||||
{
|
||||
public function __construct(
|
||||
public string $firstname,
|
||||
public string $lastname,
|
||||
public string $birthDate,
|
||||
public ?string $phone,
|
||||
public ?string $address,
|
||||
public string $postcode,
|
||||
public string $city,
|
||||
public bool $optIn,
|
||||
) {
|
||||
}
|
||||
}
|
||||
16
app/Actions/Crm/UpdateCustomerOutput.php
Normal file
16
app/Actions/Crm/UpdateCustomerOutput.php
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Crm;
|
||||
|
||||
use Domain\Models\ValueObjects\CrmCustomer;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
final readonly class UpdateCustomerOutput
|
||||
{
|
||||
public function __construct(
|
||||
public CrmCustomer | ValidationException | false $result
|
||||
) {
|
||||
}
|
||||
}
|
||||
54
app/Actions/Crm/UpdateCustomerPassword.php
Normal file
54
app/Actions/Crm/UpdateCustomerPassword.php
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Crm;
|
||||
|
||||
use App\Contracts\Action;
|
||||
use App\Contracts\ActionInput;
|
||||
use App\Services\CrmService;
|
||||
use Domain\Models\ValueObjects\CrmCustomer;
|
||||
use Infrastructure\Adapters\Services\AppCustomerAuthService;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
/**
|
||||
* @implements \App\Contracts\Action<\App\Actions\Crm\UpdateCustomerPasswordInput, \App\Actions\Crm\UpdateCustomerPasswordOutput>
|
||||
*/
|
||||
final class UpdateCustomerPassword implements Action
|
||||
{
|
||||
public function __construct(
|
||||
private readonly AppCustomerAuthService $authService,
|
||||
private readonly CrmService $crmService
|
||||
) {
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function __invoke(ActionInput $input): UpdateCustomerPasswordOutput
|
||||
{
|
||||
Assert::isInstanceOf($input, UpdateCustomerPasswordInput::class);
|
||||
|
||||
$hashcode = $this->authService->getCustomerAsMandatory()->hashcode;
|
||||
|
||||
$crmCustomer = $this->crmService->getCustomerFromHashCode(
|
||||
$hashcode
|
||||
);
|
||||
|
||||
if ($crmCustomer instanceof CrmCustomer === false) {
|
||||
return new UpdateCustomerPasswordOutput(false);
|
||||
}
|
||||
|
||||
try {
|
||||
$result = $this->crmService->changePassword(
|
||||
$crmCustomer,
|
||||
$input->currentPassword,
|
||||
$input->newPassword,
|
||||
);
|
||||
|
||||
return new UpdateCustomerPasswordOutput($result);
|
||||
} catch (\Throwable $e) {
|
||||
report($e);
|
||||
|
||||
return new UpdateCustomerPasswordOutput(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
22
app/Actions/Crm/UpdateCustomerPasswordInput.php
Normal file
22
app/Actions/Crm/UpdateCustomerPasswordInput.php
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Crm;
|
||||
|
||||
use App\Contracts\ActionInput;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
final readonly class UpdateCustomerPasswordInput implements ActionInput
|
||||
{
|
||||
public function __construct(
|
||||
#[\SensitiveParameter]
|
||||
public string $currentPassword,
|
||||
#[\SensitiveParameter]
|
||||
public string $newPassword,
|
||||
) {
|
||||
Assert::notEmpty($currentPassword);
|
||||
Assert::notEmpty($newPassword);
|
||||
Assert::notSame($currentPassword, $newPassword);
|
||||
}
|
||||
}
|
||||
16
app/Actions/Crm/UpdateCustomerPasswordOutput.php
Normal file
16
app/Actions/Crm/UpdateCustomerPasswordOutput.php
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Crm;
|
||||
|
||||
use Domain\Models\ValueObjects\CrmCustomer;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
final readonly class UpdateCustomerPasswordOutput
|
||||
{
|
||||
public function __construct(
|
||||
public CrmCustomer | ValidationException | false $result
|
||||
) {
|
||||
}
|
||||
}
|
||||
113
app/Actions/Customers/RegisterCustomer.php
Normal file
113
app/Actions/Customers/RegisterCustomer.php
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Customers;
|
||||
|
||||
use App\Contracts\Action;
|
||||
use App\Contracts\ActionInput;
|
||||
use App\Contracts\Repositories\EmployeeRepository;
|
||||
use App\Exceptions\Crm\CrmCustomerExistsException;
|
||||
use App\Exceptions\CrmServiceException;
|
||||
use App\Models\Customer;
|
||||
use App\Services\CrmService;
|
||||
use Domain\Enums\Country;
|
||||
use Domain\Models\ValueObjects\CrmCustomer;
|
||||
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Infrastructure\Adapters\Services\AppCustomerAuthService;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
use Shared\ValueObjects\CrmHashcode;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
/**
|
||||
* @implements \App\Contracts\Action<\App\Actions\Customers\RegisterCustomerInput, \App\Models\Customer>
|
||||
*/
|
||||
final class RegisterCustomer implements Action
|
||||
{
|
||||
public function __construct(
|
||||
private readonly AppCustomerAuthService $authService,
|
||||
private readonly CrmService $crmService,
|
||||
private readonly EmployeeRepository $employeeRepository,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \App\Actions\Customers\RegisterCustomerInput $input
|
||||
*
|
||||
* @throws ValidationException
|
||||
* @throws \UnexpectedValueException
|
||||
* @throws ModelNotFoundException
|
||||
*/
|
||||
#[\Override]
|
||||
public function __invoke(ActionInput $input): Customer
|
||||
{
|
||||
Assert::isInstanceOf($input, RegisterCustomerInput::class);
|
||||
|
||||
// * Persist and associate Customer + Employee
|
||||
$customer = new Customer([
|
||||
'email' => $input->email,
|
||||
]);
|
||||
// * Temp hashcode to satisfy constraint
|
||||
$customer->hashcode = new CrmHashcode(Uuid::uuid4());
|
||||
$customer->saveOrFail();
|
||||
|
||||
$employee = $this->employeeRepository->findByMatriculeAndCode(
|
||||
$input->matricule,
|
||||
$input->code
|
||||
);
|
||||
|
||||
if (! $employee) {
|
||||
throw new ModelNotFoundException('Employee not found');
|
||||
}
|
||||
|
||||
$employee->customer()->associate($customer);
|
||||
$employee->saveOrFail();
|
||||
|
||||
$crmCustomer = new CrmCustomer(
|
||||
hashcode: null,
|
||||
email: $input->email->value,
|
||||
password: $input->password,
|
||||
firstname: $input->firstname,
|
||||
lastname: $input->lastname,
|
||||
birthDate: $input->birthDate->format('Y-m-d'),
|
||||
phone: $input->phone?->value,
|
||||
address: $input->address,
|
||||
address2: null,
|
||||
city: $input->city,
|
||||
postcode: $input->postcode,
|
||||
country: Country::france,
|
||||
optIn: $input->optIn,
|
||||
status: false,
|
||||
lastConnection: null,
|
||||
);
|
||||
|
||||
try {
|
||||
$result = $this->crmService->persist($crmCustomer);
|
||||
|
||||
if (! $result->hashcode) {
|
||||
throw new \UnexpectedValueException(
|
||||
'CRM did not return a hashcode for the created customer'
|
||||
);
|
||||
}
|
||||
|
||||
$customer->hashcode = CrmHashcode::fromString($result->hashcode);
|
||||
$customer->saveOrFail();
|
||||
|
||||
$customer->sendEmailVerificationNotification();
|
||||
|
||||
$this->authService->logIn($customer);
|
||||
|
||||
return $customer;
|
||||
} catch (CrmCustomerExistsException) {
|
||||
throw ValidationException::withMessages([
|
||||
'error' => trans('An account is already associated with this email address'),
|
||||
]);
|
||||
} catch (CrmServiceException $e) {
|
||||
report($e);
|
||||
throw ValidationException::withMessages([
|
||||
'error' => trans('An error has occurred. Please try again later'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
43
app/Actions/Customers/RegisterCustomerInput.php
Normal file
43
app/Actions/Customers/RegisterCustomerInput.php
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Customers;
|
||||
|
||||
use App\Contracts\ActionInput;
|
||||
use Domain\Models\ValueObjects\Code;
|
||||
use Domain\Models\ValueObjects\Email;
|
||||
use Domain\Models\ValueObjects\Matricule;
|
||||
use Domain\Models\ValueObjects\PhoneNumber;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
final readonly class RegisterCustomerInput implements ActionInput
|
||||
{
|
||||
public function __construct(
|
||||
public Matricule $matricule,
|
||||
public Code $code,
|
||||
public string $firstname,
|
||||
public string $lastname,
|
||||
public Email $email,
|
||||
public string $password,
|
||||
public \DateTimeImmutable $birthDate,
|
||||
public ?PhoneNumber $phone,
|
||||
public ?string $address,
|
||||
public string $postcode,
|
||||
public string $city,
|
||||
public bool $optIn = false,
|
||||
) {
|
||||
Assert::stringNotEmpty(trim($this->firstname));
|
||||
Assert::maxLength($this->firstname, 255);
|
||||
Assert::stringNotEmpty(trim($this->lastname));
|
||||
Assert::maxLength($this->lastname, 255);
|
||||
Assert::stringNotEmpty(trim($this->password));
|
||||
Assert::stringNotEmpty(trim($this->postcode));
|
||||
Assert::maxLength($this->postcode, 20);
|
||||
Assert::stringNotEmpty(trim($this->city));
|
||||
Assert::maxLength($this->city, 255);
|
||||
if ($this->address !== null) {
|
||||
Assert::maxLength($this->address, 255);
|
||||
}
|
||||
}
|
||||
}
|
||||
56
app/Actions/Discounts/CreateDiscount.php
Normal file
56
app/Actions/Discounts/CreateDiscount.php
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Discounts;
|
||||
|
||||
use App\Contracts\Action;
|
||||
use App\Contracts\ActionInput;
|
||||
use App\Models\Discount;
|
||||
use App\Models\Product;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
/**
|
||||
* @implements \App\Contracts\Action<\App\Actions\Discounts\CreateDiscountInput, \App\Models\Discount>
|
||||
*/
|
||||
final class CreateDiscount implements Action
|
||||
{
|
||||
/**
|
||||
* @param \App\Actions\Discounts\CreateDiscountInput $input
|
||||
*/
|
||||
public function __invoke(ActionInput $input): Discount
|
||||
{
|
||||
Assert::isInstanceOf($input, CreateDiscountInput::class);
|
||||
|
||||
$discount = new Discount();
|
||||
$discount->type = $input->type;
|
||||
$discount->value = $input->value;
|
||||
$discount->is_old_price_displayed = $input->is_old_price_displayed;
|
||||
$discount->date_start = $input->period->start !== null ? Carbon::instance($input->period->start) : null;
|
||||
$discount->date_end = $input->period->end !== null ? Carbon::instance($input->period->end) : null;
|
||||
|
||||
$discount->saveOrFail();
|
||||
|
||||
$this->syncAssociatedProducts($discount, $input->associatedProductsIds);
|
||||
|
||||
return $discount;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string> $productIdList
|
||||
*/
|
||||
private function syncAssociatedProducts(Discount $discount, array $productIdList): void
|
||||
{
|
||||
Product::query()
|
||||
->where('discount_id', $discount->id)
|
||||
->whereNotIn('id', $productIdList)
|
||||
->update(['discount_id' => null]);
|
||||
|
||||
if (! empty($productIdList)) {
|
||||
Product::query()
|
||||
->whereIn('id', $productIdList)
|
||||
->update(['discount_id' => $discount->id]);
|
||||
}
|
||||
}
|
||||
}
|
||||
28
app/Actions/Discounts/CreateDiscountInput.php
Normal file
28
app/Actions/Discounts/CreateDiscountInput.php
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Discounts;
|
||||
|
||||
use App\Contracts\ActionInput;
|
||||
use Domain\Enums\DiscountType;
|
||||
use Domain\Models\ValueObjects\DatePeriod;
|
||||
use Domain\Models\ValueObjects\DiscountValue;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
final readonly class CreateDiscountInput implements ActionInput
|
||||
{
|
||||
/**
|
||||
* @param array<string> $associatedProductsIds
|
||||
*/
|
||||
public function __construct(
|
||||
public DiscountType $type,
|
||||
public DiscountValue $value,
|
||||
public bool $is_old_price_displayed,
|
||||
public DatePeriod $period = new DatePeriod(),
|
||||
public array $associatedProductsIds = [],
|
||||
) {
|
||||
Assert::same($this->type, $this->value->type, 'The discount value type must match the discount type.');
|
||||
Assert::allString($this->associatedProductsIds);
|
||||
}
|
||||
}
|
||||
25
app/Actions/Discounts/DeleteDiscount.php
Normal file
25
app/Actions/Discounts/DeleteDiscount.php
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Discounts;
|
||||
|
||||
use App\Contracts\Action;
|
||||
use App\Contracts\ActionInput;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
/**
|
||||
* @implements \App\Contracts\Action<\App\Actions\Discounts\DeleteDiscountInput, bool>
|
||||
*/
|
||||
final class DeleteDiscount implements Action
|
||||
{
|
||||
/**
|
||||
* @param \App\Actions\Discounts\DeleteDiscountInput $input
|
||||
*/
|
||||
public function __invoke(ActionInput $input): bool
|
||||
{
|
||||
Assert::isInstanceOf($input, DeleteDiscountInput::class);
|
||||
|
||||
return (bool) $input->discount->deleteOrFail();
|
||||
}
|
||||
}
|
||||
16
app/Actions/Discounts/DeleteDiscountInput.php
Normal file
16
app/Actions/Discounts/DeleteDiscountInput.php
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Discounts;
|
||||
|
||||
use App\Contracts\ActionInput;
|
||||
use App\Models\Discount;
|
||||
|
||||
final readonly class DeleteDiscountInput implements ActionInput
|
||||
{
|
||||
public function __construct(
|
||||
public Discount $discount,
|
||||
) {
|
||||
}
|
||||
}
|
||||
56
app/Actions/Discounts/UpdateDiscount.php
Normal file
56
app/Actions/Discounts/UpdateDiscount.php
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Discounts;
|
||||
|
||||
use App\Contracts\Action;
|
||||
use App\Contracts\ActionInput;
|
||||
use App\Models\Discount;
|
||||
use App\Models\Product;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
/**
|
||||
* @implements \App\Contracts\Action<\App\Actions\Discounts\UpdateDiscountInput, \App\Models\Discount>
|
||||
*/
|
||||
final class UpdateDiscount implements Action
|
||||
{
|
||||
/**
|
||||
* @param \App\Actions\Discounts\UpdateDiscountInput $input
|
||||
*/
|
||||
public function __invoke(ActionInput $input): Discount
|
||||
{
|
||||
Assert::isInstanceOf($input, UpdateDiscountInput::class);
|
||||
|
||||
$discount = $input->discount;
|
||||
$discount->type = $input->type;
|
||||
$discount->value = $input->value;
|
||||
$discount->is_old_price_displayed = $input->is_old_price_displayed;
|
||||
$discount->date_start = $input->period->start !== null ? Carbon::instance($input->period->start) : null;
|
||||
$discount->date_end = $input->period->end !== null ? Carbon::instance($input->period->end) : null;
|
||||
|
||||
$discount->saveOrFail();
|
||||
|
||||
$this->syncAssociatedProducts($discount, $input->associatedProductsIds);
|
||||
|
||||
return $discount;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string> $productIdList
|
||||
*/
|
||||
private function syncAssociatedProducts(Discount $discount, array $productIdList): void
|
||||
{
|
||||
Product::query()
|
||||
->where('discount_id', $discount->id)
|
||||
->whereNotIn('id', $productIdList)
|
||||
->update(['discount_id' => null]);
|
||||
|
||||
if (! empty($productIdList)) {
|
||||
Product::query()
|
||||
->whereIn('id', $productIdList)
|
||||
->update(['discount_id' => $discount->id]);
|
||||
}
|
||||
}
|
||||
}
|
||||
30
app/Actions/Discounts/UpdateDiscountInput.php
Normal file
30
app/Actions/Discounts/UpdateDiscountInput.php
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Discounts;
|
||||
|
||||
use App\Contracts\ActionInput;
|
||||
use App\Models\Discount;
|
||||
use Domain\Enums\DiscountType;
|
||||
use Domain\Models\ValueObjects\DatePeriod;
|
||||
use Domain\Models\ValueObjects\DiscountValue;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
final readonly class UpdateDiscountInput implements ActionInput
|
||||
{
|
||||
/**
|
||||
* @param array<string> $associatedProductsIds
|
||||
*/
|
||||
public function __construct(
|
||||
public Discount $discount,
|
||||
public DiscountType $type,
|
||||
public DiscountValue $value,
|
||||
public bool $is_old_price_displayed,
|
||||
public DatePeriod $period = new DatePeriod(),
|
||||
public array $associatedProductsIds = [],
|
||||
) {
|
||||
Assert::same($this->type, $this->value->type, 'The discount value type must match the discount type.');
|
||||
Assert::allString($this->associatedProductsIds);
|
||||
}
|
||||
}
|
||||
34
app/Actions/Employee/SynchronizeEmployeesWithArcadiaApi.php
Normal file
34
app/Actions/Employee/SynchronizeEmployeesWithArcadiaApi.php
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Employee;
|
||||
|
||||
use App\Contracts\Action;
|
||||
use App\Contracts\ActionInput;
|
||||
use App\Contracts\ActionInputEmpty;
|
||||
use App\Contracts\Repositories\EmployeeRepository;
|
||||
use App\Services\EmployeeSyncService;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
/**
|
||||
* @implements \App\Contracts\Action<\App\Contracts\ActionInputEmpty, void>
|
||||
*/
|
||||
final class SynchronizeEmployeesWithArcadiaApi implements Action
|
||||
{
|
||||
public function __construct(
|
||||
private readonly EmployeeRepository $employeeRepository,
|
||||
private readonly EmployeeSyncService $syncService
|
||||
) {
|
||||
}
|
||||
|
||||
/** @param \App\Contracts\ActionInputEmpty $input */
|
||||
public function __invoke(ActionInput $input): void
|
||||
{
|
||||
Assert::isAOf($input, ActionInputEmpty::class);
|
||||
|
||||
$wasLastSyncedAt = $this->employeeRepository->getMostRecentSyncDate();
|
||||
|
||||
$this->syncService->synchronize($wasLastSyncedAt);
|
||||
}
|
||||
}
|
||||
34
app/Actions/Faqs/CreateFaq.php
Normal file
34
app/Actions/Faqs/CreateFaq.php
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Faqs;
|
||||
|
||||
use App\Contracts\Action;
|
||||
use App\Contracts\ActionInput;
|
||||
use App\Models\Faq;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
/**
|
||||
* @implements \App\Contracts\Action<\App\Actions\Faqs\CreateFaqInput, \App\Models\Faq>
|
||||
*/
|
||||
final class CreateFaq implements Action
|
||||
{
|
||||
/**
|
||||
* @param \App\Actions\Faqs\CreateFaqInput $input
|
||||
*/
|
||||
#[\Override]
|
||||
public function __invoke(ActionInput $input): Faq
|
||||
{
|
||||
Assert::isInstanceOf($input, CreateFaqInput::class);
|
||||
|
||||
$faq = new Faq();
|
||||
$faq->fill([
|
||||
'question' => $input->question,
|
||||
'answer' => $input->answer,
|
||||
]);
|
||||
$faq->saveOrFail();
|
||||
|
||||
return $faq;
|
||||
}
|
||||
}
|
||||
22
app/Actions/Faqs/CreateFaqInput.php
Normal file
22
app/Actions/Faqs/CreateFaqInput.php
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Faqs;
|
||||
|
||||
use App\Contracts\ActionInput;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
final readonly class CreateFaqInput implements ActionInput
|
||||
{
|
||||
public function __construct(
|
||||
public string $question,
|
||||
public string $answer,
|
||||
) {
|
||||
Assert::stringNotEmpty(trim($this->question), 'Question cannot be empty.');
|
||||
Assert::minLength(trim($this->question), 4);
|
||||
Assert::maxLength($this->question, 255);
|
||||
Assert::stringNotEmpty(trim($this->answer), 'Answer cannot be empty.');
|
||||
Assert::minLength(trim($this->answer), 4);
|
||||
}
|
||||
}
|
||||
26
app/Actions/Faqs/DeleteFaq.php
Normal file
26
app/Actions/Faqs/DeleteFaq.php
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Faqs;
|
||||
|
||||
use App\Contracts\Action;
|
||||
use App\Contracts\ActionInput;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
/**
|
||||
* @implements \App\Contracts\Action<\App\Actions\Faqs\DeleteFaqInput, void>
|
||||
*/
|
||||
final class DeleteFaq implements Action
|
||||
{
|
||||
/**
|
||||
* @param \App\Actions\Faqs\DeleteFaqInput $input
|
||||
*/
|
||||
#[\Override]
|
||||
public function __invoke(ActionInput $input): void
|
||||
{
|
||||
Assert::isInstanceOf($input, DeleteFaqInput::class);
|
||||
|
||||
$input->faq->deleteOrFail();
|
||||
}
|
||||
}
|
||||
16
app/Actions/Faqs/DeleteFaqInput.php
Normal file
16
app/Actions/Faqs/DeleteFaqInput.php
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Faqs;
|
||||
|
||||
use App\Contracts\ActionInput;
|
||||
use App\Models\Faq;
|
||||
|
||||
final readonly class DeleteFaqInput implements ActionInput
|
||||
{
|
||||
public function __construct(
|
||||
public Faq $faq,
|
||||
) {
|
||||
}
|
||||
}
|
||||
34
app/Actions/Faqs/UpdateFaq.php
Normal file
34
app/Actions/Faqs/UpdateFaq.php
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Faqs;
|
||||
|
||||
use App\Contracts\Action;
|
||||
use App\Contracts\ActionInput;
|
||||
use App\Models\Faq;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
/**
|
||||
* @implements \App\Contracts\Action<\App\Actions\Faqs\UpdateFaqInput, \App\Models\Faq>
|
||||
*/
|
||||
final class UpdateFaq implements Action
|
||||
{
|
||||
/**
|
||||
* @param \App\Actions\Faqs\UpdateFaqInput $input
|
||||
*/
|
||||
#[\Override]
|
||||
public function __invoke(ActionInput $input): Faq
|
||||
{
|
||||
Assert::isInstanceOf($input, UpdateFaqInput::class);
|
||||
|
||||
$faq = $input->faq;
|
||||
$faq->fill([
|
||||
'question' => $input->question,
|
||||
'answer' => $input->answer,
|
||||
]);
|
||||
$faq->saveOrFail();
|
||||
|
||||
return $faq;
|
||||
}
|
||||
}
|
||||
24
app/Actions/Faqs/UpdateFaqInput.php
Normal file
24
app/Actions/Faqs/UpdateFaqInput.php
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Faqs;
|
||||
|
||||
use App\Contracts\ActionInput;
|
||||
use App\Models\Faq;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
final readonly class UpdateFaqInput implements ActionInput
|
||||
{
|
||||
public function __construct(
|
||||
public Faq $faq,
|
||||
public string $question,
|
||||
public string $answer,
|
||||
) {
|
||||
Assert::stringNotEmpty(trim($this->question), 'Question cannot be empty.');
|
||||
Assert::minLength(trim($this->question), 4);
|
||||
Assert::maxLength($this->question, 255);
|
||||
Assert::stringNotEmpty(trim($this->answer), 'Answer cannot be empty.');
|
||||
Assert::minLength(trim($this->answer), 4);
|
||||
}
|
||||
}
|
||||
42
app/Actions/Orders/ApplyPickTimeSlot.php
Normal file
42
app/Actions/Orders/ApplyPickTimeSlot.php
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Orders;
|
||||
|
||||
use App\Contracts\Action;
|
||||
use App\Contracts\ActionInput;
|
||||
use Domain\Models\ValueObjects\PickupTimeSlot;
|
||||
use Domain\Services\OrderTunnelService;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
/**
|
||||
* @implements \App\Contracts\Action<\App\Actions\Orders\ApplyPickTimeSlotInput, ?\Domain\Models\ValueObjects\PickupTimeSlot>
|
||||
*/
|
||||
final class ApplyPickTimeSlot implements Action
|
||||
{
|
||||
public function __construct(
|
||||
private readonly OrderTunnelService $orderTunnelService,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \App\Actions\Orders\ApplyPickTimeSlotInput $input
|
||||
*/
|
||||
#[\Override]
|
||||
public function __invoke(ActionInput $input): ?PickupTimeSlot
|
||||
{
|
||||
Assert::isInstanceOf($input, ApplyPickTimeSlotInput::class);
|
||||
|
||||
$builder = $this->orderTunnelService->getBuilderFromBasketOrFail();
|
||||
$shop = $this->orderTunnelService->getShopForCustomer($input->shopperId);
|
||||
$timeSlots = $this->orderTunnelService->getAvailableTimeSlots($shop);
|
||||
|
||||
return $this->orderTunnelService->applyTimeSlot(
|
||||
$builder,
|
||||
$shop,
|
||||
$input->timeslot,
|
||||
$timeSlots
|
||||
);
|
||||
}
|
||||
}
|
||||
18
app/Actions/Orders/ApplyPickTimeSlotInput.php
Normal file
18
app/Actions/Orders/ApplyPickTimeSlotInput.php
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Orders;
|
||||
|
||||
use App\Contracts\ActionInput;
|
||||
use Carbon\CarbonInterface;
|
||||
use Domain\Models\ValueObjects\Entities\ShopperId;
|
||||
|
||||
final readonly class ApplyPickTimeSlotInput implements ActionInput
|
||||
{
|
||||
public function __construct(
|
||||
public ShopperId $shopperId,
|
||||
public CarbonInterface $timeslot,
|
||||
) {
|
||||
}
|
||||
}
|
||||
32
app/Actions/Orders/CancelOrder.php
Normal file
32
app/Actions/Orders/CancelOrder.php
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Orders;
|
||||
|
||||
use App\Contracts\Action;
|
||||
use App\Contracts\ActionInput;
|
||||
use Domain\Services\OrderService as DomainOrderService;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
/**
|
||||
* @implements \App\Contracts\Action<\App\Actions\Orders\CancelOrderInput, void>
|
||||
*/
|
||||
final class CancelOrder implements Action
|
||||
{
|
||||
public function __construct(
|
||||
private readonly DomainOrderService $domainOrderService,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \App\Actions\Orders\CancelOrderInput $input
|
||||
*/
|
||||
#[\Override]
|
||||
public function __invoke(ActionInput $input): void
|
||||
{
|
||||
Assert::isInstanceOf($input, CancelOrderInput::class);
|
||||
|
||||
$this->domainOrderService->cancelOrder($input->order->getId());
|
||||
}
|
||||
}
|
||||
18
app/Actions/Orders/CancelOrderInput.php
Normal file
18
app/Actions/Orders/CancelOrderInput.php
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Orders;
|
||||
|
||||
use App\Contracts\ActionInput;
|
||||
use App\Models\Order;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
final readonly class CancelOrderInput implements ActionInput
|
||||
{
|
||||
public function __construct(
|
||||
public Order $order,
|
||||
) {
|
||||
Assert::stringNotEmpty(trim($this->order->id));
|
||||
}
|
||||
}
|
||||
36
app/Actions/Orders/FinalizeOrder.php
Normal file
36
app/Actions/Orders/FinalizeOrder.php
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Orders;
|
||||
|
||||
use App\Contracts\Action;
|
||||
use App\Contracts\ActionInput;
|
||||
use Domain\Models\ValueObjects\OrderReference;
|
||||
use Domain\Services\OrderTunnelService;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
/**
|
||||
* @implements \App\Contracts\Action<\App\Actions\Orders\FinalizeOrderInput, \Domain\Models\ValueObjects\OrderReference>
|
||||
*/
|
||||
final class FinalizeOrder implements Action
|
||||
{
|
||||
public function __construct(
|
||||
private readonly OrderTunnelService $orderTunnelService,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \App\Actions\Orders\FinalizeOrderInput $input
|
||||
*/
|
||||
#[\Override]
|
||||
public function __invoke(ActionInput $input): OrderReference
|
||||
{
|
||||
Assert::isInstanceOf($input, FinalizeOrderInput::class);
|
||||
|
||||
$builder = $this->orderTunnelService->getBuilderCompletedOrFail();
|
||||
$order = $this->orderTunnelService->finalizeOrder($builder);
|
||||
|
||||
return $order->reference;
|
||||
}
|
||||
}
|
||||
16
app/Actions/Orders/FinalizeOrderInput.php
Normal file
16
app/Actions/Orders/FinalizeOrderInput.php
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Orders;
|
||||
|
||||
use App\Contracts\ActionInput;
|
||||
use Domain\Models\ValueObjects\Entities\ShopperId;
|
||||
|
||||
final readonly class FinalizeOrderInput implements ActionInput
|
||||
{
|
||||
public function __construct(
|
||||
public ?ShopperId $shopperId = null,
|
||||
) {
|
||||
}
|
||||
}
|
||||
35
app/Actions/Orders/InitializeOrderTunnel.php
Normal file
35
app/Actions/Orders/InitializeOrderTunnel.php
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Orders;
|
||||
|
||||
use App\Contracts\Action;
|
||||
use App\Contracts\ActionInput;
|
||||
use Domain\Services\OrderTunnelService;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
/**
|
||||
* @implements \App\Contracts\Action<\App\Actions\Orders\InitializeOrderTunnelInput, void>
|
||||
*/
|
||||
final class InitializeOrderTunnel implements Action
|
||||
{
|
||||
public function __construct(
|
||||
private readonly OrderTunnelService $orderTunnelService,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \App\Actions\Orders\InitializeOrderTunnelInput $input
|
||||
*/
|
||||
#[\Override]
|
||||
public function __invoke(ActionInput $input): void
|
||||
{
|
||||
Assert::isInstanceOf($input, InitializeOrderTunnelInput::class);
|
||||
|
||||
$this->orderTunnelService->initializeFromBasket(
|
||||
$input->basket,
|
||||
$input->shopperId
|
||||
);
|
||||
}
|
||||
}
|
||||
18
app/Actions/Orders/InitializeOrderTunnelInput.php
Normal file
18
app/Actions/Orders/InitializeOrderTunnelInput.php
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Orders;
|
||||
|
||||
use App\Contracts\ActionInput;
|
||||
use Domain\Models\Entities\Basket;
|
||||
use Domain\Models\ValueObjects\Entities\ShopperId;
|
||||
|
||||
final readonly class InitializeOrderTunnelInput implements ActionInput
|
||||
{
|
||||
public function __construct(
|
||||
public Basket $basket,
|
||||
public ShopperId $shopperId,
|
||||
) {
|
||||
}
|
||||
}
|
||||
36
app/Actions/Pages/UpdatePage.php
Normal file
36
app/Actions/Pages/UpdatePage.php
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Pages;
|
||||
|
||||
use App\Contracts\Action;
|
||||
use App\Contracts\ActionInput;
|
||||
use App\Models\Page;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
/**
|
||||
* @implements \App\Contracts\Action<\App\Actions\Pages\UpdatePageInput, \App\Models\Page>
|
||||
*/
|
||||
final class UpdatePage implements Action
|
||||
{
|
||||
/**
|
||||
* @param \App\Actions\Pages\UpdatePageInput $input
|
||||
*/
|
||||
#[\Override]
|
||||
public function __invoke(ActionInput $input): Page
|
||||
{
|
||||
Assert::isInstanceOf($input, UpdatePageInput::class);
|
||||
|
||||
$page = $input->page;
|
||||
$page->fill([
|
||||
'seo_title' => $input->seo->title,
|
||||
'seo_description' => $input->seo->description,
|
||||
'title' => $input->title,
|
||||
'content' => $input->content,
|
||||
]);
|
||||
$page->saveOrFail();
|
||||
|
||||
return $page;
|
||||
}
|
||||
}
|
||||
27
app/Actions/Pages/UpdatePageInput.php
Normal file
27
app/Actions/Pages/UpdatePageInput.php
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Pages;
|
||||
|
||||
use App\Contracts\ActionInput;
|
||||
use App\Models\Page;
|
||||
use Domain\Models\ValueObjects\SeoMetadata;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
final readonly class UpdatePageInput implements ActionInput
|
||||
{
|
||||
public function __construct(
|
||||
public Page $page,
|
||||
public SeoMetadata $seo,
|
||||
public ?string $title,
|
||||
public string $content,
|
||||
) {
|
||||
Assert::stringNotEmpty(trim($this->content));
|
||||
|
||||
if ($this->title !== null) {
|
||||
Assert::stringNotEmpty(trim($this->title));
|
||||
Assert::maxLength($this->title, 255);
|
||||
}
|
||||
}
|
||||
}
|
||||
37
app/Actions/PickupExceptions/CreatePickupException.php
Normal file
37
app/Actions/PickupExceptions/CreatePickupException.php
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\PickupExceptions;
|
||||
|
||||
use App\Contracts\Action;
|
||||
use App\Contracts\ActionInput;
|
||||
use App\Models\PickupException;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
/**
|
||||
* @implements \App\Contracts\Action<\App\Actions\PickupExceptions\CreatePickupExceptionInput, \App\Models\PickupException>
|
||||
*/
|
||||
final class CreatePickupException implements Action
|
||||
{
|
||||
/**
|
||||
* @param \App\Actions\PickupExceptions\CreatePickupExceptionInput $input
|
||||
*/
|
||||
#[\Override]
|
||||
public function __invoke(ActionInput $input): PickupException
|
||||
{
|
||||
Assert::isInstanceOf($input, CreatePickupExceptionInput::class);
|
||||
|
||||
$pickupException = new PickupException();
|
||||
$pickupException->fill([
|
||||
'date' => $input->date,
|
||||
'label' => $input->label,
|
||||
'repeatable' => $input->repeatable,
|
||||
'repeat_frequency' => $input->repeat_frequency,
|
||||
]);
|
||||
$pickupException->setAsManuallySaved();
|
||||
$pickupException->saveOrFail();
|
||||
|
||||
return $pickupException;
|
||||
}
|
||||
}
|
||||
23
app/Actions/PickupExceptions/CreatePickupExceptionInput.php
Normal file
23
app/Actions/PickupExceptions/CreatePickupExceptionInput.php
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\PickupExceptions;
|
||||
|
||||
use App\Contracts\ActionInput;
|
||||
use Domain\Enums\RepeatFrequency;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
final readonly class CreatePickupExceptionInput implements ActionInput
|
||||
{
|
||||
public function __construct(
|
||||
public string $date,
|
||||
public string $label,
|
||||
public bool $repeatable,
|
||||
public RepeatFrequency $repeat_frequency,
|
||||
) {
|
||||
Assert::regex($this->date, '/^\d{4}-\d{2}-\d{2}$/', 'Date must be in Y-m-d format.');
|
||||
Assert::stringNotEmpty(trim($this->label), 'Label cannot be empty.');
|
||||
Assert::maxLength(trim($this->label), 255, 'Label cannot exceed 255 characters.');
|
||||
}
|
||||
}
|
||||
26
app/Actions/PickupExceptions/DeletePickupException.php
Normal file
26
app/Actions/PickupExceptions/DeletePickupException.php
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\PickupExceptions;
|
||||
|
||||
use App\Contracts\Action;
|
||||
use App\Contracts\ActionInput;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
/**
|
||||
* @implements \App\Contracts\Action<\App\Actions\PickupExceptions\DeletePickupExceptionInput, bool>
|
||||
*/
|
||||
final class DeletePickupException implements Action
|
||||
{
|
||||
/**
|
||||
* @param \App\Actions\PickupExceptions\DeletePickupExceptionInput $input
|
||||
*/
|
||||
#[\Override]
|
||||
public function __invoke(ActionInput $input): bool
|
||||
{
|
||||
Assert::isInstanceOf($input, DeletePickupExceptionInput::class);
|
||||
|
||||
return (bool) $input->pickupException->deleteOrFail();
|
||||
}
|
||||
}
|
||||
16
app/Actions/PickupExceptions/DeletePickupExceptionInput.php
Normal file
16
app/Actions/PickupExceptions/DeletePickupExceptionInput.php
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\PickupExceptions;
|
||||
|
||||
use App\Contracts\ActionInput;
|
||||
use App\Models\PickupException;
|
||||
|
||||
final readonly class DeletePickupExceptionInput implements ActionInput
|
||||
{
|
||||
public function __construct(
|
||||
public PickupException $pickupException,
|
||||
) {
|
||||
}
|
||||
}
|
||||
36
app/Actions/PickupExceptions/UpdatePickupException.php
Normal file
36
app/Actions/PickupExceptions/UpdatePickupException.php
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\PickupExceptions;
|
||||
|
||||
use App\Contracts\Action;
|
||||
use App\Contracts\ActionInput;
|
||||
use App\Models\PickupException;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
/**
|
||||
* @implements \App\Contracts\Action<\App\Actions\PickupExceptions\UpdatePickupExceptionInput, \App\Models\PickupException>
|
||||
*/
|
||||
final class UpdatePickupException implements Action
|
||||
{
|
||||
/**
|
||||
* @param \App\Actions\PickupExceptions\UpdatePickupExceptionInput $input
|
||||
*/
|
||||
#[\Override]
|
||||
public function __invoke(ActionInput $input): PickupException
|
||||
{
|
||||
Assert::isInstanceOf($input, UpdatePickupExceptionInput::class);
|
||||
|
||||
$input->pickupException->fill([
|
||||
'date' => $input->date,
|
||||
'label' => $input->label,
|
||||
'repeatable' => $input->repeatable,
|
||||
'repeat_frequency' => $input->repeat_frequency,
|
||||
]);
|
||||
$input->pickupException->setAsManuallySaved();
|
||||
$input->pickupException->saveOrFail();
|
||||
|
||||
return $input->pickupException;
|
||||
}
|
||||
}
|
||||
25
app/Actions/PickupExceptions/UpdatePickupExceptionInput.php
Normal file
25
app/Actions/PickupExceptions/UpdatePickupExceptionInput.php
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\PickupExceptions;
|
||||
|
||||
use App\Contracts\ActionInput;
|
||||
use App\Models\PickupException;
|
||||
use Domain\Enums\RepeatFrequency;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
final readonly class UpdatePickupExceptionInput implements ActionInput
|
||||
{
|
||||
public function __construct(
|
||||
public PickupException $pickupException,
|
||||
public string $date,
|
||||
public string $label,
|
||||
public bool $repeatable,
|
||||
public RepeatFrequency $repeat_frequency,
|
||||
) {
|
||||
Assert::regex($this->date, '/^\d{4}-\d{2}-\d{2}$/', 'Date must be in Y-m-d format.');
|
||||
Assert::stringNotEmpty(trim($this->label), 'Label cannot be empty.');
|
||||
Assert::maxLength(trim($this->label), 255, 'Label cannot exceed 255 characters.');
|
||||
}
|
||||
}
|
||||
59
app/Actions/ProductIncos/AssociateProductInco.php
Normal file
59
app/Actions/ProductIncos/AssociateProductInco.php
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\ProductIncos;
|
||||
|
||||
use App\Contracts\Action;
|
||||
use App\Contracts\ActionInput;
|
||||
use App\Models\ProductInco;
|
||||
use App\Services\ArcadiaIncoService;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
/**
|
||||
* @implements \App\Contracts\Action<\App\Actions\ProductIncos\AssociateProductIncoInput, \App\Models\ProductInco>
|
||||
*/
|
||||
final class AssociateProductInco implements Action
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ArcadiaIncoService $arcadiaIncoService,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \App\Actions\ProductIncos\AssociateProductIncoInput $input
|
||||
*/
|
||||
#[\Override]
|
||||
public function __invoke(ActionInput $input): ProductInco
|
||||
{
|
||||
Assert::isInstanceOf($input, AssociateProductIncoInput::class);
|
||||
|
||||
$incoData = $this->arcadiaIncoService->fetchInco($input->no_art);
|
||||
|
||||
if ($incoData === null) {
|
||||
throw new \RuntimeException(sprintf('INCO sheet #%d was not found on Arcadia API.', $input->no_art));
|
||||
}
|
||||
|
||||
// * Remove existing product_inco for this product if present
|
||||
$existing = ProductInco::query()->where('product_id', $input->product->id)->first();
|
||||
if ($existing !== null) {
|
||||
$existing->delete();
|
||||
}
|
||||
|
||||
$inco = new ProductInco();
|
||||
$inco->product_id = (string) $input->product->id;
|
||||
$inco->no_art = $incoData->noArt;
|
||||
$inco->ingredients = $incoData->ingredients;
|
||||
$inco->preparation = $incoData->preparation;
|
||||
$inco->servings = $incoData->servings;
|
||||
$inco->allergens = $incoData->allergens;
|
||||
$inco->selling_argument = $incoData->sellingArgument;
|
||||
$inco->nutriscore = $incoData->nutriscore;
|
||||
$inco->nutriscore_args = $incoData->nutriscoreArgs;
|
||||
$inco->nutrition = $incoData->nutrition;
|
||||
|
||||
$inco->saveOrFail();
|
||||
|
||||
return $inco;
|
||||
}
|
||||
}
|
||||
19
app/Actions/ProductIncos/AssociateProductIncoInput.php
Normal file
19
app/Actions/ProductIncos/AssociateProductIncoInput.php
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\ProductIncos;
|
||||
|
||||
use App\Contracts\ActionInput;
|
||||
use App\Models\Product;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
final readonly class AssociateProductIncoInput implements ActionInput
|
||||
{
|
||||
public function __construct(
|
||||
public Product $product,
|
||||
public int $no_art,
|
||||
) {
|
||||
Assert::greaterThanEq($this->no_art, 1, 'The INCO article number must be greater than or equal to 1.');
|
||||
}
|
||||
}
|
||||
54
app/Actions/ProductIncos/SyncProductInco.php
Normal file
54
app/Actions/ProductIncos/SyncProductInco.php
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\ProductIncos;
|
||||
|
||||
use App\Contracts\Action;
|
||||
use App\Contracts\ActionInput;
|
||||
use App\Models\ProductInco;
|
||||
use App\Services\ArcadiaIncoService;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
/**
|
||||
* @implements \App\Contracts\Action<\App\Actions\ProductIncos\SyncProductIncoInput, \App\Models\ProductInco>
|
||||
*/
|
||||
final class SyncProductInco implements Action
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ArcadiaIncoService $arcadiaIncoService,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \App\Actions\ProductIncos\SyncProductIncoInput $input
|
||||
*/
|
||||
#[\Override]
|
||||
public function __invoke(ActionInput $input): ProductInco
|
||||
{
|
||||
Assert::isInstanceOf($input, SyncProductIncoInput::class);
|
||||
|
||||
$inco = $input->product->inco;
|
||||
if ($inco === null) {
|
||||
throw new \RuntimeException('Product does not have an associated INCO sheet to sync.');
|
||||
}
|
||||
|
||||
$incoData = $this->arcadiaIncoService->fetchInco($inco->no_art);
|
||||
if ($incoData === null) {
|
||||
throw new \RuntimeException(sprintf('INCO sheet #%d was not found on Arcadia API.', $inco->no_art));
|
||||
}
|
||||
|
||||
$inco->ingredients = $incoData->ingredients;
|
||||
$inco->preparation = $incoData->preparation;
|
||||
$inco->servings = $incoData->servings;
|
||||
$inco->allergens = $incoData->allergens;
|
||||
$inco->selling_argument = $incoData->sellingArgument;
|
||||
$inco->nutriscore = $incoData->nutriscore;
|
||||
$inco->nutriscore_args = $incoData->nutriscoreArgs;
|
||||
$inco->nutrition = $incoData->nutrition;
|
||||
|
||||
$inco->saveOrFail();
|
||||
|
||||
return $inco;
|
||||
}
|
||||
}
|
||||
16
app/Actions/ProductIncos/SyncProductIncoInput.php
Normal file
16
app/Actions/ProductIncos/SyncProductIncoInput.php
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\ProductIncos;
|
||||
|
||||
use App\Contracts\ActionInput;
|
||||
use App\Models\Product;
|
||||
|
||||
final readonly class SyncProductIncoInput implements ActionInput
|
||||
{
|
||||
public function __construct(
|
||||
public Product $product,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\ProductIncos;
|
||||
|
||||
use App\Contracts\Action;
|
||||
use App\Contracts\ActionInput;
|
||||
use App\Contracts\ActionInputEmpty;
|
||||
use App\Models\ProductInco;
|
||||
use App\Services\ArcadiaIncoService;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
/**
|
||||
* @implements \App\Contracts\Action<\App\Contracts\ActionInputEmpty, int>
|
||||
*/
|
||||
final class SynchronizeProductIncosWithArcadiaApi implements Action
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ArcadiaIncoService $arcadiaIncoService,
|
||||
private readonly LoggerInterface $logger,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \App\Contracts\ActionInputEmpty $input
|
||||
*/
|
||||
#[\Override]
|
||||
public function __invoke(ActionInput $input): int
|
||||
{
|
||||
Assert::isInstanceOf($input, ActionInputEmpty::class);
|
||||
|
||||
$this->logger->info('Starting synchronization of all ProductIncos with Arcadia API.');
|
||||
$updatedCount = 0;
|
||||
|
||||
/** @var \Illuminate\Database\Eloquent\Collection<int, ProductInco> $incos */
|
||||
$incos = ProductInco::query()->get();
|
||||
|
||||
foreach ($incos as $inco) {
|
||||
$incoData = $this->arcadiaIncoService->fetchInco($inco->no_art);
|
||||
|
||||
if ($incoData === null) {
|
||||
$this->logger->warning("Could not sync ProductInco #{$inco->id} (no_art: {$inco->no_art}): not found on Arcadia.");
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$inco->ingredients = $incoData->ingredients;
|
||||
$inco->preparation = $incoData->preparation;
|
||||
$inco->servings = $incoData->servings;
|
||||
$inco->allergens = $incoData->allergens;
|
||||
$inco->selling_argument = $incoData->sellingArgument;
|
||||
$inco->nutriscore = $incoData->nutriscore;
|
||||
$inco->nutriscore_args = $incoData->nutriscoreArgs;
|
||||
$inco->nutrition = $incoData->nutrition;
|
||||
|
||||
$inco->saveOrFail();
|
||||
$updatedCount++;
|
||||
}
|
||||
|
||||
$this->logger->info("Completed synchronization of ProductIncos: {$updatedCount} updated.");
|
||||
|
||||
return $updatedCount;
|
||||
}
|
||||
}
|
||||
109
app/Actions/Products/CreateProduct.php
Normal file
109
app/Actions/Products/CreateProduct.php
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Products;
|
||||
|
||||
use App\Contracts\Action;
|
||||
use App\Contracts\ActionInput;
|
||||
use App\Models\Product;
|
||||
use App\Models\Tag;
|
||||
use Illuminate\Support\Facades\Date;
|
||||
use Illuminate\Support\Str;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
/**
|
||||
* @implements \App\Contracts\Action<\App\Actions\Products\CreateProductInput, \App\Models\Product>
|
||||
*/
|
||||
final class CreateProduct implements Action
|
||||
{
|
||||
/**
|
||||
* @param \App\Actions\Products\CreateProductInput $input
|
||||
*/
|
||||
#[\Override]
|
||||
public function __invoke(ActionInput $input): Product
|
||||
{
|
||||
Assert::isInstanceOf($input, CreateProductInput::class);
|
||||
|
||||
$product = new Product();
|
||||
$product->name = $input->name;
|
||||
$product->short_description = $input->shortDescription;
|
||||
$product->description = $input->description;
|
||||
$product->price = $input->price;
|
||||
$product->invoicing_unit = $input->invoicingUnit;
|
||||
$product->stock = $input->stock;
|
||||
$product->range_id = $input->rangeId;
|
||||
$product->sell_unit = $input->sellUnit;
|
||||
$product->order_unit = $input->orderUnit;
|
||||
$product->tax_rate = $input->taxRate;
|
||||
$product->brand = $input->brand;
|
||||
$product->weight = $input->weight;
|
||||
$product->weight_variable = $input->weightVariable;
|
||||
$product->orderable_start = $input->orderableStart !== null ? Date::instance($input->orderableStart) : null;
|
||||
$product->orderable_end = $input->orderableEnd !== null ? Date::instance($input->orderableEnd) : null;
|
||||
$product->star = $input->star;
|
||||
$product->status = $input->status;
|
||||
|
||||
if ($input->picture !== null) {
|
||||
$product->setAttribute('picture', $input->picture);
|
||||
}
|
||||
$product->picture_alt = (string) $input->pictureAlt;
|
||||
$product->picture_title = (string) $input->pictureTitle;
|
||||
|
||||
if ($input->pictureLarge !== null) {
|
||||
$product->setAttribute('picture_large', $input->pictureLarge);
|
||||
}
|
||||
$product->picture_large_alt = (string) $input->pictureLargeAlt;
|
||||
$product->picture_large_title = (string) $input->pictureLargeTitle;
|
||||
|
||||
$product->saveOrFail();
|
||||
|
||||
$this->syncTags($input->tags, $product);
|
||||
$this->syncAssociatedProducts($input->associatedProductsIds, $product);
|
||||
|
||||
return $product;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<array{id?: int|string, label: string}> $tags
|
||||
*/
|
||||
private function syncTags(array $tags, Product $product): void
|
||||
{
|
||||
$tagIdList = [];
|
||||
foreach ($tags as $tag) {
|
||||
$tagIdList[] = empty($tag['id'])
|
||||
? $this->storeTag($tag)
|
||||
: (int) $tag['id'];
|
||||
}
|
||||
|
||||
$product->tags()->sync($tagIdList);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array{id?: int|string, label: string} $tag
|
||||
*/
|
||||
private function storeTag(array $tag): int
|
||||
{
|
||||
$slug = Str::slug($tag['label']);
|
||||
$tagModel = Tag::query()
|
||||
->where('slug', $slug)
|
||||
->firstOrNew();
|
||||
$tagModel->setAttribute('label', $tag['label']);
|
||||
$tagModel->saveOrFail();
|
||||
|
||||
return (int) $tagModel->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<string> $productIdList
|
||||
*/
|
||||
private function syncAssociatedProducts(array $productIdList, Product $product): void
|
||||
{
|
||||
$syncData = [];
|
||||
foreach ($productIdList as $index => $id) {
|
||||
$syncData[$id] = ['sort_order' => $index + 1];
|
||||
}
|
||||
|
||||
$product->products()->sync($syncData);
|
||||
}
|
||||
}
|
||||
55
app/Actions/Products/CreateProductInput.php
Normal file
55
app/Actions/Products/CreateProductInput.php
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Products;
|
||||
|
||||
use App\Contracts\ActionInput;
|
||||
use Carbon\CarbonInterface;
|
||||
use Domain\Enums\ProductBrand;
|
||||
use Domain\Enums\SellUnit;
|
||||
use Domain\Enums\TaxRate;
|
||||
use Domain\Enums\Units\InvoicingUnit;
|
||||
use Domain\Enums\Units\OrderUnit;
|
||||
use Money\Money;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
final readonly class CreateProductInput implements ActionInput
|
||||
{
|
||||
/**
|
||||
* @param array<array{id?: int|string, label: string}> $tags
|
||||
* @param list<string> $associatedProductsIds
|
||||
*/
|
||||
public function __construct(
|
||||
public string $name,
|
||||
public string $shortDescription,
|
||||
public string $description,
|
||||
public Money $price,
|
||||
public InvoicingUnit $invoicingUnit,
|
||||
public int $stock,
|
||||
public int $rangeId,
|
||||
public SellUnit $sellUnit,
|
||||
public OrderUnit $orderUnit,
|
||||
public TaxRate $taxRate,
|
||||
public ProductBrand $brand,
|
||||
public ?int $weight = null,
|
||||
public bool $weightVariable = false,
|
||||
public ?CarbonInterface $orderableStart = null,
|
||||
public ?CarbonInterface $orderableEnd = null,
|
||||
public bool $star = false,
|
||||
public bool $status = false,
|
||||
public mixed $picture = null,
|
||||
public ?string $pictureAlt = null,
|
||||
public ?string $pictureTitle = null,
|
||||
public mixed $pictureLarge = null,
|
||||
public ?string $pictureLargeAlt = null,
|
||||
public ?string $pictureLargeTitle = null,
|
||||
public array $tags = [],
|
||||
public array $associatedProductsIds = [],
|
||||
) {
|
||||
Assert::stringNotEmpty(trim($this->name));
|
||||
Assert::maxLength($this->name, 255);
|
||||
Assert::greaterThanEq($this->stock, 0);
|
||||
Assert::greaterThanEq($this->rangeId, 1);
|
||||
}
|
||||
}
|
||||
26
app/Actions/Products/DeleteProduct.php
Normal file
26
app/Actions/Products/DeleteProduct.php
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Products;
|
||||
|
||||
use App\Contracts\Action;
|
||||
use App\Contracts\ActionInput;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
/**
|
||||
* @implements \App\Contracts\Action<\App\Actions\Products\DeleteProductInput, bool>
|
||||
*/
|
||||
final class DeleteProduct implements Action
|
||||
{
|
||||
/**
|
||||
* @param \App\Actions\Products\DeleteProductInput $input
|
||||
*/
|
||||
#[\Override]
|
||||
public function __invoke(ActionInput $input): bool
|
||||
{
|
||||
Assert::isInstanceOf($input, DeleteProductInput::class);
|
||||
|
||||
return (bool) $input->product->deleteOrFail();
|
||||
}
|
||||
}
|
||||
16
app/Actions/Products/DeleteProductInput.php
Normal file
16
app/Actions/Products/DeleteProductInput.php
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Products;
|
||||
|
||||
use App\Contracts\ActionInput;
|
||||
use App\Models\Product;
|
||||
|
||||
final readonly class DeleteProductInput implements ActionInput
|
||||
{
|
||||
public function __construct(
|
||||
public Product $product,
|
||||
) {
|
||||
}
|
||||
}
|
||||
109
app/Actions/Products/UpdateProduct.php
Normal file
109
app/Actions/Products/UpdateProduct.php
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Products;
|
||||
|
||||
use App\Contracts\Action;
|
||||
use App\Contracts\ActionInput;
|
||||
use App\Models\Product;
|
||||
use App\Models\Tag;
|
||||
use Illuminate\Support\Facades\Date;
|
||||
use Illuminate\Support\Str;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
/**
|
||||
* @implements \App\Contracts\Action<\App\Actions\Products\UpdateProductInput, \App\Models\Product>
|
||||
*/
|
||||
final class UpdateProduct implements Action
|
||||
{
|
||||
/**
|
||||
* @param \App\Actions\Products\UpdateProductInput $input
|
||||
*/
|
||||
#[\Override]
|
||||
public function __invoke(ActionInput $input): Product
|
||||
{
|
||||
Assert::isInstanceOf($input, UpdateProductInput::class);
|
||||
|
||||
$product = $input->product;
|
||||
$product->name = $input->name;
|
||||
$product->short_description = $input->shortDescription;
|
||||
$product->description = $input->description;
|
||||
$product->price = $input->price;
|
||||
$product->invoicing_unit = $input->invoicingUnit;
|
||||
$product->stock = $input->stock;
|
||||
$product->range_id = $input->rangeId;
|
||||
$product->sell_unit = $input->sellUnit;
|
||||
$product->order_unit = $input->orderUnit;
|
||||
$product->tax_rate = $input->taxRate;
|
||||
$product->brand = $input->brand;
|
||||
$product->weight = $input->weight;
|
||||
$product->weight_variable = $input->weightVariable;
|
||||
$product->orderable_start = $input->orderableStart !== null ? Date::instance($input->orderableStart) : null;
|
||||
$product->orderable_end = $input->orderableEnd !== null ? Date::instance($input->orderableEnd) : null;
|
||||
$product->star = $input->star;
|
||||
$product->status = $input->status;
|
||||
|
||||
if ($input->picture !== null) {
|
||||
$product->setAttribute('picture', $input->picture);
|
||||
}
|
||||
$product->picture_alt = (string) $input->pictureAlt;
|
||||
$product->picture_title = (string) $input->pictureTitle;
|
||||
|
||||
if ($input->pictureLarge !== null) {
|
||||
$product->setAttribute('picture_large', $input->pictureLarge);
|
||||
}
|
||||
$product->picture_large_alt = (string) $input->pictureLargeAlt;
|
||||
$product->picture_large_title = (string) $input->pictureLargeTitle;
|
||||
|
||||
$product->saveOrFail();
|
||||
|
||||
$this->syncTags($input->tags, $product);
|
||||
$this->syncAssociatedProducts($input->associatedProductsIds, $product);
|
||||
|
||||
return $product;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<array{id?: int|string, label: string}> $tags
|
||||
*/
|
||||
private function syncTags(array $tags, Product $product): void
|
||||
{
|
||||
$tagIdList = [];
|
||||
foreach ($tags as $tag) {
|
||||
$tagIdList[] = empty($tag['id'])
|
||||
? $this->storeTag($tag)
|
||||
: (int) $tag['id'];
|
||||
}
|
||||
|
||||
$product->tags()->sync($tagIdList);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array{id?: int|string, label: string} $tag
|
||||
*/
|
||||
private function storeTag(array $tag): int
|
||||
{
|
||||
$slug = Str::slug($tag['label']);
|
||||
$tagModel = Tag::query()
|
||||
->where('slug', $slug)
|
||||
->firstOrNew();
|
||||
$tagModel->setAttribute('label', $tag['label']);
|
||||
$tagModel->saveOrFail();
|
||||
|
||||
return (int) $tagModel->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<string> $productIdList
|
||||
*/
|
||||
private function syncAssociatedProducts(array $productIdList, Product $product): void
|
||||
{
|
||||
$syncData = [];
|
||||
foreach ($productIdList as $index => $id) {
|
||||
$syncData[$id] = ['sort_order' => $index + 1];
|
||||
}
|
||||
|
||||
$product->products()->sync($syncData);
|
||||
}
|
||||
}
|
||||
57
app/Actions/Products/UpdateProductInput.php
Normal file
57
app/Actions/Products/UpdateProductInput.php
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Products;
|
||||
|
||||
use App\Contracts\ActionInput;
|
||||
use App\Models\Product;
|
||||
use Carbon\CarbonInterface;
|
||||
use Domain\Enums\ProductBrand;
|
||||
use Domain\Enums\SellUnit;
|
||||
use Domain\Enums\TaxRate;
|
||||
use Domain\Enums\Units\InvoicingUnit;
|
||||
use Domain\Enums\Units\OrderUnit;
|
||||
use Money\Money;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
final readonly class UpdateProductInput implements ActionInput
|
||||
{
|
||||
/**
|
||||
* @param array<array{id?: int|string, label: string}> $tags
|
||||
* @param list<string> $associatedProductsIds
|
||||
*/
|
||||
public function __construct(
|
||||
public Product $product,
|
||||
public string $name,
|
||||
public string $shortDescription,
|
||||
public string $description,
|
||||
public Money $price,
|
||||
public InvoicingUnit $invoicingUnit,
|
||||
public int $stock,
|
||||
public int $rangeId,
|
||||
public SellUnit $sellUnit,
|
||||
public OrderUnit $orderUnit,
|
||||
public TaxRate $taxRate,
|
||||
public ProductBrand $brand,
|
||||
public ?int $weight = null,
|
||||
public bool $weightVariable = false,
|
||||
public ?CarbonInterface $orderableStart = null,
|
||||
public ?CarbonInterface $orderableEnd = null,
|
||||
public bool $star = false,
|
||||
public bool $status = false,
|
||||
public mixed $picture = null,
|
||||
public ?string $pictureAlt = null,
|
||||
public ?string $pictureTitle = null,
|
||||
public mixed $pictureLarge = null,
|
||||
public ?string $pictureLargeAlt = null,
|
||||
public ?string $pictureLargeTitle = null,
|
||||
public array $tags = [],
|
||||
public array $associatedProductsIds = [],
|
||||
) {
|
||||
Assert::stringNotEmpty(trim($this->name));
|
||||
Assert::maxLength($this->name, 255);
|
||||
Assert::greaterThanEq($this->stock, 0);
|
||||
Assert::greaterThanEq($this->rangeId, 1);
|
||||
}
|
||||
}
|
||||
43
app/Actions/Ranges/CreateRange.php
Normal file
43
app/Actions/Ranges/CreateRange.php
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Ranges;
|
||||
|
||||
use App\Contracts\Action;
|
||||
use App\Contracts\ActionInput;
|
||||
use App\Models\Range;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
/**
|
||||
* @implements \App\Contracts\Action<\App\Actions\Ranges\CreateRangeInput, \App\Models\Range>
|
||||
*/
|
||||
final class CreateRange implements Action
|
||||
{
|
||||
/**
|
||||
* @param \App\Actions\Ranges\CreateRangeInput $input
|
||||
*/
|
||||
#[\Override]
|
||||
public function __invoke(ActionInput $input): Range
|
||||
{
|
||||
Assert::isInstanceOf($input, CreateRangeInput::class);
|
||||
|
||||
$attributes = [
|
||||
'name' => $input->name,
|
||||
'description' => $input->description,
|
||||
'picture_alt' => $input->pictureMetadata->alt ?? '',
|
||||
'picture_title' => $input->pictureMetadata->title ?? '',
|
||||
'status' => $input->status,
|
||||
];
|
||||
|
||||
if ($input->picture !== null) {
|
||||
$attributes['picture'] = $input->picture;
|
||||
}
|
||||
|
||||
$range = new Range();
|
||||
$range->fill($attributes);
|
||||
$range->saveOrFail();
|
||||
|
||||
return $range;
|
||||
}
|
||||
}
|
||||
33
app/Actions/Ranges/CreateRangeInput.php
Normal file
33
app/Actions/Ranges/CreateRangeInput.php
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Ranges;
|
||||
|
||||
use App\Contracts\ActionInput;
|
||||
use Domain\Models\ValueObjects\PictureMetadata;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
final readonly class CreateRangeInput implements ActionInput
|
||||
{
|
||||
public function __construct(
|
||||
public string $name,
|
||||
public ?string $description,
|
||||
public UploadedFile | string | null $picture,
|
||||
public PictureMetadata $pictureMetadata,
|
||||
public bool $status,
|
||||
) {
|
||||
$trimmedName = trim($this->name);
|
||||
Assert::stringNotEmpty($trimmedName, 'The range name cannot be empty.');
|
||||
Assert::minLength($trimmedName, 2, 'The range name must be at least 2 characters.');
|
||||
Assert::maxLength($trimmedName, 45, 'The range name cannot exceed 45 characters.');
|
||||
|
||||
if ($this->picture !== null) {
|
||||
Assert::notNull($this->pictureMetadata->alt, 'Picture alt is required when picture is provided.');
|
||||
Assert::stringNotEmpty(trim($this->pictureMetadata->alt), 'Picture alt cannot be empty when picture is provided.');
|
||||
Assert::notNull($this->pictureMetadata->title, 'Picture title is required when picture is provided.');
|
||||
Assert::stringNotEmpty(trim($this->pictureMetadata->title), 'Picture title cannot be empty when picture is provided.');
|
||||
}
|
||||
}
|
||||
}
|
||||
26
app/Actions/Ranges/DeleteRange.php
Normal file
26
app/Actions/Ranges/DeleteRange.php
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Ranges;
|
||||
|
||||
use App\Contracts\Action;
|
||||
use App\Contracts\ActionInput;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
/**
|
||||
* @implements \App\Contracts\Action<\App\Actions\Ranges\DeleteRangeInput, bool>
|
||||
*/
|
||||
final class DeleteRange implements Action
|
||||
{
|
||||
/**
|
||||
* @param \App\Actions\Ranges\DeleteRangeInput $input
|
||||
*/
|
||||
#[\Override]
|
||||
public function __invoke(ActionInput $input): bool
|
||||
{
|
||||
Assert::isInstanceOf($input, DeleteRangeInput::class);
|
||||
|
||||
return (bool) $input->range->deleteOrFail();
|
||||
}
|
||||
}
|
||||
16
app/Actions/Ranges/DeleteRangeInput.php
Normal file
16
app/Actions/Ranges/DeleteRangeInput.php
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Ranges;
|
||||
|
||||
use App\Contracts\ActionInput;
|
||||
use App\Models\Range;
|
||||
|
||||
final readonly class DeleteRangeInput implements ActionInput
|
||||
{
|
||||
public function __construct(
|
||||
public Range $range,
|
||||
) {
|
||||
}
|
||||
}
|
||||
43
app/Actions/Ranges/UpdateRange.php
Normal file
43
app/Actions/Ranges/UpdateRange.php
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Ranges;
|
||||
|
||||
use App\Contracts\Action;
|
||||
use App\Contracts\ActionInput;
|
||||
use App\Models\Range;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
/**
|
||||
* @implements \App\Contracts\Action<\App\Actions\Ranges\UpdateRangeInput, \App\Models\Range>
|
||||
*/
|
||||
final class UpdateRange implements Action
|
||||
{
|
||||
/**
|
||||
* @param \App\Actions\Ranges\UpdateRangeInput $input
|
||||
*/
|
||||
#[\Override]
|
||||
public function __invoke(ActionInput $input): Range
|
||||
{
|
||||
Assert::isInstanceOf($input, UpdateRangeInput::class);
|
||||
|
||||
$attributes = [
|
||||
'name' => $input->name,
|
||||
'description' => $input->description,
|
||||
'picture_alt' => $input->pictureMetadata->alt ?? '',
|
||||
'picture_title' => $input->pictureMetadata->title ?? '',
|
||||
'status' => $input->status,
|
||||
];
|
||||
|
||||
if ($input->picture !== null) {
|
||||
$attributes['picture'] = $input->picture;
|
||||
}
|
||||
|
||||
$range = $input->range;
|
||||
$range->fill($attributes);
|
||||
$range->saveOrFail();
|
||||
|
||||
return $range;
|
||||
}
|
||||
}
|
||||
35
app/Actions/Ranges/UpdateRangeInput.php
Normal file
35
app/Actions/Ranges/UpdateRangeInput.php
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Ranges;
|
||||
|
||||
use App\Contracts\ActionInput;
|
||||
use App\Models\Range;
|
||||
use Domain\Models\ValueObjects\PictureMetadata;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
final readonly class UpdateRangeInput implements ActionInput
|
||||
{
|
||||
public function __construct(
|
||||
public Range $range,
|
||||
public string $name,
|
||||
public ?string $description,
|
||||
public UploadedFile | string | null $picture,
|
||||
public PictureMetadata $pictureMetadata,
|
||||
public bool $status,
|
||||
) {
|
||||
$trimmedName = trim($this->name);
|
||||
Assert::stringNotEmpty($trimmedName, 'The range name cannot be empty.');
|
||||
Assert::minLength($trimmedName, 2, 'The range name must be at least 2 characters.');
|
||||
Assert::maxLength($trimmedName, 45, 'The range name cannot exceed 45 characters.');
|
||||
|
||||
if ($this->picture !== null) {
|
||||
Assert::notNull($this->pictureMetadata->alt, 'Picture alt is required when picture is provided.');
|
||||
Assert::stringNotEmpty(trim($this->pictureMetadata->alt), 'Picture alt cannot be empty when picture is provided.');
|
||||
Assert::notNull($this->pictureMetadata->title, 'Picture title is required when picture is provided.');
|
||||
Assert::stringNotEmpty(trim($this->pictureMetadata->title), 'Picture title cannot be empty when picture is provided.');
|
||||
}
|
||||
}
|
||||
}
|
||||
39
app/Actions/Settings/UpdateSettings.php
Normal file
39
app/Actions/Settings/UpdateSettings.php
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Settings;
|
||||
|
||||
use App\Contracts\Action;
|
||||
use App\Contracts\ActionInput;
|
||||
use App\Models\Setting;
|
||||
use Domain\Models\ValueObjects\ShopperSiteCode;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
/**
|
||||
* @implements \App\Contracts\Action<\App\Actions\Settings\UpdateSettingsInput, \App\Models\Setting>
|
||||
*/
|
||||
final class UpdateSettings implements Action
|
||||
{
|
||||
/**
|
||||
* @param \App\Actions\Settings\UpdateSettingsInput $input
|
||||
*/
|
||||
#[\Override]
|
||||
public function __invoke(ActionInput $input): Setting
|
||||
{
|
||||
Assert::isInstanceOf($input, UpdateSettingsInput::class);
|
||||
|
||||
$setting = Setting::getSingleton();
|
||||
$setting->fill([
|
||||
'employee_sites' => array_values(
|
||||
array_map(
|
||||
static fn (ShopperSiteCode $siteCode): string => $siteCode->value,
|
||||
$input->employeeSites
|
||||
)
|
||||
),
|
||||
]);
|
||||
$setting->saveOrFail();
|
||||
|
||||
return $setting;
|
||||
}
|
||||
}
|
||||
21
app/Actions/Settings/UpdateSettingsInput.php
Normal file
21
app/Actions/Settings/UpdateSettingsInput.php
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Settings;
|
||||
|
||||
use App\Contracts\ActionInput;
|
||||
use Domain\Models\ValueObjects\ShopperSiteCode;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
final readonly class UpdateSettingsInput implements ActionInput
|
||||
{
|
||||
/**
|
||||
* @param array<int, \Domain\Models\ValueObjects\ShopperSiteCode> $employeeSites
|
||||
*/
|
||||
public function __construct(
|
||||
public array $employeeSites = [],
|
||||
) {
|
||||
Assert::allIsInstanceOf($this->employeeSites, ShopperSiteCode::class);
|
||||
}
|
||||
}
|
||||
42
app/Actions/Shops/CreateShop.php
Normal file
42
app/Actions/Shops/CreateShop.php
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Shops;
|
||||
|
||||
use App\Contracts\Action;
|
||||
use App\Contracts\ActionInput;
|
||||
use App\Models\Shop;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
/**
|
||||
* @implements \App\Contracts\Action<\App\Actions\Shops\CreateShopInput, \App\Models\Shop>
|
||||
*/
|
||||
final class CreateShop implements Action
|
||||
{
|
||||
/**
|
||||
* @param \App\Actions\Shops\CreateShopInput $input
|
||||
*/
|
||||
#[\Override]
|
||||
public function __invoke(ActionInput $input): Shop
|
||||
{
|
||||
Assert::isInstanceOf($input, CreateShopInput::class);
|
||||
|
||||
$attributes = [
|
||||
'name' => $input->name,
|
||||
'address' => $input->address,
|
||||
'postcode' => $input->postcode,
|
||||
'city' => $input->city,
|
||||
'phone' => $input->phone?->value,
|
||||
'latitude' => $input->coordinates !== null ? (string) $input->coordinates->latitude : null,
|
||||
'longitude' => $input->coordinates !== null ? (string) $input->coordinates->longitude : null,
|
||||
];
|
||||
|
||||
$shop = new Shop();
|
||||
$shop->fill($attributes);
|
||||
$shop->default = $input->default;
|
||||
$shop->saveOrFail();
|
||||
|
||||
return $shop;
|
||||
}
|
||||
}
|
||||
39
app/Actions/Shops/CreateShopInput.php
Normal file
39
app/Actions/Shops/CreateShopInput.php
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Shops;
|
||||
|
||||
use App\Contracts\ActionInput;
|
||||
use Domain\Models\ValueObjects\Coordinates;
|
||||
use Domain\Models\ValueObjects\PhoneNumber;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
final readonly class CreateShopInput implements ActionInput
|
||||
{
|
||||
public function __construct(
|
||||
public string $name,
|
||||
public string $address,
|
||||
public string $postcode,
|
||||
public string $city,
|
||||
public ?PhoneNumber $phone,
|
||||
public ?Coordinates $coordinates,
|
||||
public bool $default,
|
||||
) {
|
||||
$trimmedName = trim($this->name);
|
||||
Assert::stringNotEmpty($trimmedName, 'Shop name cannot be empty.');
|
||||
Assert::maxLength($trimmedName, 255, 'Shop name cannot exceed 255 characters.');
|
||||
|
||||
$trimmedAddress = trim($this->address);
|
||||
Assert::stringNotEmpty($trimmedAddress, 'Shop address cannot be empty.');
|
||||
Assert::maxLength($trimmedAddress, 255, 'Shop address cannot exceed 255 characters.');
|
||||
|
||||
$trimmedPostcode = trim($this->postcode);
|
||||
Assert::stringNotEmpty($trimmedPostcode, 'Shop postcode cannot be empty.');
|
||||
Assert::maxLength($trimmedPostcode, 255, 'Shop postcode cannot exceed 255 characters.');
|
||||
|
||||
$trimmedCity = trim($this->city);
|
||||
Assert::stringNotEmpty($trimmedCity, 'Shop city cannot be empty.');
|
||||
Assert::maxLength($trimmedCity, 255, 'Shop city cannot exceed 255 characters.');
|
||||
}
|
||||
}
|
||||
26
app/Actions/Shops/DeleteShop.php
Normal file
26
app/Actions/Shops/DeleteShop.php
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Shops;
|
||||
|
||||
use App\Contracts\Action;
|
||||
use App\Contracts\ActionInput;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
/**
|
||||
* @implements \App\Contracts\Action<\App\Actions\Shops\DeleteShopInput, bool>
|
||||
*/
|
||||
final class DeleteShop implements Action
|
||||
{
|
||||
/**
|
||||
* @param \App\Actions\Shops\DeleteShopInput $input
|
||||
*/
|
||||
#[\Override]
|
||||
public function __invoke(ActionInput $input): bool
|
||||
{
|
||||
Assert::isInstanceOf($input, DeleteShopInput::class);
|
||||
|
||||
return (bool) $input->shop->deleteOrFail();
|
||||
}
|
||||
}
|
||||
16
app/Actions/Shops/DeleteShopInput.php
Normal file
16
app/Actions/Shops/DeleteShopInput.php
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Shops;
|
||||
|
||||
use App\Contracts\ActionInput;
|
||||
use App\Models\Shop;
|
||||
|
||||
final readonly class DeleteShopInput implements ActionInput
|
||||
{
|
||||
public function __construct(
|
||||
public Shop $shop,
|
||||
) {
|
||||
}
|
||||
}
|
||||
42
app/Actions/Shops/UpdateShop.php
Normal file
42
app/Actions/Shops/UpdateShop.php
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Shops;
|
||||
|
||||
use App\Contracts\Action;
|
||||
use App\Contracts\ActionInput;
|
||||
use App\Models\Shop;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
/**
|
||||
* @implements \App\Contracts\Action<\App\Actions\Shops\UpdateShopInput, \App\Models\Shop>
|
||||
*/
|
||||
final class UpdateShop implements Action
|
||||
{
|
||||
/**
|
||||
* @param \App\Actions\Shops\UpdateShopInput $input
|
||||
*/
|
||||
#[\Override]
|
||||
public function __invoke(ActionInput $input): Shop
|
||||
{
|
||||
Assert::isInstanceOf($input, UpdateShopInput::class);
|
||||
|
||||
$attributes = [
|
||||
'name' => $input->name,
|
||||
'address' => $input->address,
|
||||
'postcode' => $input->postcode,
|
||||
'city' => $input->city,
|
||||
'phone' => $input->phone?->value,
|
||||
'latitude' => $input->coordinates !== null ? (string) $input->coordinates->latitude : null,
|
||||
'longitude' => $input->coordinates !== null ? (string) $input->coordinates->longitude : null,
|
||||
];
|
||||
|
||||
$shop = $input->shop;
|
||||
$shop->fill($attributes);
|
||||
$shop->default = $input->default;
|
||||
$shop->saveOrFail();
|
||||
|
||||
return $shop;
|
||||
}
|
||||
}
|
||||
41
app/Actions/Shops/UpdateShopInput.php
Normal file
41
app/Actions/Shops/UpdateShopInput.php
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Shops;
|
||||
|
||||
use App\Contracts\ActionInput;
|
||||
use App\Models\Shop;
|
||||
use Domain\Models\ValueObjects\Coordinates;
|
||||
use Domain\Models\ValueObjects\PhoneNumber;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
final readonly class UpdateShopInput implements ActionInput
|
||||
{
|
||||
public function __construct(
|
||||
public Shop $shop,
|
||||
public string $name,
|
||||
public string $address,
|
||||
public string $postcode,
|
||||
public string $city,
|
||||
public ?PhoneNumber $phone,
|
||||
public ?Coordinates $coordinates,
|
||||
public bool $default,
|
||||
) {
|
||||
$trimmedName = trim($this->name);
|
||||
Assert::stringNotEmpty($trimmedName, 'Shop name cannot be empty.');
|
||||
Assert::maxLength($trimmedName, 255, 'Shop name cannot exceed 255 characters.');
|
||||
|
||||
$trimmedAddress = trim($this->address);
|
||||
Assert::stringNotEmpty($trimmedAddress, 'Shop address cannot be empty.');
|
||||
Assert::maxLength($trimmedAddress, 255, 'Shop address cannot exceed 255 characters.');
|
||||
|
||||
$trimmedPostcode = trim($this->postcode);
|
||||
Assert::stringNotEmpty($trimmedPostcode, 'Shop postcode cannot be empty.');
|
||||
Assert::maxLength($trimmedPostcode, 255, 'Shop postcode cannot exceed 255 characters.');
|
||||
|
||||
$trimmedCity = trim($this->city);
|
||||
Assert::stringNotEmpty($trimmedCity, 'Shop city cannot be empty.');
|
||||
Assert::maxLength($trimmedCity, 255, 'Shop city cannot exceed 255 characters.');
|
||||
}
|
||||
}
|
||||
28
app/Actions/Shops/UpdateShopPickupExceptions.php
Normal file
28
app/Actions/Shops/UpdateShopPickupExceptions.php
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Shops;
|
||||
|
||||
use App\Contracts\Action;
|
||||
use App\Contracts\ActionInput;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
/**
|
||||
* @implements \App\Contracts\Action<\App\Actions\Shops\UpdateShopPickupExceptionsInput, bool>
|
||||
*/
|
||||
final class UpdateShopPickupExceptions implements Action
|
||||
{
|
||||
/**
|
||||
* @param \App\Actions\Shops\UpdateShopPickupExceptionsInput $input
|
||||
*/
|
||||
#[\Override]
|
||||
public function __invoke(ActionInput $input): bool
|
||||
{
|
||||
Assert::isInstanceOf($input, UpdateShopPickupExceptionsInput::class);
|
||||
|
||||
$input->shop->pickupExceptions()->sync($input->syncData);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
45
app/Actions/Shops/UpdateShopPickupExceptionsInput.php
Normal file
45
app/Actions/Shops/UpdateShopPickupExceptionsInput.php
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Shops;
|
||||
|
||||
use App\Contracts\ActionInput;
|
||||
use App\Models\Shop;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
final readonly class UpdateShopPickupExceptionsInput implements ActionInput
|
||||
{
|
||||
/**
|
||||
* @param array<array-key, array{replacement_date: string|null, order_deadline_at: string|null}> $syncData
|
||||
*/
|
||||
public function __construct(
|
||||
public Shop $shop,
|
||||
public array $syncData,
|
||||
) {
|
||||
foreach ($this->syncData as $item) {
|
||||
Assert::isArray($item, 'Each sync item must be an array.');
|
||||
Assert::keyExists($item, 'replacement_date', 'Sync item must contain replacement_date key.');
|
||||
Assert::keyExists($item, 'order_deadline_at', 'Sync item must contain order_deadline_at key.');
|
||||
Assert::nullOrString($item['replacement_date'], 'replacement_date must be a string or null.');
|
||||
Assert::nullOrString($item['order_deadline_at'], 'order_deadline_at must be a string or null.');
|
||||
|
||||
if ($item['order_deadline_at'] !== null) {
|
||||
Assert::notNull(
|
||||
$item['replacement_date'],
|
||||
'A replacement date is required when an order deadline is set.'
|
||||
);
|
||||
}
|
||||
|
||||
if ($item['replacement_date'] !== null && $item['order_deadline_at'] !== null) {
|
||||
$replacementDate = new \DateTimeImmutable($item['replacement_date']);
|
||||
$orderDeadlineAt = new \DateTimeImmutable($item['order_deadline_at']);
|
||||
|
||||
Assert::true(
|
||||
$orderDeadlineAt < $replacementDate,
|
||||
'Order deadline must be strictly before the replacement date.'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue