Compare commits

...
Sign in to create a new pull request.

337 commits
crud ... master

Author SHA1 Message Date
Jeremy Munsch
a69f51b61a
Merge branch 'trunk' 2026-10-26 12:17:59 +01:00
Jeremy Munsch
1069076e48
Merge branch 'develop' into trunk 2026-10-26 12:17:49 +01:00
Jeremy Munsch
a2ded8f2b2
build: fix manifest user to 82 2026-10-26 12:17:34 +01:00
Noah Lancien
4fa91c3c88 Merge branch 'trunk' 2026-10-22 16:52:24 +02:00
Noah Lancien
d49d28ffc6 Merge branch 'develop' into trunk 2026-10-22 16:52:02 +02:00
Noix
ca569455dd Merge branch 'bump-deps-and-docker-file' into 'develop'
Bump deps and docker file

See merge request ldc-web/projets/crm!57
2026-10-22 14:50:37 +00:00
b01f0fb9dd
chore: add missing phpstan 2026-10-22 16:40:39 +02:00
cc6ad2e6e1
chore: phpcs fix 2026-10-22 16:37:28 +02:00
ea1d6c6bba
chore : vscode config intellephnese php version 2026-10-22 16:01:36 +02:00
132c25a75e
chore: replace deprecated constant mysql in config 2026-10-22 16:01:21 +02:00
c235055b62
build: docker file bump 2026-10-22 15:58:19 +02:00
4eaebf86c0
fix: Claim factory 2026-10-22 15:58:06 +02:00
61641660e6
chore: bump composer (php8.5 with advisories) 2026-10-22 15:57:51 +02:00
29e5728220
chore: git ignore tests coverage 2026-10-22 15:57:30 +02:00
Vincent BRIDIER
feda6e7613 Merge branch 'trunk' 2026-07-29 11:48:56 +02:00
Vincent BRIDIER
07804370d5 Merge branch 'develop' into trunk 2026-07-29 11:23:25 +02:00
Vincent BRIDIER
c1a244a4f4 feat: Add k8s internal url for trusting host 2026-07-29 11:23:09 +02:00
K Muller
700992e89f Merge branch 'trunk' 2026-05-21 13:09:08 +02:00
K Muller
c26b8c071d Merge branch 'develop' into trunk 2026-05-21 13:08:39 +02:00
karl.muller
eeddf109f5 Merge branch '59-cron-job' into 'develop'
feat: ✨ run job only on weekdays

See merge request ldc-web/projets/crm!55
2026-05-21 11:07:50 +00:00
Corentin Huet
25188fcbcf feat: ✨ run job only on weekdays 2026-05-12 13:25:49 +02:00
Noah Lancien
3e954bb51b Merge branch 'trunk' 2026-04-23 12:45:18 +02:00
Noah Lancien
d8e00b3833 Merge branch 'develop' into trunk 2026-04-23 12:45:03 +02:00
Noix
820eb7b62c Merge branch 'issue-51' into 'develop'
Issue 51

See merge request ldc-web/projets/crm!54
2026-04-23 10:41:56 +00:00
Alexis Gousseau
85acd0be1e
chore: 🔖 release v4.1.10 2026-04-23 11:38:41 +02:00
Alexis Gousseau
56c96cc794
chore: 🔧 set new rediction in .htaccess 2026-04-23 11:38:24 +02:00
Alexis Gousseau
b5bf949dca
ci: 👷 Added @uwil npm config in Dockerfile 2026-04-23 11:38:24 +02:00
Alexis Gousseau
a226e37086
feat: ✨ add ckeditor input in crud of claim mail templates and claim manual response #51 2026-04-23 11:38:23 +02:00
Alexis Gousseau
49ee59295b
feat: ✨ add LogoutUnpublishedUsers middleware 2026-04-23 11:38:23 +02:00
Alexis Gousseau
8f7ef0ed92
feat: ✨ add commit lint from husky configuration 2026-04-23 11:38:23 +02:00
Noah Lancien
6e12256a33 Merge branch 'trunk' 2026-04-23 09:37:35 +02:00
Noah Lancien
5ef171c666 Merge branch 'develop' into trunk 2026-04-23 09:37:20 +02:00
Noix
15eef6a692 Merge branch 'issue-52' into 'develop'
Issue 52

See merge request ldc-web/projets/crm!53
2026-04-23 07:36:57 +00:00
Alexis Gousseau
8f5f01df8e
chore: 🔖 release v4.1.9 2026-04-15 12:23:33 +02:00
Alexis Gousseau
6ceb32fdca
fix: 🐛 set brand id from service in claim call #52 2026-04-15 12:22:56 +02:00
Noah Lancien
e6194a3d73 Merge branch 'trunk' 2026-04-13 11:30:57 +02:00
Noah Lancien
1defb88357 Merge branch 'develop' into trunk 2026-04-13 11:30:40 +02:00
Noix
c5cb57d88a Merge branch 'add-germany' into 'develop'
fix: 🏷️ add Germany to the valid countries

See merge request ldc-web/projets/crm!52
2026-04-13 09:29:59 +00:00
Noah Lancien
0787d77beb Merge branch 'trunk' 2026-04-13 09:15:24 +02:00
Corentin Huet
69b63d0aae fix: 🏷️ add Germany to the valid countries 2026-04-09 16:20:57 +02:00
Noah Lancien
01f3c8a319 Merge branch 'develop' into trunk 2026-04-09 11:11:36 +02:00
Noix
47018bad75 Merge branch 'issue-53' into 'develop'
Issue 53 - Association customer à faire côté Serveur et non client

See merge request ldc-web/projets/crm!50
2026-04-09 09:10:49 +00:00
Alexis Gousseau
b23a1ab795
chore: 🔖 release v4.1.8 2026-03-31 15:26:15 +02:00
Alexis Gousseau
c4d45c2501
fix: 🚨 resolve phpcs errors 2026-03-31 15:25:55 +02:00
Alexis Gousseau
1920acf6e6
test: ✅ update and pass all tests 2026-03-31 15:04:41 +02:00
Alexis Gousseau
2a56bf4c67
fix: 🔥 move claim customer association into server side 2026-03-31 14:59:48 +02:00
Noah Lancien
25e58a6ed2 Merge branch 'trunk' 2026-01-29 15:33:25 +01:00
Noah Lancien
67d4debcdb Merge branch 'develop' into trunk 2026-01-29 15:32:45 +01:00
Noah Lancien
e170cf351e ci: limit jobs history to 1 2026-01-29 15:32:05 +01:00
Noah Lancien
1d28cbe7cd Merge branch 'trunk' 2026-01-12 10:14:14 +01:00
Noah Lancien
cfeca2bd21 Merge branch 'develop' into trunk 2026-01-12 10:13:48 +01:00
Noah Lancien
4f21f46464 feat: updated admin-secure 2026-01-12 10:12:57 +01:00
Noah Lancien
f2965dd9ef ci: added ci variables to define pod resources 2026-01-12 10:04:08 +01:00
Noah Lancien
dac9b12106 Merge branch 'develop' into trunk 2026-01-06 17:17:07 +01:00
noah.lancien
d20d3bd710 Merge branch '54-mail-to-digital' into 'develop'
feat: send mail daily at 7am if some claims are waiting for the digital service

See merge request ldc-web/projets/crm!49
2026-01-06 16:16:11 +00:00
Noah Lancien
6e8a296205 feat: send mail daily at 7am if some claims are waiting for the digital service 2026-01-06 17:05:32 +01:00
Vincent BRIDIER
83400e1e20 Merge branch 'trunk' 2025-12-19 08:53:08 +01:00
Vincent BRIDIER
e7ca76950a Merge branch 'develop' into trunk 2025-12-19 08:52:57 +01:00
Vincent BRIDIER
ff3550893a fix: Fix Claim interface 2025-12-19 08:52:46 +01:00
Vincent BRIDIER
93202edd09 Merge branch 'trunk' 2025-12-18 14:39:30 +01:00
Vincent BRIDIER
285e12bde4 Merge branch 'develop' into trunk 2025-12-16 19:59:39 +01:00
Vincent BRIDIER
93294ef6de fix: Some layout + Claims fixes 2025-12-16 19:59:05 +01:00
Vincent BRIDIER
06f71db87b feat: Some common fixes 2025-12-16 19:56:11 +01:00
Noah Lancien
a2fe764732 Merge branch 'develop' into trunk 2025-12-08 16:50:03 +01:00
noah.lancien
a831803437 Merge branch 'claims-brand-filter' into 'develop'
fix: hide subject filter when there's no brand selected + select default brand if user only has one

See merge request ldc-web/projets/crm!48
2025-12-08 15:47:28 +00:00
Noah Lancien
e815b89e53 fix: hide subject filter when there's no brand selected + select default brand if user only has one 2025-12-08 16:35:26 +01:00
Noah Lancien
215fb1327e Merge branch 'develop' into trunk 2025-12-08 15:11:21 +01:00
Noah Lancien
92607611dd fix: 500 on users.create because of wrong condition for brands input 2025-12-08 15:10:57 +01:00
Noah Lancien
00a9fc7d76 Merge branch 'develop' into trunk 2025-12-08 15:01:27 +01:00
Noah Lancien
ffaaf1ec1b fix: increase table size in index-list of claims 2025-12-08 15:01:01 +01:00
Noah Lancien
dbd4c88e74 Merge branch 'develop' into trunk 2025-12-08 11:56:12 +01:00
Noah Lancien
f7bac9c221 fix: style update for index-list of claims 2025-12-08 11:55:45 +01:00
Noah Lancien
5354adab6e Merge branch 'develop' into trunk 2025-12-03 15:57:59 +01:00
noah.lancien
8887aabac4 Merge branch 'display-claim-file' into 'develop'
fix: claim.show file button was broken since the file is in private storage

See merge request ldc-web/projets/crm!47
2025-12-03 14:54:56 +00:00
Noah Lancien
faa63e271f fix: claim.show file button was broken since the file is in private storage 2025-12-03 10:31:18 +01:00
Noah Lancien
bd5862d592 Merge branch 'develop' into trunk 2025-12-02 11:42:59 +01:00
noah.lancien
b6fe43cdcf Merge branch '49-update-recla' into 'develop'
feat: many ui improvements for the claim part

See merge request ldc-web/projets/crm!46
2025-12-02 10:40:31 +00:00
Noah Lancien
647943da11 fix: make export more generic 2025-12-02 09:48:28 +01:00
Noah Lancien
0aea086f26 fix: make the export feature more generic 2025-12-01 16:23:45 +01:00
Noah Lancien
2b240ed4ca fix: move uploaded claims files to private storage + use .zip for some exports types 2025-11-27 16:19:48 +01:00
Noah Lancien
b7b4013897 feat: remove export mail and replaced with .csv and .pdf downloads 2025-11-25 14:57:34 +01:00
Noah Lancien
8fbaa234b4 feat: many ui improvements for the claim part 2025-11-24 12:58:47 +01:00
Vincent BRIDIER
d8796ee78a feat: Update README 2025-11-05 09:10:46 +00:00
Noah Lancien
d1d9ffd418 Merge branch 'develop' into trunk 2025-10-16 16:37:58 +02:00
noah.lancien
45849688f6 Merge branch '48-docs-reclas' into 'develop'
feat: ✨ wording update and restrict profile page for marketing role

See merge request ldc-web/projets/crm!45
2025-10-16 14:35:44 +00:00
Corentin Huet
5d5a4c828f refactor: ♻️ make user policies strict to conceptor role 2025-10-16 16:20:47 +02:00
Corentin Huet
58c116f2f0 feat: ✨ wording update and restrict profile page for marketing role 2025-10-15 15:57:38 +02:00
Noah Lancien
cc9521c233 Merge branch 'develop' into trunk 2025-10-07 12:58:21 +02:00
Noah Lancien
b4f343940e ci: added staging-resources.yaml 2025-10-07 12:57:51 +02:00
Noah Lancien
de20063c5f Merge branch 'develop' into trunk 2025-10-07 12:49:01 +02:00
noah.lancien
fd11b966e4 Merge branch 'update' into 'develop'
chore: 🔖 release v4.1.7

See merge request ldc-web/projets/crm!44
2025-10-07 10:46:17 +00:00
Alexis Gousseau
d9357de45f
chore: 🔖 release v4.1.7 2025-10-07 11:32:03 +02:00
Alexis Gousseau
aa8ac3fec5
fix: 🐛 restrict user brands associated to conceptor users 2025-10-07 11:32:03 +02:00
Alexis Gousseau
b36aa19028
fix: ✅ update and pass all tests 2025-10-07 11:32:02 +02:00
Alexis Gousseau
16e4e4ee19
fix: 🐛 update claim customer relation with hashcode 2025-10-07 11:32:02 +02:00
Alexis Gousseau
151d9e04a9
fix: ♿️ update claim status after sending manual response 2025-10-07 11:32:02 +02:00
Alexis Gousseau
bd343277cc
fix: 🛂 update claims permissions 2025-10-07 11:31:53 +02:00
Alexis Gousseau
44035c9589
fix: 🌐 update claim messages translations 2025-10-07 10:32:38 +02:00
Noah Lancien
14de384af1 Merge branch 'develop' into trunk 2025-09-17 10:52:10 +02:00
Noah Lancien
b6b06d6cf8 chore: bump laravel-common to 4.1.1 2025-09-17 10:49:43 +02:00
Noah Lancien
9c91351388 Merge branch 'trunk' 2025-09-09 09:24:53 +02:00
Noah Lancien
d5d9ef6fdc Merge branch 'develop' into trunk 2025-09-08 15:36:16 +02:00
noah.lancien
b31f87bf8b Merge branch 'claim_file' into 'develop'
fix: 🐛 use store base64 file method of laravel common to save claim file

See merge request ldc-web/projets/crm!39
2025-09-08 13:34:45 +00:00
noah.lancien
ace4e0cce8 Merge branch 'cast_model_id' into 'develop'
fix: 🚑 update casting model_id column

See merge request ldc-web/projets/crm!43
2025-09-08 13:33:34 +00:00
Alexis Gousseau
c9f25b17aa
chore: 🔖 release v4.1.5 2025-09-08 15:06:33 +02:00
Alexis Gousseau
ecc2b7059e
fix: ⚡ improve bo pagination #46 2025-09-08 15:06:00 +02:00
Alexis Gousseau
3ab1a2244e
fix: 🐛 associating answer to their question on store action #25 2025-09-08 14:59:54 +02:00
Alexis Gousseau
7a2318b6a3
fix: 🐛 use store base64 file method of laravel common to save claim file 2025-09-08 14:59:54 +02:00
Alexis Gousseau
ded7cd7266
chore: ⬆️ update laravel-common to v4.1.0 2025-09-08 14:59:54 +02:00
Alexis Gousseau
1aaadb88d0
fix: 🚨 resolve phpcs errors 2025-09-08 14:59:53 +02:00
Alexis Gousseau
b3fb0301f8
chore: 🔖 release v4.1.4 2025-09-08 14:25:24 +02:00
Alexis Gousseau
09414e228f
fix: 🚑 update casting model_id column 2025-09-08 14:25:11 +02:00
Vincent BRIDIER
ff123f6769
Merge branch 'trunk' 2025-09-03 12:42:10 +02:00
Vincent BRIDIER
811795abd8
Merge branch 'develop' into trunk 2025-09-03 12:41:44 +02:00
Vincent BRIDIER
de13bde18f Merge branch 'cast_model_id' into 'develop'
fix: 🚑 force cast to string for model_id column

See merge request ldc-web/projets/crm!42
2025-09-03 10:41:28 +00:00
Alexis Gousseau
18b83b7197
chore: 🔖 release v4.1.3 2025-09-03 10:39:52 +02:00
Alexis Gousseau
93331aaca1
fix: 🚑 force cast to string for model_id column 2025-09-03 10:39:29 +02:00
Vincent BRIDIER
d61da12e63 Merge branch 'singleoptin' into 'develop'
fix: 🐛 change the customer's opt-in value if it exists when call api optin route #27

Closes #27

See merge request ldc-web/projets/crm!38
2025-08-28 10:08:54 +00:00
Alexis Gousseau
11a999ce42
chore: 🔖 release v4.1.2 2025-08-27 11:52:09 +02:00
Alexis Gousseau
1b9a5dacf8
fix: 🐛 update questions/answers filters on index pages 2025-08-27 11:51:56 +02:00
Vincent BRIDIER
e76312fad2
Merge branch 'develop' into trunk 2025-08-27 09:41:31 +02:00
Alexis Gousseau
0f008b0db4
fix: 🐛 change the customer's opt-in value if it exists when call api optin route #27 2025-08-27 09:39:17 +02:00
Vincent BRIDIER
06694f0a48 Merge branch 'cache' into 'develop'
fix: 🐛 update saving claim file

See merge request ldc-web/projets/crm!37
2025-08-27 07:30:43 +00:00
Alexis Gousseau
448369c7ab
chore: 🔖 release v4.1.1 2025-08-26 09:38:02 +02:00
Alexis Gousseau
eeb467d1f0
fix: 🐛 update saving claim file 2025-08-26 09:34:58 +02:00
Alexis Gousseau
4a23d74917
fix: 🌐 update some claim translations 2025-08-25 14:26:32 +02:00
Alexis Gousseau
b9f43428d8
fix: 🐛 call the cache reset method on every model event for the claim subject and template 2025-08-25 14:25:38 +02:00
Noah Lancien
5527740c26 Merge branch 'trunk' 2025-08-12 08:52:49 +02:00
Vincent BRIDIER
83c4e4d7c4
Merge branch 'develop' into trunk 2025-08-07 18:08:21 +02:00
Vincent BRIDIER
2f8a53c653 Merge branch 'clean' into 'develop'
Clean

See merge request ldc-web/projets/crm!36
2025-08-07 16:07:45 +00:00
Alexis Gousseau
351126ebf8
fix: 🐛 update answer cache and cascade delete 2025-08-07 17:41:07 +02:00
Alexis Gousseau
da8074c5a4
fix: 🐛 reorder function parameters to avoid PHP 8.0+ optional parameter warning 2025-08-07 17:41:07 +02:00
Alexis Gousseau
36494a7944
feat: ✨ add brand fields (logo/domain/url/url_claim) 2025-08-07 17:40:37 +02:00
Noah Lancien
c4473115d2 Merge branch 'develop' into trunk 2025-08-06 16:23:46 +02:00
Noah Lancien
30c113a30c ci: update image version + replace 443 port by 80 in manifest 2025-08-06 16:23:30 +02:00
Noah Lancien
1ac10f9276 Merge branch 'develop' into trunk 2025-08-06 15:27:02 +02:00
noah.lancien
515bbf4758 Merge branch 'reclas' into 'develop'
Add claim functionnality

Closes #40

See merge request ldc-web/projets/crm!35
2025-08-06 13:24:18 +00:00
Alexis Gousseau
76cc3a5cd8
fix: ✅ pass all tests with client up to date 2025-08-05 15:30:34 +02:00
Alexis Gousseau
978cfc2109
fix: 🔥 remove rennokki/laravel-eloquent-query-cache package 2025-08-05 14:12:05 +02:00
Alexis Gousseau
8caaff989c
feat: ✨ adding a field to associate brands with users 2025-08-05 12:37:46 +02:00
Alexis Gousseau
fff623144a
fix: 🐛 standardize ID extraction using modelKeys() instead of pluck('id') 2025-08-05 09:49:31 +02:00
Alexis Gousseau
927ae02240
chore: 📦 update laravel common dependency (v3.4.3) 2025-08-04 16:07:31 +02:00
Alexis Gousseau
281d23b03e
feat: ✨ add cache on models 2025-08-04 16:03:46 +02:00
Alexis Gousseau
ed7ed6f55e
feat: ✨ add claim mail templates crud 2025-08-04 16:03:46 +02:00
Alexis Gousseau
7df0fe4634
feat: ✨ add claim subjects crud 2025-08-04 16:03:45 +02:00
Alexis Gousseau
ced124b60e
feat: ✨ add claims bo interface 2025-08-04 16:03:38 +02:00
Alexis Gousseau
187b96fbaf
feat: ✅ add claims api route and tests for V4 2025-07-28 09:08:01 +02:00
Alexis Gousseau
a3a84446f8
feat: 🗃️ add claims tables, seeder and relations 2025-07-28 09:07:54 +02:00
Vincent BRIDIER
af090bbef7 Merge branch 'update' into 'develop'
chore: 🔖 release v4.1.0

Closes #41

See merge request ldc-web/projets/crm!33
2025-07-25 12:54:21 +00:00
Vincent BRIDIER
a269295a79 Merge branch 'saml' into 'develop'
feat: 🛂 change BO Auth for SAML #42

Closes #42

See merge request ldc-web/projets/crm!34
2025-07-25 12:33:00 +00:00
Alexis Gousseau
c828952e7b
feat: 🛂 change BO Auth for SAML #42 2025-07-25 11:41:52 +02:00
Alexis Gousseau
6a60f00fbe
chore: 🔖 release v4.1.0 2025-07-25 09:46:12 +02:00
Alexis Gousseau
7cfe36b40f
feat: ✨ add answers filter on single optins 2025-07-25 09:46:11 +02:00
Alexis Gousseau
89609454ad
feat: ✨ segmentation optins with answers #41 2025-07-25 09:44:40 +02:00
Karl
b178539626 Merge branch 'develop' into trunk 2025-06-04 09:01:00 +02:00
Karl
688ac1a2e2 fix: 🐛 delete the limit resource after k8s issues 2025-06-04 08:58:30 +02:00
Karl
6fdcdd1a2a Merge branch 'develop' into trunk 2025-06-03 17:13:51 +02:00
karl.muller
f9c19f2334 Merge branch 'issue-24' into 'develop'
fix: 🔥 remove double call of methods on store service #24

Closes #24

See merge request ldc-web/projets/crm!31
2025-06-03 15:13:32 +00:00
Alexis Gousseau
64d4397c0c fix: 🔥 remove double call of methods on store service #24 2025-06-03 11:23:48 +02:00
Karl Muller
0f1f258706 Merge branch 'develop' into trunk 2025-04-28 09:51:25 +00:00
Karl Muller
20546b8625 Merge branch 'issue-38' into develop 2025-04-28 09:51:12 +00:00
Karl Muller
c74935eb99 fix: delete the favicon copy 2025-04-28 09:50:42 +00:00
Karl Muller
7eeeb54ccd Merge branch 'develop' into trunk 2025-04-28 09:27:33 +00:00
karl.muller
699d9acb07 Merge branch 'issue-38' into 'develop'
feat: add Docker build multistage

See merge request ldc-web/projets/crm!30
2025-04-28 09:27:07 +00:00
Karl Muller
b494c36849 feat: add Docker build multistage 2025-04-28 09:01:28 +00:00
Vincent BRIDIER
1ea25ed3c7
Merge branch 'trunk' 2025-04-23 11:59:47 +02:00
Vincent BRIDIER
dd3a4b53c2
Merge branch 'develop' into trunk 2025-04-23 11:27:18 +02:00
Vincent BRIDIER
074322aca5
fix: Add missing var for Admin Secure 2025-04-23 11:27:03 +02:00
Vincent BRIDIER
bddd622895
Merge branch 'develop' into trunk 2025-04-23 11:10:24 +02:00
Vincent BRIDIER
5d3896c261 Merge branch 'tests' into 'develop'
fix: 🎨 improve methods prototype

Closes #37

See merge request ldc-web/projets/crm!29
2025-04-23 09:09:35 +00:00
Vincent BRIDIER
48c6b1feaf
Merge branch 'develop' into trunk 2025-04-23 11:09:04 +02:00
Vincent BRIDIER
8be2809dc6
fix: ✅ Fix Docker PHP version 2025-04-23 11:08:54 +02:00
Vincent BRIDIER
8949a22b5f
Merge branch 'develop' into trunk 2025-04-23 10:58:00 +02:00
Vincent BRIDIER
683bc1c8f3
feat: Update manifest for DB vars 2025-04-23 10:57:51 +02:00
Alexis Gousseau
6ca2fe1779 fix: 🎨 improve methods prototype 2025-04-23 10:57:33 +02:00
Vincent BRIDIER
a9f28657e5
Merge branch 'develop' into trunk 2025-04-23 10:52:48 +02:00
Vincent BRIDIER
acef67939a
feat: ✅ Update customer_logins last_connection field for unit tests with MySQL 8 2025-04-23 10:40:26 +02:00
Vincent BRIDIER
b340a252ff
fix: ✅ Fix Unit tests with Carbon major update to v3.* 2025-04-23 10:10:37 +02:00
Vincent BRIDIER
88531b4906 Merge branch 'issue-1-old-crm-files' into 'develop'
chore: remove files related to old crm migrations

Closes #1

See merge request ldc-web/projets/crm!28
2025-02-03 16:25:07 +00:00
noah.lancien
b472ab94d8 chore: remove files related to old crm migrations 2025-02-03 16:25:07 +00:00
Vincent BRIDIER
d9f44df691 Merge branch 'hashcode' into 'develop'
feat: ✨ add hashcode column of single optins in index page #31

Closes #31

See merge request ldc-web/projets/crm!24
2025-01-16 15:17:39 +00:00
Alexis Gousseau
7d2e072c9e fix: 💬 update some plural words 2025-01-16 15:17:23 +00:00
Alexis Gousseau
9e3e855485 feat: ✨ add hashcode column of single optins in index page #31 2025-01-16 15:17:23 +00:00
Vincent BRIDIER
7b3193fa82 Merge branch 'packages' into 'develop'
chore: ⬆️ bump project to laravel 11 & php8.3 #8 #14

Closes #8 and #14

See merge request ldc-web/projets/crm!23
2025-01-16 15:17:19 +00:00
Alexis Gousseau
becc1234d9 chore: ⬆️ bump project to laravel 11 & php8.3 #8 #14 2025-01-16 16:11:05 +01:00
Vincent BRIDIER
ce931c6dcf Merge branch 'trunk' into 'master'
Trunk

See merge request ldc-web/projets/crm!27
2025-01-13 15:06:32 +00:00
Noah Lancien
5778c7aa02 Merge branch 'develop' into trunk 2025-01-13 12:41:45 +01:00
Noah Lancien
9c835ff767 ci: Added 'concurrencyPolicy: Forbid' in cron jobs 2025-01-13 12:41:04 +01:00
Noah Lancien
46787d2677 fix: composer update to fix CVE 2025-01-13 12:41:04 +01:00
Thibault Raquet
f23aa94101 Merge branch 'develop' into 'trunk'
Develop

See merge request ldc-web/projets/crm!26
2025-01-13 10:58:16 +00:00
Thibault Raquet
760e68737b Merge branch 'trunk' into 'develop'
# Conflicts:
#   manifest.yaml
2025-01-13 10:55:32 +00:00
Thibault Raquet
7b5b7d5ada Merge branch 'add-side-car-container' into 'develop'
add a side car container instead of queue worker

Closes #36

See merge request ldc-web/projets/crm!25
2025-01-13 10:48:25 +00:00
thibault.raquet
8fae9e9e4c add a side car container instead of queue worker 2025-01-13 11:40:15 +01:00
Noah Lancien
36997e6a28 Merge branch 'develop' into trunk 2024-10-22 11:27:51 +02:00
Noah Lancien
b0e381768a chore: bump to 4.0.8 2024-10-22 11:10:55 +02:00
Vincent BRIDIER
44d60527b8
Merge branch 'trunk' 2024-10-18 14:53:27 +02:00
Vincent BRIDIER
132ec57bde
Merge branch 'develop' into trunk 2024-10-18 14:53:16 +02:00
Vincent BRIDIER
5922dbf143
fix: LogLevel Enum type 2024-10-18 14:53:02 +02:00
Vincent BRIDIER
680afd45f3
Merge branch 'develop' into trunk 2024-10-15 14:24:57 +02:00
Vincent BRIDIER
0939ae3cca
feat: Clean bump to 4.0.6 2024-10-15 14:24:49 +02:00
Vincent BRIDIER
2a7cd8bb9f
Merge branch 'develop' into trunk 2024-10-15 14:13:39 +02:00
Vincent BRIDIER
f528c6a5f8
fix: Remove Laravel deps call in baseEnum trait 2024-10-15 14:10:04 +02:00
Vincent BRIDIER
bbeef29646
Merge branch 'develop' into trunk 2024-10-14 16:14:43 +02:00
Vincent BRIDIER
ea360d9263 Merge branch 'vanilla-php' into 'develop'
refactor: ♻️ update enum trait toLabels method

See merge request ldc-web/projets/crm!22
2024-10-14 14:14:26 +00:00
Alexis Gousseau
ff4167aa5a
refactor: ♻ update enum trait toLabels method 2024-10-14 16:13:23 +02:00
Vincent BRIDIER
d57e1c55af
Merge branch 'develop' into trunk 2024-10-14 14:48:35 +02:00
Alexis Gousseau
a571775f33
fix: Update methods in vanilla php #35 2024-10-14 14:45:52 +02:00
Vincent BRIDIER
8814ca0a46
Merge branch 'develop' into trunk 2024-10-10 14:59:45 +02:00
Vincent BRIDIER
076398d1c0 Merge branch 'issue-35' into 'develop'
refactor: ♻️ replace laravel methods by vanilla php #35

Closes #35

See merge request ldc-web/projets/crm!21
2024-10-10 12:59:17 +00:00
Alexis Gousseau
996a0594a7 refactor: ♻️ replace laravel methods by vanilla php #35 2024-10-10 13:58:01 +02:00
Vincent BRIDIER
cbbb389a47
Merge branch 'trunk' 2024-09-11 17:04:39 +02:00
Vincent BRIDIER
0ff4de2b1f
Merge branch 'develop' into trunk 2024-09-11 17:04:25 +02:00
Vincent BRIDIER
9eaccbb15a Merge branch 'fix-postcode' into 'develop'
fix: 🐛 update sanitizer for postal code

See merge request ldc-web/projets/crm!20
2024-09-11 15:04:08 +00:00
Alexis Gousseau
2f860b9a42 fix: 🐛 update sanitizer for postal code 2024-09-11 16:32:47 +02:00
Vincent BRIDIER
f56ef0999e
Merge branch 'trunk' 2024-09-09 15:46:33 +02:00
Vincent BRIDIER
9c6842bc56
Merge branch 'develop' into trunk 2024-09-09 15:29:26 +02:00
Vincent BRIDIER
9183822b72 Merge branch 'ts-2089' into 'develop'
fix: 🏷️ add Netherlands to the valid countries

See merge request ldc-web/projets/crm!19
2024-09-09 13:28:56 +00:00
Alexis Gousseau
031d2b25b5 fix: 🚨 resolve some phpcs errors 2024-09-09 15:26:35 +02:00
Alexis Gousseau
96ea1483f9 fix: 🏷️ add Netherlands to the valid countries 2024-09-09 15:15:42 +02:00
Noah Lancien
0fd22a9a82 Merge branch 'trunk' 2024-08-12 19:26:28 +02:00
Noah Lancien
e422d5dfd4 Merge branch 'develop' into trunk 2024-08-12 19:24:25 +02:00
noah.lancien
21c839f90d Merge branch 'fix-answers-erasing' into 'develop'
fix: Replaced sync by detach/attach in answers store/update

See merge request ldc-web/projets/crm!18
2024-08-12 17:23:55 +00:00
Noah Lancien
e6135ab857 fix: Replaced sync by detach/attach in answers store/update 2024-08-12 19:04:17 +02:00
Noah Lancien
391d99528c Merge branch 'develop' into trunk 2024-08-06 11:58:56 +02:00
noah.lancien
0300fada45 Merge branch 'include-path' into 'develop'
fix: 🐛 update include path in answers.create

Closes #34

See merge request ldc-web/projets/crm!17
2024-08-06 09:58:28 +00:00
Alexis Gousseau
5f3d03fd96 fix: 🐛 update include path in answers.create 2024-08-06 11:55:11 +02:00
Noah Lancien
05f0e2c229 Merge branch 'trunk' 2024-08-05 17:04:59 +02:00
Noah Lancien
d504821bea Merge branch 'develop' into trunk 2024-08-05 16:46:28 +02:00
noah.lancien
a53acba571 Merge branch 'breadcrumbs' into 'develop'
fix: 🐛 update breadcrumbs include in blades files

See merge request ldc-web/projets/crm!16
2024-08-05 14:46:10 +00:00
Alexis Gousseau
60cff329fd fix: 🐛 update breadcrumbs include in blades files 2024-08-05 14:48:59 +02:00
Noah Lancien
cf2f6bbf76 Merge branch 'trunk' 2024-08-05 13:11:04 +02:00
Noah Lancien
2bdd7f639b Merge branch 'develop' into trunk 2024-08-05 13:10:30 +02:00
noah.lancien
3c1958ea75 Merge branch 'answers' into 'develop'
fix: 🐛 update the saving of answers

Closes #25

See merge request ldc-web/projets/crm!15
2024-08-05 11:07:52 +00:00
Alexis Gousseau
a6639cb87e fix: 🐛 update the saving of answers 2024-08-05 12:43:26 +02:00
Vincent BRIDIER
394d558cf1
Merge branch 'trunk' 2024-07-11 11:59:18 +02:00
Vincent BRIDIER
513e0c25a4
Merge branch 'develop' into trunk 2024-07-10 16:10:16 +02:00
Vincent BRIDIER
186a271540
Make CRM compliant with Traefik + K8s 2024-07-10 16:09:58 +02:00
Thibault Raquet
d60aa2c35c Merge branch 'trunk' into 'master'
Trunk

See merge request ldc-web/projets/crm!14
2024-06-28 14:32:41 +00:00
Thibault Raquet
702504c539 Merge branch 'develop' into 'trunk'
Update manifest.yaml

See merge request ldc-web/projets/crm!13
2024-06-28 14:32:17 +00:00
Thibault Raquet
12fbf8d67f Update manifest.yaml 2024-06-28 14:30:26 +00:00
Vincent BRIDIER
fe254722b0
Merge branch 'trunk' 2024-06-28 16:09:01 +02:00
Vincent BRIDIER
095c420191
Merge branch 'develop' into trunk 2024-06-28 16:08:53 +02:00
Vincent BRIDIER
932a5bb84e
Update for app domain change 2024-06-28 16:08:44 +02:00
Vincent BRIDIER
3b11316017
Merge branch 'trunk' 2024-06-28 15:03:25 +02:00
Vincent BRIDIER
c9fcb85fff
Merge branch 'develop' into trunk 2024-06-28 15:03:07 +02:00
Vincent BRIDIER
fdb5ddabd7
Migration vers new K8S Cluster 2024-06-28 14:47:08 +02:00
Vincent BRIDIER
fd0f14fc9c fix queue worker manifest 2024-06-07 13:16:47 +00:00
Vincent BRIDIER
2b494c7324 Fix preStop Manifest 2024-06-07 13:04:52 +00:00
Jérémy Munsch
ae4da57006
Merge branch 'develop' into trunk 2024-06-05 13:14:24 +02:00
Jérémy Munsch
13bc1d9ce5
force redeploy 2024-06-05 13:14:19 +02:00
Jérémy Munsch
78d1ab42d4
Merge branch 'develop' into trunk 2024-06-05 13:02:08 +02:00
Jérémy Munsch
efc930a2db
chore: 🧱 manifest.yaml: fix preStop typo 2024-06-05 13:02:04 +02:00
Jérémy Munsch
7f7d9d68f2
Merge branch 'develop' into trunk 2024-06-05 12:20:17 +02:00
Jérémy Munsch
cd85772884
chore: manifest.yaml lifecycle test 2024-06-05 12:20:10 +02:00
Jérémy Munsch
8100b8ccf0
Merge branch 'develop' into trunk 2024-06-05 11:23:26 +02:00
Jérémy Munsch
02f515629e
fix: 🧱 manifest.yaml: PreStop was not contained in lifecycle 2024-06-05 11:23:19 +02:00
Jérémy Munsch
cd2af65e8a
Merge branch 'develop' into trunk 2024-06-05 11:16:49 +02:00
Jérémy Munsch
5198fe6264
chore: 🧱 manifest.yaml: queue-worker-deployment, add PreStop command 2024-06-05 11:16:40 +02:00
Jérémy Munsch
282ebffbb8
Merge branch 'develop' into trunk 2024-06-05 10:58:55 +02:00
Jérémy Munsch
e57c36ca8b
chore: 🧱 k8s-queues.sh: fix stopQueues sleep timer 2024-06-05 10:58:24 +02:00
Jérémy Munsch
1e1b6be8ea
chore: add resource limit to manifest.yaml 2024-06-05 10:56:17 +02:00
Jérémy Munsch
0406c5c97f
Merge branch 'develop' into trunk 2024-06-05 09:49:20 +02:00
Jérémy Munsch
71ccade4ad Update k8s-queues.sh, replace with BASH EXIT internal signal 2024-06-05 07:48:05 +00:00
Vincent BRIDIER
7cdd5df10a
Merge branch 'develop' into trunk 2023-12-11 17:09:50 +01:00
Vincent BRIDIER
04f337b21d
Add missing queue connection var into manifest 2023-12-11 17:09:41 +01:00
Vincent BRIDIER
ef28c99ed3
Merge branch 'develop' into trunk 2023-12-11 16:56:41 +01:00
Vincent BRIDIER
b59dcf576c
Fix Queue Worker args in manifest 2023-12-11 16:56:33 +01:00
Vincent BRIDIER
7000243424
Merge branch 'develop' into trunk 2023-12-11 16:30:52 +01:00
Vincent BRIDIER
6adff1d5a5
Fix Queue Worker container name 2023-12-11 16:30:42 +01:00
Vincent BRIDIER
87273397b2
Merge branch 'develop' into trunk 2023-12-11 15:57:55 +01:00
Vincent BRIDIER
dcf222e1a2
Add K8S config for queues 2023-12-11 15:57:36 +01:00
Vincent BRIDIER
75e65e7ebe
Update Manifest Cron ImagePolicy 2023-12-11 12:16:21 +01:00
Vincent BRIDIER
4e57bfdb15
Update Dockerfile 2023-12-11 12:15:57 +01:00
Jérémy Munsch
9ac4da9a5c
add /bootstrap to gitignore 2023-10-09 11:45:25 +02:00
Jérémy Munsch
974386cc7a
Merge branch 'develop' into trunk 2023-10-09 11:04:45 +02:00
Jérémy Munsch
b1071c57de
add missing uwil/k8s-laravel 2023-10-09 11:04:33 +02:00
Jérémy Munsch
255c5f245b
Merge branch 'develop' into trunk 2023-10-09 10:45:19 +02:00
Jérémy Munsch
f11ed1dc17
Fix theme selector causing CSP issue 2023-10-09 10:45:00 +02:00
Jérémy Munsch
017fcec47a
Merge branch 'develop' into trunk 2023-10-06 16:56:44 +02:00
Jérémy Munsch
a3cf5f0ee2
add Manifest 2023-10-06 16:56:21 +02:00
Jérémy Munsch
72f44ef3a3
Merge branch 'develop' into trunk 2023-10-06 16:45:26 +02:00
Jérémy Munsch
6fa57cd878 Merge branch 'apiV4' into 'develop'
Crm V4

Closes #19 and #20

See merge request ldc-web/projets/crm!12
2023-10-06 14:42:56 +00:00
Jérémy Munsch
23c9efdb22
phpcs 2023-10-06 16:37:31 +02:00
Jérémy Munsch
e96da4a22a
Hudge bump to LaravelBaseProject 2023-10-06 16:37:30 +02:00
Alexis Gousseau
303834bbca
Crm V4
- Make all tests pass Client (V4),CRMV2,CRMV4
- Simplify client submodule path
- Move App_url to .env.testing
2023-10-06 16:37:29 +02:00
Alexis Gousseau
782a030ae3
bump laravel 10 - issue Closes #19 2023-10-06 16:37:29 +02:00
Alexis Gousseau
d07760fa95
Remove crm v1 - issue Closes #20 2023-10-06 16:37:27 +02:00
Alexis Gousseau
764d0b2043
bump laravel 10 - issue Closes #19 2023-10-06 16:33:22 +02:00
Alexis Gousseau
e4b5eed7e4
Remove crm v1 - issue Closes #20 2023-10-06 16:33:21 +02:00
Vincent BRIDIER
74aec7bf9f Merge branch 'issue-9' into 'develop'
Issue 9

Closes #9

See merge request ldc-web/projets/crm!10
2023-08-31 13:28:53 +00:00
sebastien.hattry
c0f579eddb Issue 9 2023-08-31 13:28:52 +00:00
Jérémy Munsch
6e548a7823
Merge branch 'trunk' 2023-07-06 11:21:30 +02:00
Jérémy Munsch
d02f2df5b3
Merge branch 'develop' into trunk 2023-07-06 11:07:32 +02:00
Vincent BRIDIER
7ee0a472e3 Merge branch 'add-sentry' into 'develop'
Remove kwaadpepper Handler and add sentry

Closes #5

See merge request ldc-web/projets/crm!8
2023-07-06 09:03:31 +00:00
Vincent BRIDIER
8b1ec125cc Merge branch 'issue-6' into 'develop'
fix nonce script - issue #6

See merge request ldc-web/projets/crm!9
2023-07-06 09:02:47 +00:00
Alexis Gousseau
3f931b569e fix logout event in the header 2023-07-06 10:59:37 +02:00
Jérémy Munsch
b34c06a32f
Closes #5 Remove kwaadpepper Handler and add sentry 2023-07-06 10:52:02 +02:00
Alexis Gousseau
fa701a05c4 fix nonce script - issue #6 2023-07-06 09:50:27 +02:00
Vincent BRIDIER
1974969ccc
Merge branch 'trunk' 2023-07-05 17:05:03 +02:00
Vincent BRIDIER
391b76d75a
Merge branch 'develop' into trunk 2023-07-05 17:04:46 +02:00
Vincent BRIDIER
332ded505d Merge branch 'issue-2' into 'develop'
Issue 2 Les carbon format() deviennent isoFormat()

Closes #2 and #3

See merge request ldc-web/projets/crm!6
2023-07-05 15:00:45 +00:00
Vincent BRIDIER
dda5cc3177 Merge branch 'single-optins' into 'develop'
Single optins

See merge request ldc-web/projets/crm!5
2023-07-05 14:57:45 +00:00
Jérémy Munsch
ac060ba167
Fixes #2 use isoFormat instead of format
(cherry picked from commit ca5419a992d0e1838d1030db9469edbebc20d995)
2023-06-30 15:09:50 +02:00
Jérémy Munsch
ebe8d8daa3
Fixes #3 Push empty line if no CSV question to export for a brand
(cherry picked from commit bf5477d15c56e33fe980dc67cbc10a0b18f58169)
2023-06-30 15:04:50 +02:00
Jérémy Munsch
cb65aba6c9
Update artisan user:create command
(cherry picked from commit e6cf237fa4ba116b1cb57a496a2fee00ecea6f97)
2023-06-30 15:04:45 +02:00
Jérémy Munsch
935a742698
remove docker build from gitlab-ci 2023-06-30 15:02:46 +02:00
Jérémy Munsch
df44247ede
Add some missing files 2023-06-30 15:02:46 +02:00
Jérémy Munsch
b321f7ce4c
Add custom CI 2023-06-30 15:02:46 +02:00
Jérémy Munsch
df61723aae
Client bumped to 3.0.6 2023-06-30 15:02:45 +02:00
Jérémy Munsch
454f310316
Update test coverage 2023-06-30 15:02:45 +02:00
Jérémy Munsch
78a166af00
Add Api Remove single optin using hashcode 2023-06-30 14:59:48 +02:00
Jérémy Munsch
020fd8b092
Fix testApiAuthenticateCustomerShouldFailIfCustomerNotActivated having random results 2023-06-30 14:59:48 +02:00
Jérémy Munsch
6be77f21c6
force add .vscode/settings.json 2023-06-30 14:59:47 +02:00
Jérémy Munsch
97f9f49f2e
Evolution Set hashcode as PK on single optins 2023-06-30 14:59:47 +02:00
Jérémy Munsch
b65ad8c19e
Correction typages 2023-06-30 14:59:07 +02:00
Jérémy Munsch
d930a9e13c
Fix handler inconsistence 2023-06-30 14:59:06 +02:00
Jérémy Munsch
dac29a03d3
Composer serve add quiet for tests 2023-06-30 14:59:06 +02:00
Jérémy Munsch
a256cb2986
enhance cutomer info uuid column type 2023-06-30 14:59:06 +02:00
Jérémy Munsch
5c770eb9d5
phpcs Change object to full class path in docblock comments 2023-06-30 14:59:00 +02:00
Vincent BRIDIER
3a0044afbc Merge branch 'crud' into 'develop'
remove crud-policies

See merge request ldc-web/projets/crm!3
2023-06-30 12:42:06 +00:00
Vincent BRIDIER
b31ebdcf79
Add missing backslashes on docblocks 2023-06-30 12:51:21 +02:00
Alexis Gousseau
1b8951fd0d run tests with coverage 2023-06-26 12:50:29 +02:00
Alexis Gousseau
721b519858 fix bootstrap paginator 2023-06-26 12:49:47 +02:00
Alexis Gousseau
8ac9932cc5 add Secure Header + add nonce on all script balise + update ViewServiceProvider 2023-06-26 12:22:00 +02:00
Alexis Gousseau
214ff79085 remove cdn print-js to import it 2023-06-26 12:16:17 +02:00
Alexis Gousseau
fa265018f8 remove cdn chart.js to import it 2023-06-26 12:01:36 +02:00
Alexis Gousseau
98cf4030e3 rename compile files 2023-06-26 11:59:20 +02:00
Alexis Gousseau
08432a9c2a remove mix manifest 2023-06-26 10:30:09 +02:00
Alexis Gousseau
2d2aae3834 update validations ISO crud policies 2023-06-26 10:13:19 +02:00
Alexis Gousseau
823c4f1e3c remove unused aliases 2023-06-26 09:52:35 +02:00
Alexis Gousseau
ad8e3447b4 remove models list in sidenav 2023-06-26 09:30:49 +02:00
Alexis Gousseau
d9b9e3c188 fix search belongs to many dropdown route for brands 2023-06-26 09:25:41 +02:00
Alexis Gousseau
77fe7bb082 fix policies auto discover 2023-06-26 09:24:42 +02:00
Alexis Gousseau
29d511eb7d update absolute paths in blade 2023-06-26 09:24:29 +02:00
Alexis Gousseau
3feea553dc update dockblocs 2023-06-23 17:18:59 +02:00
Jérémy Munsch
b5f6faa995 Merge branch 'fix-tests' into 'develop'
fix tests stats

See merge request ldc-web/projets/crm!4
2023-06-22 14:50:14 +00:00
Jérémy Munsch
c5182b65b4
Merge branch 'hotfix' 2022-08-31 10:28:07 +02:00
1153 changed files with 178192 additions and 131572 deletions

5
.browserslistrc Normal file
View file

@ -0,0 +1,5 @@
# Browsers that we support
last 3 version
> 0.3%
not dead # no browsers without security updates

2
.dockerignore Normal file
View file

@ -0,0 +1,2 @@
.git
*.md

View file

@ -3,9 +3,9 @@ root = true
[*] [*]
charset = utf-8 charset = utf-8
end_of_line = lf end_of_line = lf
insert_final_newline = true
indent_style = space
indent_size = 4 indent_size = 4
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true trim_trailing_whitespace = true
[*.md] [*.md]
@ -14,8 +14,8 @@ trim_trailing_whitespace = false
[*.{yml,yaml}] [*.{yml,yaml}]
indent_size = 2 indent_size = 2
[*.vue]
indent_size = 2
[docker-compose.yml] [docker-compose.yml]
indent_size = 4 indent_size = 4
[*.vue]
indent_size = 2

View file

@ -14,18 +14,9 @@ DB_DATABASE=laravel
DB_USERNAME=root DB_USERNAME=root
DB_PASSWORD= DB_PASSWORD=
DB_CONNECTION_OLD=mysql
DB_HOST_OLD=127.0.0.1
DB_PORT_OLD=3306
DB_DATABASE_OLD=ldccrm_12_2020
DB_USERNAME_OLD=root
DB_PASSWORD_OLD=
MIGRATIONPAGINATION=20000
BROADCAST_DRIVER=log BROADCAST_DRIVER=log
CACHE_DRIVER=file CACHE_DRIVER=file
QUEUE_CONNECTION=sync QUEUE_CONNECTION=database
SESSION_DRIVER=file SESSION_DRIVER=file
SESSION_LIFETIME=120 SESSION_LIFETIME=120
@ -34,23 +25,27 @@ REDIS_PASSWORD=null
REDIS_PORT=6379 REDIS_PORT=6379
MAIL_MAILER=smtp MAIL_MAILER=smtp
MAIL_HOST=smtp.mailtrap.io MAIL_HOST=dev-mailpit
MAIL_PORT=2525 MAIL_PORT=1025
MAIL_USERNAME=null MAIL_USERNAME=null
MAIL_PASSWORD=null MAIL_PASSWORD=null
MAIL_ENCRYPTION=null MAIL_ENCRYPTION=null
MAIL_FROM_ADDRESS=null MAIL_FROM_ADDRESS=null
MAIL_FROM_NAME="${APP_NAME}" MAIL_FROM_NAME="${APP_NAME}"
MAIL_DIGITAL_SERVICE=""
AWS_ACCESS_KEY_ID= # Sentry
AWS_SECRET_ACCESS_KEY= SENTRY_LARAVEL_DSN=
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=
PUSHER_APP_ID= # UWIL ADMIN SECURE
PUSHER_APP_KEY= # - LIST SEPARATED WITH ',' (Required)
PUSHER_APP_SECRET= UWIL_ADMIN_SECURE_CIDR_LIST=""
PUSHER_APP_CLUSTER=mt1 # - LIST SEPARATED WITH ',' (Optional)
UWIL_ADMIN_SECURE_CIDR_PROJECT_ALLOWED_LIST=""
MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}" # SAML
MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}" SAML2_DEFAULT_TENANT_UUID=
SAML2_SP_CERT_x509=
SAML2_SP_CERT_PRIVATEKEY=
SAML2_CONTACT_TECHNICAL_EMAIL=
SAML2_CONTACT_SUPPORT_EMAIL=

View file

@ -14,15 +14,6 @@ DB_DATABASE=testing_ldccrm_12_2020
DB_USERNAME=root DB_USERNAME=root
DB_PASSWORD=DBPASSWORD DB_PASSWORD=DBPASSWORD
DB_CONNECTION_OLD=mysql
DB_HOST_OLD=127.0.0.1
DB_PORT_OLD=3306
DB_DATABASE_OLD="ldc-crm"
DB_USERNAME_OLD=root
DB_PASSWORD_OLD=
MIGRATIONPAGINATION=20000
BROADCAST_DRIVER=log BROADCAST_DRIVER=log
CACHE_DRIVER=file CACHE_DRIVER=file
QUEUE_CONNECTION=database QUEUE_CONNECTION=database

View file

@ -37,5 +37,19 @@
"semi": ["error", "always"], "semi": ["error", "always"],
"vue/script-indent": ["error", 2, { "baseIndent": 0, "switchCase": 1 }], "vue/script-indent": ["error", 2, { "baseIndent": 0, "switchCase": 1 }],
"vue/one-component-per-file": "off" "vue/one-component-per-file": "off"
},
"settings": {
"import/resolver": {
"node": {
"extensions": [".js", ".vue", ".ts", ".d.ts"]
},
"alias": {
"extensions": [".vue", ".js", ".ts", ".scss", ".d.ts"],
"map": [
["~", "./resources/assets"],
["@", "./node_modules"]
]
}
}
} }
} }

14
.gitattributes vendored
View file

@ -1,5 +1,11 @@
* text=auto * text=auto eol=lf
*.css linguist-vendored
*.scss linguist-vendored *.blade.php diff=html
*.js linguist-vendored *.css diff=css
*.html diff=html
*.md diff=markdown
*.php diff=php
/.github export-ignore
CHANGELOG.md export-ignore CHANGELOG.md export-ignore
.styleci.yml export-ignore

41
.gitignore vendored
View file

@ -1,26 +1,31 @@
/node_modules
/public/hot
/public/storage
/public/build
/public/medias
/public/uploads
/public/cache
/public/sitemap.xml
/storage/*.key
/vendor
.vscode/
.idea/
.env .env
.env.testing .env.testing
.env.backup .env.backup
.env.production
.phpunit.result.cache .phpunit.result.cache
.stylelintcache
/.fleet
/.idea
/.phpunit.cache
/.vscode
/node_modules
/bootstrap
/public/build
/public/cache
/public/hot
/public/sitemap.xml
/public/storage
/storage/*.key
/vendor
auth.json
docker-compose.override.yml
Homestead.json Homestead.json
Homestead.yaml Homestead.yaml
npm-debug.log npm-debug.log
yarn-error.log yarn-error.log
storage/crm_migration.csv .npmrc
MIG/errors_sql.txt .phpdoc
public/js .zshrc
public/css .npm/
public/assets .composer/
auth.json tests/coverage

23
.gitlab-ci.yml Normal file
View file

@ -0,0 +1,23 @@
include:
- project: "ldc-web/shared-ci"
ref: "main"
file: "lint/lint.yml"
- project: "ldc-web/shared-ci"
ref: "main"
file: "security/security.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"
stages:
- lint
- security
- build
- test
- deploy

4
.gitmodules vendored
View file

@ -1,3 +1,3 @@
[submodule "clients/V2"] [submodule "client"]
path = clients/V2 path = client
url = git@lab.s-web.fr:ldc-web/projets/crm-client.git url = git@lab.s-web.fr:ldc-web/projets/crm-client.git

3
.husky/commit-msg Normal file
View file

@ -0,0 +1,3 @@
#!/bin/sh
npx --no -- commitlint --config node_modules/@uwil/husky-config/commitlint.config.cjs --edit $1

View file

@ -1,12 +1,16 @@
{ {
"extends": [ "extends": [
"stylelint-config-prettier",
"stylelint-config-standard-scss", "stylelint-config-standard-scss",
"stylelint-config-sass-guidelines", "stylelint-config-sass-guidelines",
"stylelint-config-idiomatic-order" "stylelint-config-idiomatic-order"
], ],
"plugins": ["stylelint-scss"], "plugins": [
"files": ["**/*.css", "**/*.scss"], "stylelint-scss"
],
"files": [
"**/*.css",
"**/*.scss"
],
"ignoreFiles": [ "ignoreFiles": [
"**/vendor/*.css", "**/vendor/*.css",
"**/vendor/**/*.css", "**/vendor/**/*.css",
@ -19,16 +23,14 @@
], ],
"customSyntax": "postcss-scss", "customSyntax": "postcss-scss",
"rules": { "rules": {
"indentation": 4, "declaration-property-value-no-unknown": true,
"string-quotes": "double",
"selector-max-id": null, "selector-max-id": null,
"color-no-invalid-hex": true, "color-no-invalid-hex": true,
"order/properties-alphabetical-order": null, "order/properties-alphabetical-order": null,
"max-nesting-depth": 5, "max-nesting-depth": 5,
"selector-max-compound-selectors": 5, "selector-max-compound-selectors": 5,
"declaration-colon-space-after": "always-single-line",
"function-parentheses-space-inside": "never-single-line",
"declaration-colon-newline-after": null, "declaration-colon-newline-after": null,
"value-list-comma-newline-after": null,
"scss/double-slash-comment-empty-line-before": [ "scss/double-slash-comment-empty-line-before": [
"always", "always",
{ {

20
.vscode/extensions.json vendored Normal file
View file

@ -0,0 +1,20 @@
{
"recommendations": [
"esbenp.prettier-vscode",
"rvest.vs-code-prettier-eslint",
"onecentlin.laravel-extension-pack",
"johnpapa.vscode-peacock",
"xdebug.php-debug",
"neilbrayfield.php-docblocker",
"bmewburn.vscode-intelephense-client",
"valeryanm.vscode-phpsab",
"syler.sass-indented",
"sibiraj-s.vscode-scss-formatter",
"mrmlnc.vscode-scss",
"stylelint.vscode-stylelint",
"vue.vscode-typescript-vue-plugin",
"wscats.vue",
"vue.volar",
"misterj.vue-volar-extention-pack"
]
}

25
.vscode/settings.json vendored Normal file
View file

@ -0,0 +1,25 @@
{
"workbench.colorCustomizations": {
"activityBar.activeBackground": "#8dc952",
"activityBar.activeBorder": "#3d7fc2",
"activityBar.background": "#8dc952",
"activityBar.foreground": "#15202b",
"activityBar.inactiveForeground": "#15202b99",
"activityBarBadge.background": "#3d7fc2",
"activityBarBadge.foreground": "#e7e7e7",
"sash.hoverBorder": "#8dc952",
"statusBar.background": "#73b137",
"statusBar.foreground": "#15202b",
"statusBarItem.hoverBackground": "#5a8a2b",
"statusBarItem.remoteBackground": "#73b137",
"statusBarItem.remoteForeground": "#15202b",
"titleBar.activeBackground": "#73b137",
"titleBar.activeForeground": "#15202b",
"titleBar.inactiveBackground": "#73b13799",
"titleBar.inactiveForeground": "#15202b99",
"commandCenter.border": "#15202b99"
},
"peacock.remoteColor": "#73b137",
"intelephense.environment.phpVersion": "8.5"
}

50
Dockerfile Normal file
View file

@ -0,0 +1,50 @@
ARG PHP_8_5_TAG=latest
FROM composer:2.8 AS vendor
WORKDIR /app
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 node:20.18 AS frontend
ARG GITLAB_PASSWORD
RUN mkdir -p /app/public/build
COPY package*.json tsconfig.json vite.config.ts .eslintrc.json .stylelintrc.json /app/
COPY resources/ /app/resources/
WORKDIR /app
RUN npm config set @ldc-web:registry https://lab.s-web.fr/api/v4/packages/npm/
RUN npm config set -- '//lab.s-web.fr/api/v4/packages/npm/:_authToken' "${GITLAB_PASSWORD}"
RUN npm config set @uwil:registry https://lab.s-web.fr/api/v4/groups/ldc-web/-/packages/npm/
RUN npm config set -- '//lab.s-web.fr/api/v4/groups/ldc-web/-/packages/npm/:_authToken' "${GITLAB_PASSWORD}"
RUN npm ci --no-audit --no-fund --loglevel=error && npm 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 --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 ${USER}

View file

@ -1,69 +0,0 @@
<?php
use phpseclib3\Crypt\Rijndael;
class CPlatosChiffrement
{
private static $cipher = MCRYPT_RIJNDAEL_128; // Algorithme utilisé pour le cryptage des blocs
//private static $key = "yY75y2kLQ"; // Clé de cryptage PROD
private static $key = "123456789"; // Clé de cryptage DEV
private static $mode = 'cbc'; // Mode opératoire (traitement des blocs)
public static function crypt($data)
{
$keyHash = md5(self::$key);
$data = mcrypt_encrypt(self::$cipher, self::getKey($keyHash), $data, self::$mode, self::getIv($keyHash));
return base64_encode($data);
}
public static function decrypt($data)
{
$keyHash = md5(self::$key);
$data = base64_decode($data);
$data = mcrypt_decrypt(self::$cipher, self::getKey($keyHash), $data, self::$mode, self::getIv($keyHash));
return rtrim($data);
}
public static function getIv($keyHash)
{
$iv = substr($keyHash, 0, mcrypt_get_block_size(self::$cipher, self::$mode));
return $iv;
}
public static function getKey($keyHash)
{
$key = substr($keyHash, 0, mcrypt_get_key_size(self::$cipher, self::$mode));
return $key;
}
private function encryptRJ256($key, $iv, $text)
{
$cipher = new Rijndael('cbc');
$cipher->setBlockLength(128);
// keys are null-padded to the closest valid size
// longer than the longest key and it's truncated
$cipher->setKeyLength(128);
$cipher->setKey($key);
// the IV defaults to all-NULLs if not explicitly defined
$cipher->setIV($iv);
$cipher->disablePadding();
$length = strlen($text);
$pad = 32 - ($length % 32);
$text = str_pad($text, $length + $pad, chr(0));
return base64_encode($cipher->encrypt($text));
}
private function decryptRJ256($key, $iv, $text)
{
$cipher = new Rijndael('cbc'); // could use CRYPT_RIJNDAEL_MODE_CBC
$cipher->setBlockLength(128);
// keys are null-padded to the closest valid size
// longer than the longest key and it's truncated
$cipher->setKeyLength(128);
$cipher->setKey($key);
// the IV defaults to all-NULLs if not explicitly defined
$cipher->setIV($iv);
$cipher->disablePadding();
return $cipher->decrypt(base64_decode($text));
}
}

View file

@ -1,96 +0,0 @@
<?php
class Database
{
static $instance;
private static $host;
private static $database;
private static $user;
private static $password;
public function __construct($host, $database, $user, $password)
{
self::$host = $host;
self::$database = $database;
self::$user = $user;
self::$password = $password;
}
public static function Instance()
{
if (!self::$instance) {
try {
self::$instance = new \PDO('mysql:host=' . self::$host . ';dbname=' . self::$database . ';charset=UTF8', self::$user, self::$password, array(\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION));
} catch (\Exception $e) {
die($e->getMessage());
}
}
return self::$instance;
}
public function request($statement, $params = false)
{
try {
$db = self::Instance();
$query = $db->prepare($statement);
if ($params) {
$query->execute($params);
} else {
$query->execute();
}
} catch (\PDOException $e) {
die($e->getMessage());
}
$query->setFetchMode(\PDO::FETCH_ASSOC);
$result = $query->fetch();
$query->closeCursor();
return ($result);
}
public function requestAll($statement, $params = false)
{
try {
$db = self::Instance();
$query = $db->prepare($statement);
if ($params) {
$query->execute($params);
} else {
$query->execute();
}
} catch (\PDOException $e) {
die($e->getMessage());
}
$query->setFetchMode(\PDO::FETCH_ASSOC);
$result = $query->fetchAll();
$query->closeCursor();
return ($result);
}
public function requestPush($statement, $params = false)
{
try {
$db = self::Instance();
$query = $db->prepare($statement);
if ($params)
$query->execute($params);
else
$query->execute();
return true;
} catch (\PDOException $e) {
die($e->getMessage());
}
}
public function requestPushInsert($statement, $params)
{
try {
$db = self::Instance();
$query = $db->prepare($statement);
$query->execute($params);
return $db->lastInsertId();
} catch (\PDOException $e) {
file_put_contents("errors_sql.txt", implode(',', $params) . PHP_EOL, FILE_APPEND);
}
}
}

View file

@ -1,90 +0,0 @@
<?php
ini_set("memory_limit", "-1");
set_time_limit(0);
error_reporting(E_ALL);
require_once 'Db.php';
require_once 'CPlatosChiffrement.class.php';
require_once 'LDCCRM_V1.php';
$db = new Database('localhost', 'ldc-crm', 'ldc-crmadm', 'wai6dahB');
//$db = new Database('192.168.30.3', 'ldc-crm', 'ldc-crmadm', 'wai6dahB');
$apiCrm = new \LDCCRM\ApiV1('http://crm.test', 'giu6OCyG6FuJvM56zFntDcrtDI23GU7WNnkvOk9rPPTOzKArI8IV9nqgEAnsA8wEQUDnKheQ9FDfKUwG');
$siteId = 17;
$siteName = "REGHALAL";
$accounts = $db->requestAll("SELECT *
FROM useraccount
INNER JOIN siteuser su ON useraccount.user_id = su.siteuser_useraccountId
WHERE su.siteuser_siteId = :siteid
AND user_status = 1
AND user_lastUpdateDate >= '2018-03-01'
AND (user_emailCheckStatus <> 0 OR user_emailCheckStatus IS NULL)
AND (user_lastCnxDateHeure IS NOT NULL AND user_lastCnxDateHeure <> '0000-00-00 00:00:00')", ['siteid' => $siteId]);
foreach ($accounts as $account) {
$isSingleOptin = false;
$oldCRMid = $account['user_id'];
$civility = $account['user_civilite'];
$firstname = $account['user_prenom'];
$lastname = $account['user_nom'];
$birthdate = $account['user_dateNaissance'];
$address1 = !empty($account['user_adresse1']) ? $account['user_adresse1'] : null;
$address2 = !empty($account['user_adresse2']) ? $account['user_adresse2'] : null;
$postcode = (!empty($account['user_codePostal']) && $account['user_codePostal'] !== 0 ? $account['user_codePostal'] : null);
$city = !empty($account['user_ville']) ? $account['user_ville'] : null;
$optin = $account['user_inscritNewsletter'];
$haschcode = $account['user_hashcode'];
$status = $account['user_status'];
$created_at = $account['user_creationDate'];
$updated_at = $account['user_lastUpdateDate'];
$email = $account['user_email'];
$password = CPlatosChiffrement::decrypt($account['user_pwd']);
$last_cnx = $account['user_lastCnxDateHeure'];
if (empty($firstname) && empty($lastname))
$isSingleOptin = true;
// Récupération des champs complémentaires
$fields = [['fieldkey' => 'created_at', 'fieldvalue' => $created_at], ['fieldkey' => 'updated_at', 'fieldvalue' => $updated_at], ['fieldkey' => 'last_cnx', 'fieldvalue' => $last_cnx]];
if (!$isSingleOptin) {
$crmUser = $apiCrm->createCustomer($email, $password, $firstname, $lastname, $optin, $birthdate, null, $address1, $address2, $city, $postcode, "France");
if ($crmUser->status === 'OK') {
$newCRMId = $crmUser->user->crm_id;
$activate = $apiCrm->activateCustomer($newCRMId);
if ($activate->status === 'NOK') {
file_put_contents("Export_errors.txt", "OLD CRM ID : $oldCRMid - NEW CRM ID : $newCRMId - ACTIVATE API ERROR : " . $activate->code . " - " . $activate->message . PHP_EOL, FILE_APPEND);
}
} else {
file_put_contents("Export_errors.txt", "OLD CRM ID : $oldCRMid - API ERROR : " . $crmUser->code . " - " . $crmUser->message . PHP_EOL, FILE_APPEND);
continue;
}
} else {
$result = $apiCrm->createSingleOptin($email);
if ($result->status === 'OK') {
$newCRMId = "SINGLEOPTIN";
} else {
file_put_contents("Export_errors.txt", "OLD CRM ID : $oldCRMid - API ERROR : " . $crmUser->code . " - " . $crmUser->message . PHP_EOL, FILE_APPEND);
continue;
}
}
write("$oldCRMid;$newCRMId;$email;" . json_encode($fields) . PHP_EOL, $siteName);
}
function write($text, $siteName)
{
if (!file_exists("Export_$siteName.csv"))
file_put_contents("Export_$siteName.csv", "\xEF\xBB\xBF", FILE_APPEND); // BOM UTF-8
file_put_contents("Export_$siteName.csv", $text, FILE_APPEND);
}

View file

@ -1,9 +0,0 @@
CREATE TABLE `legaulois_migration` (
`id` BIGINT(20) NOT NULL AUTO_INCREMENT,
`user_id` BIGINT(20) NOT NULL DEFAULT '0',
`pwd` VARCHAR(255) NOT NULL DEFAULT '0',
PRIMARY KEY (`id`)
)
COLLATE='utf8_unicode_ci'
ENGINE=InnoDB
AUTO_INCREMENT=0;

View file

@ -1,80 +0,0 @@
<?php
ini_set("memory_limit", "-1");
set_time_limit(0);
error_reporting(E_ALL);
require_once 'Db.php';
$db = new Database('192.168.31.102', 'ldccrm_12_2020', 'ldccrmadm', 'wai6dahB'); // INFOS NOUVELLE CRM
$newBrandId = 2; // BRAND ID de la migration
/**
* @return Generator
*/
$fileData = function() {
$file = fopen(__DIR__ . '/Export_REGHALAL.csv', 'r');
if (!$file)
die('file does not exist or cannot be opened');
while (($line = fgets($file)) !== false) {
yield $line;
}
fclose($file);
};
foreach ($fileData() as $line) {
$isSingleOptin = false;
$datas = explode(";", $line);
$oldCrmId = $datas[0];
$newCrmId = $datas[1];
$email = $datas[2];
$otherFields = json_decode($datas[3]);
if ($newCrmId === 'SINGLEOPTIN')
$isSingleOptin = true;
foreach ($otherFields as $otherField){
// created_at - updated_at - last_cnx ne bougent pas car génériques et db sur nouvelle CRM sinon DB sur nouveau site.
switch($otherField->fieldkey){
case 'created_at':
if (!$isSingleOptin) {
$query = "UPDATE customers SET created_at = :nvalue WHERE id = :newid";
$db->requestPush($query, ['nvalue' => $otherField->fieldvalue, 'newid' => $newCrmId]);
$query = "UPDATE customer_infos SET created_at = :nvalue WHERE customer_id = :newid AND brand_id = :brandid";
$db->requestPush($query, ['nvalue' => $otherField->fieldvalue, 'newid' => $newCrmId, 'brandid' => $newBrandId]);
$query = "UPDATE customer_login SET created_at = :nvalue WHERE customer_id = :newid AND brand_id = :brandid";
$db->requestPush($query, ['nvalue' => $otherField->fieldvalue, 'newid' => $newCrmId, 'brandid' => $newBrandId]);
$query = "UPDATE customer_opt_in_history SET created_at = :nvalue WHERE customer_id = :newid AND brand_id = :brandid";
$db->requestPush($query, ['nvalue' => $otherField->fieldvalue, 'newid' => $newCrmId, 'brandid' => $newBrandId]);
}else{
$query = "UPDATE single_optin SET created_at = :nvalue, updated_at = :nvalue WHERE email = :email AND brand_id = :brandid";
$db->requestPush($query, ['nvalue' => $otherField->fieldvalue, 'email' => $email, 'brandid' => $newBrandId]);
}
break;
case 'updated_at':
if (!$isSingleOptin) {
$query = "UPDATE customers SET updated_at = :nvalue WHERE id = :newid";
$db->requestPush($query, ['nvalue' => $otherField->fieldvalue, 'newid' => $newCrmId]);
$query = "UPDATE customer_infos SET updated_at = :nvalue WHERE customer_id = :newid AND brand_id = :brandid";
$db->requestPush($query, ['nvalue' => $otherField->fieldvalue, 'newid' => $newCrmId, 'brandid' => $newBrandId]);
$query = "UPDATE customer_login SET updated_at = :nvalue WHERE customer_id = :newid AND brand_id = :brandid";
$db->requestPush($query, ['nvalue' => $otherField->fieldvalue, 'newid' => $newCrmId, 'brandid' => $newBrandId]);
$query = "UPDATE customer_opt_in_history SET updated_at = :nvalue WHERE customer_id = :newid AND brand_id = :brandid";
$db->requestPush($query, ['nvalue' => $otherField->fieldvalue, 'newid' => $newCrmId, 'brandid' => $newBrandId]);
}
break;
case 'last_cnx':
if (!$isSingleOptin) {
$query = "UPDATE customer_login SET last_connection = :nvalue WHERE customer_id = :newid AND brand_id = :brandid";
$db->requestPush($query, ['nvalue' => $otherField->fieldvalue, 'newid' => $newCrmId, 'brandid' => $newBrandId]);
}
break;
}
//var_dump($otherField);
}
}

View file

@ -1,89 +0,0 @@
<?php
ini_set("memory_limit", "-1");
set_time_limit(0);
error_reporting(E_ALL);
require_once 'Db.php';
require_once 'CPlatosChiffrement.class.php';
$db = new Database('localhost', 'ldc-crm', 'root', '');
//$db = new Database('192.168.34.2', 'ldc-crm', 'ldc-crmadm', 'wai6dahB');
if (!isset($argv[1]) || !isset($argv[2]))
die('params missing');
$name = $argv[1];
$siteId = $argv[2];
$table = $name."_migration";
$maxdate = date("Y-m-d", strtotime("-36 month", time()));
$db->requestPush("CREATE TABLE IF NOT EXISTS $table (
`id` BIGINT(20) NOT NULL AUTO_INCREMENT,
`user_id` BIGINT(20) NOT NULL DEFAULT '0',
`firstname` VARCHAR(255) NOT NULL DEFAULT '0',
`lastname` VARCHAR(255) NOT NULL DEFAULT '0',
`birthdate` VARCHAR(255) NULL DEFAULT '0',
`address` VARCHAR(255) NULL DEFAULT '0',
`address2` VARCHAR(255) NULL DEFAULT '0',
`city` VARCHAR(255) NULL DEFAULT '0',
`postcode` VARCHAR(255) NULL DEFAULT '0',
`optin` INT(11) NOT NULL DEFAULT '0',
`email` VARCHAR(255) NOT NULL DEFAULT '0',
`pwd` VARCHAR(255) NOT NULL DEFAULT '0',
`created_at` TIMESTAMP NOT NULL,
`updated_at` TIMESTAMP NOT NULL,
`last_cnx` TIMESTAMP NOT NULL,
PRIMARY KEY (`id`)
) COLLATE='utf8_unicode_ci' ENGINE=InnoDB AUTO_INCREMENT=0;");
$getAccounts = function ($skip) use ($db, $siteId, $maxdate){
return $db->requestAll("SELECT *
FROM useraccount
INNER JOIN siteuser su ON useraccount.user_id = su.siteuser_useraccountId
WHERE su.siteuser_siteId = :siteid
AND user_id >= 701697
AND user_status = 1
AND user_lastUpdateDate >= :maxdate
AND (user_emailCheckStatus <> 0 OR user_emailCheckStatus IS NULL)
AND (user_lastCnxDateHeure IS NOT NULL AND user_lastCnxDateHeure <> '0000-00-00 00:00:00')
AND user_lastCnxDateHeure >= :maxdate
AND user_email IS NOT NULL LIMIT 4600 OFFSET ". ($skip * 4600), ['siteid' => $siteId, 'maxdate' => $maxdate]);
};
$k = 0;
while (count($accounts = $getAccounts($k++))) {
$sql = [];
$toto = "";
foreach ($accounts as $account){
$crm_id = $account['user_id'];
$civility = $account['user_civilite'];
$firstname = ucfirst($account['user_prenom']);
$lastname = ucfirst($account['user_nom']);
$birthdate = $account['user_dateNaissance'];
$address1 = !empty($account['user_adresse1']) ? $account['user_adresse1'] : null;
$address2 = !empty($account['user_adresse2']) ? $account['user_adresse2'] : null;
$postcode = (!empty($account['user_codePostal']) && $account['user_codePostal'] !== 0 ? $account['user_codePostal'] : null);
$city = !empty($account['user_ville']) ? $account['user_ville'] : null;
$optin = $account['user_inscritNewsletter'];
$haschcode = $account['user_hashcode'];
$status = $account['user_status'];
$created_at = $account['user_creationDate'];
$updated_at = $account['user_lastUpdateDate'];
$email = strtolower($account['user_email']);
//$password = CPlatosChiffrement::decrypt($account['user_pwd']);
$password = "toto";
$last_cnx = $account['user_lastCnxDateHeure'];
$datas = [
$crm_id,
$firstname,
$lastname,
$birthdate,
$address1,
$address2,
$city,
$postcode,
$optin,
$email,
$password,
$created_at,
$updated_at,
$last_cnx
];
$toto = "(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
$db->requestPushInsert("INSERT INTO $table (user_id, firstname, lastname, birthdate, address, address2, city, postcode, optin, email, pwd, created_at, updated_at, last_cnx) VALUES $toto", $datas);
}
}

View file

@ -1,52 +1,10 @@
# Comment lancer les tests unitaires ? # CRM
### Attention, si vous utilisez juste un .env à la place d'un .env.testing votre base de donnée de developpement sera effacée à chaque lancement des tests unitaires ## Documentation
https://cdlfr.sharepoint.com/:b:/r/sites/LDCWEB/Documents%20partages/Documentations/Projets/CRM/FICHE%20FONCTIONNELLE%20OUTIL%20R%C3%89CLAMATIONS%20CONSO.pdf?csf=1&web=1&e=qktL4V
Etapes : ## Tests unitaire
- cp .env.testing.example .env.testing (spécial tests unitaires)
- php artisan key:generate
- parameter les BDD dans le .env.testing
- php artisan config:cache
- git submodule init
- git submodule update (récupérer le client)
- composer install
- service start mysql (votre base de donnée accessible sur 127.0.0.1:3306)
- chmod u+x ./test.sh
- php artisan optimize
- composer run test
Config Supervisor 2 Fichiers : ### Comment lancer les tests unitaires ?
`ldccrm-worker.conf` [Suivre le wiki dédié au tests unitaires](https://lab.s-web.fr/ldc-web/projets/crm/-/wikis/Tests-unitaires)
**Pour les jobs en général**
[program:ldccrm-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /u1/DATA01/webldc/Apps/ldccrm/ldccrm-12-2020/srcs/artisan queue:work --sleep=3 --tries=3 --max-time=3600
autostart=true
autorestart=true
startasgroup=true
killasgroup=true
user=www-data
umask=077
numprocs=8
redirect_stderr=true
stdout_logfile=/u1/DATA01/webldc/Apps/ldccrm/ldccrm-12-2020/srcs/storage/logs/worker.log
stopwaitsecs=3600
`ldccrm-stats-worker.conf`
**Pour les jobs de stats uniquement**
[program:ldccrm-stats-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /u1/DATA01/webldc/Apps/ldccrm/ldccrm-12-2020/srcs/artisan queue:work --queue=stats --sleep=3 --tries=3 --max-time=3600
autostart=true
autorestart=true
startasgroup=true
killasgroup=true
user=www-data
umask=077
numprocs=1
redirect_stderr=true
stdout_logfile=/u1/DATA01/webldc/Apps/ldccrm/ldccrm-12-2020/srcs/storage/logs/worker.log
stopwaitsecs=3600

View file

@ -35,10 +35,11 @@ class DeleteUnconfirmedCustomers extends Command
* *
* @return integer * @return integer
*/ */
public function handle() public function handle(): int
{ {
/** @var \Illuminate\Database\Eloquent\Collection $customersLogins */ /** @var \Illuminate\Database\Eloquent\Collection $customersLogins */
$customersLogins = CustomerLogin::whereStatus(false) $customersLogins = CustomerLogin::query()
->where('status', false)
->where('created_at', '<', Carbon::now()->subDay(1)) ->where('created_at', '<', Carbon::now()->subDay(1))
->with(['brand', 'customer']) ->with(['brand', 'customer'])
->get(); ->get();

View file

@ -7,18 +7,20 @@ use App\Enums\ApiStatus;
use App\Enums\ArcadiaOrderType; use App\Enums\ArcadiaOrderType;
use App\Enums\Country; use App\Enums\Country;
use App\Enums\QuestionType; use App\Enums\QuestionType;
use App\Exceptions\Api\V2\ApiErrorException; use App\Exceptions\Api\V4\ApiErrorException;
use App\Exceptions\Api\V2\CrmException; use App\Exceptions\Api\V4\CrmException;
use App\Traits\Enums\BaseEnum;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Illuminate\Support\Facades\File; use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
use Illuminate\Support\Str;
/** /**
* @codeCoverageIgnore * @codeCoverageIgnore
*/ */
class GenerateCrmClient extends Command class GenerateClient extends Command
{ {
private const TARGET = 'clients/V2/src'; private const TARGET = 'client/src';
private const CLIENTNAMESPACE = 'LDC\\LDCCRM'; private const CLIENTNAMESPACE = 'LDC\\LDCCRM';
/** /**
@ -33,15 +35,16 @@ class GenerateCrmClient extends Command
* *
* @var string * @var string
*/ */
protected $description = 'Generate the crm v2 client in clients/V2 folder'; protected $description = 'Generate the crm V4 client in client folder';
/** /**
* Execute the console command. * Execute the console command.
* *
* @return integer * @return integer
*/ */
public function handle() public function handle(): int
{ {
$this->copyTraits();
$this->copyEnums(); $this->copyEnums();
$this->copyExceptions(); $this->copyExceptions();
$this->copyRoutesList(); $this->copyRoutesList();
@ -54,7 +57,7 @@ class GenerateCrmClient extends Command
* *
* @return void * @return void
*/ */
private function copyApiConfigInfo() private function copyApiConfigInfo(): void
{ {
$this->info('Copy Api Config'); $this->info('Copy Api Config');
$rootTarget = app_path('../' . self::TARGET); $rootTarget = app_path('../' . self::TARGET);
@ -72,13 +75,13 @@ class GenerateCrmClient extends Command
* *
* @return void * @return void
*/ */
private function copyRoutesList() private function copyRoutesList(): void
{ {
$this->info('Copy Routes'); $this->info('Copy Routes');
$routes = []; $routes = [];
/** @var \Illuminate\Routing\Route */ /** @var \Illuminate\Routing\Route */
foreach (Route::getRoutes() as $route) { foreach (Route::getRoutes() as $route) {
if (\strpos($route->getPrefix(), 'v2') !== false) { if (\strpos($route->getPrefix(), 'v4') !== false) {
$routes[$route->getName()] = [ $routes[$route->getName()] = [
'uri' => $route->uri(), 'uri' => $route->uri(),
'methods' => $route->methods() 'methods' => $route->methods()
@ -104,15 +107,36 @@ class GenerateCrmClient extends Command
{ {
$this->info('Copy Exceptions'); $this->info('Copy Exceptions');
$rootTarget = app_path('../' . self::TARGET); $rootTarget = app_path('../' . self::TARGET);
$this->copyToFolder([ $classes = [
CrmException::class, CrmException::class,
ApiErrorException::class ApiErrorException::class
], 'Exceptions'); ];
$this->replaceInPhpFile( $this->copyToFolder($classes, 'Exceptions');
"$rootTarget/Exceptions/ApiErrorException.php", foreach ($classes as $class) {
'App\Enums\ApiError', $classBasename = class_basename($class);
'LDC\LDCCRM\Enums\ApiError' $dest = "$rootTarget/Exceptions/$classBasename.php";
); $this->replaceInPhpFile($dest, 'App\\', self::CLIENTNAMESPACE . '\\');
}
}
/**
* Copy base enums classes
*
* @return void
*/
private function copyTraits(): void
{
$this->info('Copy Traits');
$rootTarget = app_path('../' . self::TARGET);
$classes = [
BaseEnum::class
];
$this->copyToFolder($classes, 'Traits');
foreach ($classes as $class) {
$classBasename = class_basename($class);
$dest = "$rootTarget/Traits/$classBasename.php";
$this->replaceInPhpFile($dest, 'App\\', self::CLIENTNAMESPACE . '\\');
}
} }
/** /**
@ -123,7 +147,8 @@ class GenerateCrmClient extends Command
private function copyEnums(): void private function copyEnums(): void
{ {
$this->info('Copy Enums'); $this->info('Copy Enums');
$classes = [ $rootTarget = app_path('../' . self::TARGET);
$classes = [
ApiError::class, ApiError::class,
ApiStatus::class, ApiStatus::class,
ArcadiaOrderType::class, ArcadiaOrderType::class,
@ -131,11 +156,10 @@ class GenerateCrmClient extends Command
QuestionType::class QuestionType::class
]; ];
$this->copyToFolder($classes, 'Enums'); $this->copyToFolder($classes, 'Enums');
$rootTarget = app_path('../' . self::TARGET);
foreach ($classes as $class) { foreach ($classes as $class) {
$classBasename = class_basename($class); $classBasename = class_basename($class);
$dest = "$rootTarget/Enums/$classBasename.php"; $dest = "$rootTarget/Enums/$classBasename.php";
$this->replaceInPhpFile($dest, 'BaseEnumRoutable', 'BaseEnum'); $this->replaceInPhpFile($dest, 'App\\', self::CLIENTNAMESPACE . '\\');
} }
} }
@ -148,9 +172,10 @@ class GenerateCrmClient extends Command
*/ */
private function copyToFolder(array $classes, string $folder): void private function copyToFolder(array $classes, string $folder): void
{ {
$rootTarget = app_path('../' . self::TARGET); $rootTarget = app_path('../' . self::TARGET);
$folderNamespace = \str_replace('/', '\\', $folder);
foreach ($classes as $class) { foreach ($classes as $class) {
$classFileName = \str_replace("App\\$folder\\", '', sprintf('%s.php', $class)); $classFileName = \str_replace("App\\$folderNamespace\\", '', sprintf('%s.php', $class));
$classFileName = \str_replace('\\', '/', $classFileName); $classFileName = \str_replace('\\', '/', $classFileName);
$classBasename = class_basename($class); $classBasename = class_basename($class);
$source = \app_path("$folder/$classFileName"); $source = \app_path("$folder/$classFileName");
@ -175,11 +200,15 @@ class GenerateCrmClient extends Command
{ {
$fContent = File::get($source); $fContent = File::get($source);
// Remove trailing \Api\V2 . // Remove trailing \Api\V4 .
if ($pos = strpos($fContent, '\Api\V2')) { if ($pos = strpos($fContent, '\Api\V4')) {
$fContent = substr_replace($fContent, '', $pos, strlen('\Api\V2')); $fContent = substr_replace($fContent, '', $pos, strlen('\Api\V4'));
} }
// Remove trailing \Api .
$fContent = Str::of($fContent)->replaceMatches("/\\\(Api)([\s;\\\])/", '$2');
$fContent = Str::of($fContent)->replaceMatches("/\\\Traits\\\Enums/", '\Traits');
// Replace namespace top level. // Replace namespace top level.
$pos = strpos($fContent, $search); $pos = strpos($fContent, $search);
if ($pos !== false) { if ($pos !== false) {

View file

@ -1,78 +0,0 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
/**
* Command Name: LDC Web Status Checker
* Description: Envoi des infos Serveur/Laravel vers LDC Web
* Version: 1.1
* Author: Vincent BRIDIER
* Author URI: vincent.bridier@ldc.fr
*
* @codeCoverageIgnore
*/
class LdcWebStatusChecker extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'ldcweb:statuschecker';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Envoi des infos Serveur/Laravel vers LDC Web';
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$data = [];
$data['uuid'] = config('ldcweb.project_uuid');
// Sitemap.xml ?
if (file_exists(public_path() . '/sitemap.xml')) {
$data['sitemap'] = 'true';
} else {
$data['sitemap'] = 'false';
}
// Robots.txt ?
if (file_exists(public_path() . '/robots.txt')) {
$data['robots'] = 'true';
} else {
$data['robots'] = 'false';
}
// Check logs.
$data['logs'] = 0;
$patherror = storage_path('/logs');
$files = glob($patherror . '/*.log');
if (count($files) > 0) {
foreach ($files as $file) {
$data['logs'] = $data['logs'] + filesize($file);
}
}
$data['code']['version'] = app()->version();
// Send by cURL.
$url = 'https://ldc-web.ldc.fr/api/status-checker';
$ch = curl_init($url);
$payload = ['token' => 'CE8S52Qq7stf5k8frF4T', 'datas' => json_encode($data)];
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_exec($ch);
curl_close($ch);
return 0;
}
}

View file

@ -1,73 +0,0 @@
<?php
namespace App\Console\Commands;
use App\Lib\MigrateOldData;
use App\Models\Brand;
use Exception;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
/**
* @codeCoverageIgnore
*/
class MigrateCrmUsers extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'migrate:crm';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Migrate crm users';
/**
* Execute the console command.
*
* @return integer
*/
public function handle()
{
try {
$table = $this->ask('What is the tmp table name with users data ?');
try {
DB::connection(config('database.oldDB'))->table($table)->exists();
} catch (Exception $e) {
$this->error(sprintf('%s table not found on old crm database', $table));
return -1;
}
$brandname = $this->ask('What is the brand name ?');
$brand = Brand::where('name', 'LIKE', "%$brandname%")->first();
if (!$brand) {
if (!$this->confirm(sprintf('Would you like to use %s ? ', $brandname))) {
$this->info('Command cancelled');
return 0;
}
if ($this->confirm('Would you like to create a new one ?')) {
$brand = new Brand();
$brand->name = $brandname;
$brand->save();
} else {
$this->info('Command cancelled');
return 0;
}
}
$mgOldData = new MigrateOldData();
$mgOldData->runMigration($brand, $table);
} catch (Exception $e) {
$this->error(sprintf('Failed %s', $e));
return -1;
}//end try
return 0;
}
}

View file

@ -29,7 +29,7 @@ class StartStatJobs extends Command
* *
* @return integer * @return integer
*/ */
public function handle() public function handle(): int
{ {
\dispatch(new JobsStartStatJobs())->afterResponse(); \dispatch(new JobsStartStatJobs())->afterResponse();
return 0; return 0;

View file

@ -0,0 +1,35 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Mail;
class TestMail extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'uwil:mail:test';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Send test email to equipe.web@ldc.fr';
/**
* Execute the console command.
*
* @return void
*/
public function handle()
{
Mail::raw(sprintf('UWIL mail-test from %s %s', \config('app.name'), \config('app.url')), function ($msg) {
$msg->to('equipe.web@ldc.fr')->subject('Test Email');
});
}
}

View file

@ -2,13 +2,14 @@
namespace App\Console\Commands; namespace App\Console\Commands;
use App\Enums\Users\RoleEnum;
use App\Models\User; use App\Models\User;
use Carbon\Carbon;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\ValidationException;
use Illuminate\Support\Str;
/**
* @codeCoverageIgnore
*/
class UserCreate extends Command class UserCreate extends Command
{ {
/** /**
@ -16,35 +17,189 @@ class UserCreate extends Command
* *
* @var string * @var string
*/ */
protected $signature = 'user:create'; protected $signature = 'uwil:user:create';
/** /**
* The console command description. * The console command description.
* *
* @var string * @var string
*/ */
protected $description = 'Command description'; protected $description = 'Create a backend admin user';
/**
* The first name of the new user.
*
* @var string|null
*/
protected $firstName;
/**
* The last name of the new user.
*
* @var string|null
*/
protected $lastName;
/**
* The email of the new user.
*
* @var string|null
*/
protected $email;
/**
* The role of the new user.
*
* @var string|null
*/
protected $role;
/** /**
* Execute the console command. * Execute the console command.
* *
* @return integer * @return void
*/ */
public function handle() public function handle(): void
{ {
$firstname = $this->ask('What is the firstname?'); $this->firstName = null;
$lastname = $this->ask('What is the lastname?'); $this->addFirstName();
$email = $this->ask('What is the email?');
$pwd = $this->secret('What is the password?');
$user = new User(); $this->lastName = null;
$user->firstname = $firstname; $this->addLastName();
$user->lastname = $lastname;
$user->email = $email;
$user->password = Hash::make($pwd);
$user->save();
$this->info('User created.'); $this->email = null;
return 0; $this->addEmail();
$this->role = null;
$this->addRole();
$user = new User([
'first_name' => $this->firstName,
'last_name' => $this->lastName,
'email' => $this->email,
'role' => $this->role,
'published' => true,
'published_at' => Carbon::now(),
]);
$user->saveOrFail();
$this->info('User created 👌');
}
/**
* Add a first name to the user.
*
* @return void
*/
private function addFirstName(): void
{
while (is_null($this->firstName)) {
try {
$tmp = $this->ask('Type the wanted user first name', 'Admin');
$validator = Validator::make(['first_name' => $tmp], [
'first_name' => 'required|min:3|max:255'
]);
$validator->validate();
$this->firstName = $tmp;
} catch (ValidationException $e) {
$this->error(sprintf(
'Please provide a valid first name %s',
\implode(',', collect($e->errors())->flatten()->all())
));
continue;
}
}
}
/**
* Add a last name to the user.
*
* @return void
*/
private function addLastName(): void
{
while (is_null($this->lastName)) {
try {
$tmp = $this->ask('Type the wanted user last name', 'Saïtama');
$validator = Validator::make(['last_name' => $tmp], [
'last_name' => 'required|min:3|max:255'
]);
$validator->validate();
$this->lastName = $tmp;
} catch (ValidationException $e) {
$this->error(sprintf(
'Please provide a valid last_name %s',
\implode(',', collect($e->errors())->flatten()->all())
));
continue;
}
}
}
/**
* Add an email to the user.
*
* @return void
*/
private function addEmail(): void
{
while (is_null($this->email)) {
try {
$tmp = $this->ask('Type the wanted user email', 'equipe.web@ldc.fr');
$validator = Validator::make(['email' => $tmp], [
'email' => 'required|unique:users,email|email:rfc,dns'
]);
$validator->validate();
$this->email = $tmp;
} catch (ValidationException $e) {
$this->error(sprintf(
'Please provide a valid email %s',
\implode(',', collect($e->errors())->flatten()->all())
));
continue;
}
}
}
/**
* Add a role to the user.
*
* @return void
*/
private function addRole(): void
{
while (is_null($this->role)) {
try {
$tmp = $this->choice(
'Select his role',
[
Str::ucFirst(RoleEnum::conceptor->label()),
Str::ucFirst(RoleEnum::admin->label()),
Str::ucFirst(RoleEnum::marketing->label())
],
Str::ucFirst(RoleEnum::marketing->label()),
);
if ($tmp === Str::ucFirst(RoleEnum::conceptor->label())) {
$tmp = RoleEnum::conceptor;
}
if ($tmp === Str::ucFirst(RoleEnum::admin->label())) {
$tmp = RoleEnum::admin;
}
if ($tmp === Str::ucFirst(RoleEnum::marketing->label())) {
$tmp = RoleEnum::marketing;
}
$validator = Validator::make(['role' => $tmp], [
'role' => 'required'
]);
$validator->validate();
$this->role = $tmp;
} catch (ValidationException $e) {
$this->error(sprintf(
'Please choose from the selection %s',
\implode(',', collect($e->errors())->flatten()->all())
));
continue;
}
}
} }
} }

View file

@ -3,13 +3,11 @@
namespace App\Console; namespace App\Console;
use App\Console\Commands\DeleteUnconfirmedCustomers; use App\Console\Commands\DeleteUnconfirmedCustomers;
use App\Jobs\SendTransferredToDigitalCountMail;
use App\Jobs\StartStatJobs; use App\Jobs\StartStatJobs;
use Illuminate\Console\Scheduling\Schedule; use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel; use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
/**
* @codeCoverageIgnore
*/
class Kernel extends ConsoleKernel class Kernel extends ConsoleKernel
{ {
/** /**
@ -25,10 +23,13 @@ class Kernel extends ConsoleKernel
* @param \Illuminate\Console\Scheduling\Schedule $schedule * @param \Illuminate\Console\Scheduling\Schedule $schedule
* @return void * @return void
*/ */
protected function schedule(Schedule $schedule) protected function schedule(Schedule $schedule): void
{ {
// @codeCoverageIgnoreStart
$schedule->job(StartStatJobs::class)->dailyAt('03:30'); $schedule->job(StartStatJobs::class)->dailyAt('03:30');
$schedule->job(SendTransferredToDigitalCountMail::class)->weekdays()->at('07:00');
$schedule->command(DeleteUnconfirmedCustomers::class)->dailyAt('05:30'); $schedule->command(DeleteUnconfirmedCustomers::class)->dailyAt('05:30');
// @codeCoverageIgnoreEnd
} }
/** /**
@ -36,7 +37,7 @@ class Kernel extends ConsoleKernel
* *
* @return void * @return void
*/ */
protected function commands() protected function commands(): void
{ {
$this->load(__DIR__ . '/Commands'); $this->load(__DIR__ . '/Commands');

View file

@ -0,0 +1,46 @@
<?php
namespace App\Enums\ActivityLogs;
use Uwil\LaravelCommon\Traits\Enums\BaseEnum;
enum ActivityLogsEventEnum: int
{
use BaseEnum;
case created = 1;
case updated = 2;
case duplicated = 3;
case deleted = 4;
/**
* Optionnal labels definition
*/
private const LABELS = [
self::created->name => 'Création',
self::updated->name => 'Mise à jour',
self::duplicated->name => 'Duplication',
self::deleted->name => 'Suppression',
];
/**
* Custom added classes definition
*/
private const BOOTSTRAPCLASS = [
self::created->name => 'success',
self::updated->name => 'warning',
self::duplicated->name => 'secondary',
self::deleted->name => 'danger',
];
/**
* Get Class
*
* @return string
*/
public function bootstrapClass(): string
{
return self::BOOTSTRAPCLASS[$this->name];
}
}

View file

@ -2,113 +2,43 @@
namespace App\Enums; namespace App\Enums;
use Kwaadpepper\Enum\BaseEnumRoutable; use Uwil\LaravelCommon\Traits\Enums\BaseEnum;
/** enum ApiError: int
* This describes an API error.
*
* @method static self modelNotFound()
* The request model was not found (typically the customer)
*
* @method static self validationFailed()
* This means some data could not be validated,
* you may have to look into message to get the details
*
* @method static self customerExists()
* The customer already exists
*
* @method static self customerNotActivated()
* The customer account is not activated and cannot perform
* this action
*
* @method static self customerAlreadyActive()
* You have requested to activate a customer account,
* but it is already active
*
* @method static self singleOptinCustomerExists()
* You have requested to create a single optin for an email,
* but it already exists
*
* @method static self apiAuthFailed()
* The authentification to the api failed. You may have to
* check into config or .env file for your service credentials
*
* @method static self authFailed()
* The authentification for this customer failed because of wrong credentials
*
* @method static self methodNotAllowed()
* This http method is not allowed
*
* @method static self wrongTimeStamp()
* The timestamp given is wrong. This would happen in case if
* a replay attack scenario. Timestamps are provided in headers
* and in the http body
*
* @method static self missingParameter()
* The request is missing some parameters
*
* @method static self throttleRequestsException()
* Too many requests have been attempted. The is a throttle security.
* Normally you should not trigger this.
* If this happens fill a ticket on https://supportweb.ldc.fr
* with some technical details
*
* @method static self authorizationException()
* You are maybe trying to access data that is out of your range.
* This should not happen normally, it is here to allow you to handle this case.
* If this happens fill a ticket on https://supportweb.ldc.fr
* with some technical details
*
* @package LDCCrmClient
* @subpackage enums
*/
class ApiError extends BaseEnumRoutable
{ {
/** use BaseEnum;
* Valeurs des possibilités de cet énumération (value)
* case modelNotFound = 101;
* @return array case validationFailed = 102;
*/ case customerExists = 103;
public static function values(): array case customerAlreadyActive = 110;
{ case customerNotActivated = 116;
return [ case authFailed = 117;
'modelNotFound' => 101, case apiAuthFailed = 119;
'validationFailed' => 102, case singleOptinCustomerExists = 120;
'customerExists' => 103, case methodNotAllowed = 150;
'customerAlreadyActive' => 110, case wrongTimeStamp = 151;
'customerNotActivated' => 116, case missingParameter = 400;
'authFailed' => 117, case authorizationException = 403;
'apiAuthFailed' => 119, case throttleRequestsException = 429;
'singleOptinCustomerExists' => 120,
'methodNotAllowed' => 150,
'wrongTimeStamp' => 151,
'missingParameter' => 400,
'authorizationException' => 403,
'throttleRequestsException' => 429
];
}
/** /**
* Libellés des possibilités de cet énumération (label) * Optionnal labels definition
*
* @return array
*/ */
public static function labels(): array private const LABELS = [
{ self::modelNotFound->name => '%s introuvable.',
return [ self::validationFailed->name => 'Impossible de valider les données',
'modelNotFound' => '%s introuvable.', self::customerExists->name => 'L\'utilisateur est déjà enregistré sur ce site',
'validationFailed' => 'Impossible de valider les données', self::customerNotActivated->name => 'Le compte n\'est pas activé',
'customerExists' => 'L\'utilisateur est déjà enregistré sur ce site', self::customerAlreadyActive->name => 'Le compte est déjà activé',
'customerNotActivated' => 'Le compte n\'est pas activé', self::singleOptinCustomerExists->name => 'Le single optin ne peut être ajouté car un compte existe.',
'customerAlreadyActive' => 'Le compte est déjà activé', self::authFailed->name => 'Echec de l\'authentification du compte',
'singleOptinCustomerExists' => 'Le single optin ne peut être ajouté car un compte existe.', self::apiAuthFailed->name => 'Echec de l\'authentification client à l\'Api LDC-CRM pour ce service',
'authFailed' => 'Echec de l\'authentification du compte', self::methodNotAllowed->name => 'This method is not allowed for this request',
'apiAuthFailed' => 'Echec de l\'authentification client à l\'Api LDC-CRM pour ce service', self::wrongTimeStamp->name => 'Your request is malformed, please check timestamp header and
'methodNotAllowed' => 'This method is not allowed for this request', parameter',
'wrongTimeStamp' => 'Your request is malformed, please check timestamp header and parameter', self::missingParameter->name => 'The request is missing some parameters',
'missingParameter' => 'The request is missing some parameters', self::authorizationException->name => 'You are not autorized to access this',
'authorizationException' => 'You are not autorized to access this', self::throttleRequestsException->name => 'Throttle: too many request have been attempted, please retry later'
'throttleRequestsException' => 'Throttle: too many request have been attempted, please retry later' ];
];
}
} }

View file

@ -2,15 +2,12 @@
namespace App\Enums; namespace App\Enums;
use Kwaadpepper\Enum\BaseEnumRoutable; use Uwil\LaravelCommon\Traits\Enums\BaseEnum;
/** enum ApiStatus: int
* @method static self OK()
* @method static self KO()
*
* @package LDCCrmClient
* @subpackage enums
*/
class ApiStatus extends BaseEnumRoutable
{ {
use BaseEnum;
case OK = 1;
case KO = 2;
} }

View file

@ -2,27 +2,12 @@
namespace App\Enums; namespace App\Enums;
use Kwaadpepper\Enum\BaseEnumRoutable; use Uwil\LaravelCommon\Traits\Enums\BaseEnum;
/** enum ArcadiaMailType: int
* @method static self default()
* @method static self game()
*
* @package LDCCrmClient
* @subpackage enums
*/
class ArcadiaMailType extends BaseEnumRoutable
{ {
/** use BaseEnum;
* Valeurs des possibilités de cet énumération (value)
* case default = 1;
* @return array case game = 2;
*/
public static function values(): array
{
return [
'default' => 1,
'game' => 2
];
}
} }

View file

@ -2,27 +2,12 @@
namespace App\Enums; namespace App\Enums;
use Kwaadpepper\Enum\BaseEnumRoutable; use Uwil\LaravelCommon\Traits\Enums\BaseEnum;
/** enum ArcadiaOrderType: int
* @method static self cb()
* @method static self check()
*
* @package LDCCrmClient
* @subpackage enums
*/
class ArcadiaOrderType extends BaseEnumRoutable
{ {
/** use BaseEnum;
* Valeurs des possibilités de cet énumération (value)
* case cb = 0;
* @return array case check = 1;
*/
public static function values(): array
{
return [
'cb' => 0,
'check' => 1
];
}
} }

View file

@ -0,0 +1,21 @@
<?php
namespace App\Enums\Claims;
use Uwil\LaravelCommon\Traits\Enums\BaseEnum;
enum ClaimMessageType: int
{
use BaseEnum;
case system = 1;
case human = 2;
/**
* Optionnal labels definition
*/
private const LABELS = [
self::system->name => 'Système',
self::human->name => 'Humain',
];
}

View file

@ -0,0 +1,51 @@
<?php
namespace App\Enums\Claims;
use Uwil\LaravelCommon\Traits\Enums\BaseEnum;
enum ClaimResponseType: int
{
use BaseEnum;
case none = 1;
case manual = 2;
case mail = 3;
case email = 4;
case phone = 5;
case other = 99;
/**
* Optionnal labels definition
*/
private const LABELS = [
self::none->name => 'Non défini',
self::manual->name => 'Manuelle',
self::mail->name => 'Courrier',
self::email->name => 'E-mail',
self::phone->name => 'Téléphone',
self::other->name => 'Autre',
];
/**
* Optionnal labels definition.
*/
private const COLORS = [
self::none->name => '#b8b8b8',
self::manual->name => '#a7a7a7',
self::mail->name => '#757575',
self::email->name => '#5e5e5e',
self::phone->name => '#414141',
self::other->name => '#2b2b2b',
];
/**
* Get Class.
*
* @return string
*/
public function color(): string
{
return self::COLORS[$this->name];
}
}

View file

@ -0,0 +1,48 @@
<?php
namespace App\Enums\Claims;
use Uwil\LaravelCommon\Traits\Enums\BaseEnum;
enum ClaimStatus: int
{
use BaseEnum;
case untreated = 1;
case sentDigital = 2;
case sentMarketing = 3;
case treated = 4;
case refused = 5;
/**
* Optionnal labels definition
*/
private const LABELS = [
self::untreated->name => 'Non traitée',
self::sentDigital->name => 'Transférée au service Digital',
self::sentMarketing->name => 'Retournée au Marketing',
self::treated->name => 'Traitée',
self::refused->name => 'Refusé de répondre',
];
/**
* Optionnal labels definition.
*/
private const COLORS = [
self::untreated->name => '#5a5a5a',
self::sentDigital->name => '#0033c0',
self::sentMarketing->name => '#69077c',
self::treated->name => '#188618',
self::refused->name => '#dc143c',
];
/**
* Get Class.
*
* @return string
*/
public function color(): string
{
return self::COLORS[$this->name];
}
}

View file

@ -0,0 +1,42 @@
<?php
namespace App\Enums\Claims;
use Uwil\LaravelCommon\Traits\Enums\BaseEnum;
enum ClaimType: int
{
use BaseEnum;
case classic = 1;
case br = 2;
case rgpd = 3;
/**
* Optionnal labels definition
*/
private const LABELS = [
self::classic->name => 'Classique',
self::br->name => 'BR',
self::rgpd->name => 'RGPD',
];
/**
* Optionnal labels definition.
*/
private const COLORS = [
self::classic->name => '#08818a',
self::br->name => '#9021f8',
self::rgpd->name => '#c2237a',
];
/**
* Get Class.
*
* @return string
*/
public function color(): string
{
return self::COLORS[$this->name];
}
}

File diff suppressed because it is too large Load diff

View file

@ -2,42 +2,23 @@
namespace App\Enums; namespace App\Enums;
use Kwaadpepper\Enum\BaseEnumRoutable; use Uwil\LaravelCommon\Traits\Enums\BaseEnum;
/** enum CustomerExportType: int
* @method static self singleOptin()
* @method static self customer()
* @method static self both()
*
* @codeCoverageIgnore
*/
class CustomerExportType extends BaseEnumRoutable
{ {
/** use BaseEnum;
* Valeurs des possibilités de cet énumération (value)
* case singleOptin = 1;
* @return array case customer = 2;
*/ case both = 3;
public static function values(): array
{
return [
'singleOptin' => 1,
'customer' => 2,
'both' => 3
];
}
/** /**
* Libellés des possibilités de cet énumération (label) * Optionnal labels definition
*
* @return array
*/ */
public static function labels(): array private const LABELS = [
{ self::singleOptin->name => 'Les single-optins uniquement',
return [ self::customer->name => 'Les clients uniquement acceptant de recevoir les newsletters',
'singleOptin' => trans('Les single optins uniquement'), self::both->name => 'La totalité des single-optins ainsi que des clients acceptant
'customer' => trans('Les clients uniquement acceptant de recevoir les newsletters'), de recevoir les newsletters',
'both' => trans('La totalité des client acceptant de recevoir les newsletters') ];
];
}
} }

View file

@ -0,0 +1,85 @@
<?php
namespace App\Enums;
use Uwil\LaravelCommon\Traits\Enums\BaseEnum;
enum ExportFormat: int
{
use BaseEnum;
case csv = 1;
case csvWithoutAttachment = 2;
case pdf = 3;
/**
* Optionnal labels definition
*/
private const LABELS = [
self::csv->name => 'csv',
self::csvWithoutAttachment->name => 'csv sans pièces jointes',
self::pdf->name => 'pdf',
];
/**
* Bootstrap color class to use for the export buttons.
*
* @return string
*/
public function class(): string
{
return match ($this) {
self::csv,
self::csvWithoutAttachment => 'success',
self::pdf => 'danger',
default => 'primary',
};
}
/**
* Font Awesome picto to use for the export buttons.
*
* @return string
*/
public function picto(): string
{
return match ($this) {
self::csv,
self::csvWithoutAttachment => 'fa-solid fa-file-csv',
self::pdf => 'fa-solid fa-file-pdf',
default => 'fa-solid fa-file',
};
}
/**
* File headers to use when the exported file is return with StreamedResponse.
*
* @return array
*/
public function fileHeaders(): array
{
return match ($this) {
self::csv,
self::csvWithoutAttachment => [
'Content-Type' => 'text/csv',
'Content-Disposition' => 'attachment; filename="export.csv"',
],
default => [],
};
}
/**
* Export file name.
*
* @return string
*/
public function fileName(): string
{
return match ($this) {
self::csv,
self::csvWithoutAttachment => 'export.csv',
self::pdf => 'export.pdf',
};
}
}

View file

@ -0,0 +1,16 @@
<?php
namespace App\Enums\Pagination;
use Uwil\LaravelCommon\Traits\Enums\BaseEnum;
enum ItemsPerPaginationEnum: int
{
use BaseEnum;
case five = 5;
case twelve = 12;
case twentyfive = 25;
case fifty = 50;
case onehundred = 100;
}

View file

@ -2,29 +2,13 @@
namespace App\Enums; namespace App\Enums;
use Kwaadpepper\Enum\BaseEnumRoutable; use Uwil\LaravelCommon\Traits\Enums\BaseEnum;
/** enum QuestionType: int
* @method static self select()
* @method static self radio()
* @method static self checkbox()
*
* @package LDCCrmClient
* @subpackage enums
*/
class QuestionType extends BaseEnumRoutable
{ {
/** use BaseEnum;
* Valeurs des possibilités de cet énumération (value)
* case select = 1;
* @return array case radio = 2;
*/ case checkbox = 3;
public static function values(): array
{
return [
'select' => 1,
'radio' => 2,
'checkbox' => 3
];
}
} }

View file

@ -0,0 +1,21 @@
<?php
namespace App\Enums\Theme;
use Uwil\LaravelCommon\Traits\Enums\BaseEnum;
enum BootstrapThemeEnum: int
{
use BaseEnum;
case light = 1;
case dark = 2;
/**
* Optionnal labels definition
*/
private const LABELS = [
self::light->name => 'Light',
self::dark->name => 'Dark',
];
}

View file

@ -0,0 +1,23 @@
<?php
namespace App\Enums\Users;
use Uwil\LaravelCommon\Traits\Enums\BaseEnum;
enum RoleEnum: int
{
use BaseEnum;
case conceptor = 1;
case admin = 2;
case marketing = 3;
/**
* Optionnal labels definition.
*/
private const LABELS = [
self::conceptor->name => 'Concepteur',
self::admin->name => 'Administrateur',
self::marketing->name => 'Marketing',
];
}

View file

@ -0,0 +1,55 @@
<?php
namespace App\Events;
use App\Models\Claim;
use App\Models\User;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class ClaimModelChanged
{
use Dispatchable;
use InteractsWithSockets;
use SerializesModels;
/** @var \App\Models\Claim $claim Current claim modified. */
public $claim;
/** @var string $field Modified field. */
public $field;
/** @var string $label Label of the modified field. */
public $label;
/** @var \App\Models\User|null $user Current authenticated user. */
public $user;
/** @var string|null $content Custom content message. */
public $content;
/**
* Create a new event instance.
*
* @param \App\Models\Claim $claim
* @param string $field
* @param string $label
* @param \App\Models\User|null $user
* @param string|null $content
* @return void
*/
public function __construct(
Claim $claim,
string $field,
string $label,
User|null $user = null,
string|null $content = null,
) {
$this->claim = $claim;
$this->field = $field;
$this->label = $label;
$this->user = $user;
$this->content = $content;
}
}

View file

@ -1,14 +0,0 @@
<?php
namespace App\Exceptions;
use Exception;
/**
* This class Exception is used to skip a row import from
* database migration (2016 -> 2020)
* @author Jérémy Munsch <jeremy.munsch@ldc.fr>
*/
class AbortRowImportException extends Exception
{
}

View file

@ -10,11 +10,11 @@ use Throwable;
/** /**
* This would be thrown if any of the case * This would be thrown if any of the case
* referenced in \App\Enums\ApiError would * referenced in \App\Enums\ApiError would
* occur within the crm * occur within the crm.
* *
* On the client an ApiErrorException would * On the client an ApiErrorException would
* also be throw with the ApiError corresponding to * also be throw with the ApiError corresponding to
* the situation * the situation.
* *
* @package LDCCrmClient * @package LDCCrmClient
* @subpackage exceptions * @subpackage exceptions
@ -34,7 +34,7 @@ class ApiErrorException extends Exception
/** /**
* This is an explicit error message, * This is an explicit error message,
* ou may print this to the front or not depending * ou may print this to the front or not depending
* on $error value * on $error value.
* *
* @var string * @var string
*/ */
@ -52,10 +52,10 @@ class ApiErrorException extends Exception
/** /**
* Create a new exception base on ApiError enum. * Create a new exception base on ApiError enum.
* *
* @param ApiError $error * @param \App\Enums\ApiError $error
* @param integer $httpCode * @param integer $httpCode
* @param string $message * @param string $message
* @param Throwable|null $previous * @param Throwable|null $previous
*/ */
public function __construct(ApiError $error, int $httpCode = 400, string $message = '', Throwable $previous = null) public function __construct(ApiError $error, int $httpCode = 400, string $message = '', Throwable $previous = null)
{ {
@ -63,16 +63,16 @@ class ApiErrorException extends Exception
$this->message = $message; $this->message = $message;
$this->httpCode = $httpCode; $this->httpCode = $httpCode;
parent::__construct( parent::__construct(
strlen($this->message) ? $this->message : $error->label, strlen($this->message) ? $this->message : $error->label(),
$error->value, $error->value(),
$previous $previous
); );
} }
/** /**
* Get the Api Error * Get the Api Error.
* *
* @return ApiError * @return \App\Enums\ApiError
*/ */
public function getApiError(): ApiError public function getApiError(): ApiError
{ {
@ -80,7 +80,7 @@ class ApiErrorException extends Exception
} }
/** /**
* Get the http code * Get the http code.
* *
* @return integer * @return integer
*/ */
@ -95,7 +95,7 @@ class ApiErrorException extends Exception
* *
* @return void * @return void
*/ */
public function report() public function report(): void
{ {
// @codeCoverageIgnoreStart // @codeCoverageIgnoreStart
Log::debug(sprintf('CRM API: %s', $this->getMessage())); Log::debug(sprintf('CRM API: %s', $this->getMessage()));

View file

@ -12,7 +12,6 @@ use Illuminate\Support\Facades\Log;
* *
* @package LDCCrmClient * @package LDCCrmClient
* @subpackage exceptions * @subpackage exceptions
* @codeCoverageIgnore
*/ */
class CrmException extends Exception class CrmException extends Exception
{ {
@ -22,7 +21,7 @@ class CrmException extends Exception
* *
* @return void * @return void
*/ */
public function report() public function report(): void
{ {
Log::critical(sprintf('CRM API: %s', $this->getMessage())); Log::critical(sprintf('CRM API: %s', $this->getMessage()));
} }

View file

@ -0,0 +1,102 @@
<?php
namespace App\Exceptions\Api\V4;
use App\Enums\ApiError;
use Illuminate\Support\Facades\Log;
/**
* This would be thrown if any of the case
* referenced in \App\Enums\ApiError would
* occur within the crm.
*
* On the client an ApiErrorException would
* also be throw with the ApiError corresponding to
* the situation.
*
* @package LDCCrmClient
* @subpackage exceptions
*/
class ApiErrorException extends \RuntimeException
{
/**
* Api Error
* This value makes the error more explicit.
* Handling these errors are normal as inputs or
* request may be undesired or refused by the crm.
*
* @var \App\Enums\ApiError
*/
protected $error;
/**
* This is an explicit error message,
* ou may print this to the front or not depending
* on $error value.
*
* @var string
*/
protected $message;
/**
* This is the http code returned by the crm.
* This value could be used to handle errors.
* But please take in consideration $error first.
*
* @var integer
*/
protected $httpCode;
/**
* Create a new exception base on ApiError enum.
*
* @param \App\Enums\ApiError $error
* @param integer $httpCode
* @param string $message
* @param \Throwable|null $previous
*/
public function __construct(ApiError $error, int $httpCode = 400, string $message = '', \Throwable $previous = null)
{
$this->error = $error;
$this->message = $message;
$this->httpCode = $httpCode;
parent::__construct(
strlen($this->message) ? $this->message : $error->label(),
$error->value(),
$previous
);
}
/**
* Get the Api Error.
*
* @return \App\Enums\ApiError
*/
public function getApiError(): ApiError
{
return $this->error;
}
/**
* Get the http code.
*
* @return integer
*/
public function getStatusCode(): int
{
return $this->httpCode;
}
/**
* This is used by laravel only.
* It uses Log class to print the error.
*
* @return void
*/
public function report(): void
{
// @codeCoverageIgnoreStart
Log::debug(sprintf('CRM API: %s', $this->getMessage()));
// @codeCoverageIgnoreEnd
}
}

View file

@ -0,0 +1,28 @@
<?php
namespace App\Exceptions\Api\V4;
use Illuminate\Support\Facades\Log;
/**
* This is a critical error.
* The request could not be handled. You may dump any value you can
* get before submitting a ticker to https://support.uwil.fr/
*
* @package LDCCrmClient
* @subpackage exceptions
*
*/
class CrmException extends \RuntimeException
{
/**
* This is used by laravel only.
* It uses Log class to print the error.
*
* @return void
*/
public function report(): void
{
Log::critical(sprintf('CRM API: %s', $this->getMessage()));
}
}

View file

@ -4,8 +4,10 @@ namespace App\Exceptions;
use App\Enums\ApiError; use App\Enums\ApiError;
use App\Enums\ApiStatus; use App\Enums\ApiStatus;
use App\Exceptions\Api\V2\ApiErrorException; use App\Exceptions\Api\V2\ApiErrorException as V2ApiErrorException;
use App\Exceptions\Api\V4\ApiErrorException as V4ApiErrorException;
use App\Http\Resources\Api\V2\ApiResponse as V2ApiResponse; use App\Http\Resources\Api\V2\ApiResponse as V2ApiResponse;
use App\Http\Resources\Api\V4\ApiResponse as V4ApiResponse;
use App\Lib\Helper; use App\Lib\Helper;
use Illuminate\Auth\Access\AuthorizationException; use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Auth\AuthenticationException; use Illuminate\Auth\AuthenticationException;
@ -14,9 +16,9 @@ use Illuminate\Http\Exceptions\ThrottleRequestsException;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException; use Illuminate\Validation\ValidationException;
use Kwaadpepper\ExceptionHandler\Exceptions\ExceptionHandler; use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException; use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException;
use Throwable;
class Handler extends ExceptionHandler class Handler extends ExceptionHandler
{ {
@ -45,8 +47,13 @@ class Handler extends ExceptionHandler
* *
* @return void * @return void
*/ */
public function register() public function register(): void
{ {
$this->reportable(function (\Throwable $e) {
if (app()->bound('sentry')) {
app('sentry')->captureException($e);
}
});
} }
/** /**
@ -56,10 +63,10 @@ class Handler extends ExceptionHandler
* @param \Throwable $e * @param \Throwable $e
* @return \Symfony\Component\HttpFoundation\Response * @return \Symfony\Component\HttpFoundation\Response
*/ */
public function render($request, Throwable $e) public function render(mixed $request, \Throwable $e): Response
{ {
if (Helper::isRequestForApiV2($request) and $request->isJson()) { if ((Helper::isRequestForApiV2($request) || Helper::isRequestForApiV4($request)) and $request->isJson()) {
return $this->handleApiV2Exception($request, $e); return $this->handleApiException($request, $e);
} //end if } //end if
// @codeCoverageIgnoreStart // @codeCoverageIgnoreStart
return parent::render($request, $e); return parent::render($request, $e);
@ -67,13 +74,13 @@ class Handler extends ExceptionHandler
} }
/** /**
* Generate a json response for any apiV2 error case * Generate a json response for any apiV4 error case.
* *
* @param Request $request * @param \Illuminate\Http\Request $request
* @param \Throwable $e * @param \Throwable $e
* @return JsonResponse * @return \Illuminate\Http\JsonResponse
*/ */
private function handleApiV2Exception(Request $request, \Throwable $e): JsonResponse private function handleApiException(Request $request, \Throwable $e): JsonResponse
{ {
switch (get_class($e)) { switch (get_class($e)) {
case AuthenticationException::class: case AuthenticationException::class:
@ -81,24 +88,24 @@ class Handler extends ExceptionHandler
return $this->getReponse( return $this->getReponse(
$request, $request,
401, 401,
ApiError::apiAuthFailed()->value, ApiError::apiAuthFailed->value(),
sprintf('%s, %s', ApiError::apiAuthFailed()->label, $e->getMessage()) sprintf('%s, %s', ApiError::apiAuthFailed->label(), $e->getMessage())
); );
case MethodNotAllowedHttpException::class: case MethodNotAllowedHttpException::class:
/** @var MethodNotAllowedHttpException $e */ /** @var MethodNotAllowedHttpException $e */
return $this->getReponse( return $this->getReponse(
$request, $request,
$e->getStatusCode(), $e->getStatusCode(),
ApiError::methodNotAllowed()->value, ApiError::methodNotAllowed->value(),
sprintf('%s, %s', ApiError::methodNotAllowed()->label, $e->getMessage()) sprintf('%s, %s', ApiError::methodNotAllowed->label(), $e->getMessage())
); );
case ValidationException::class: case ValidationException::class:
/** @var ValidationException $e */ /** @var ValidationException $e */
return $this->getReponse( return $this->getReponse(
$request, $request,
$e->status, $e->status,
ApiError::validationFailed()->value, ApiError::validationFailed->value(),
sprintf(ApiError::validationFailed()->label, $e->getMessage()), sprintf(ApiError::validationFailed->label(), $e->getMessage()),
['errors' => $e->errors()] ['errors' => $e->errors()]
); );
case ModelNotFoundException::class: case ModelNotFoundException::class:
@ -106,14 +113,22 @@ class Handler extends ExceptionHandler
return $this->getReponse( return $this->getReponse(
$request, $request,
404, 404,
ApiError::modelNotFound()->value, ApiError::modelNotFound->value(),
sprintf( sprintf(
ApiError::modelNotFound()->label, ApiError::modelNotFound->label(),
\array_slice(explode('\\', $e->getModel()), -1)[0] \array_slice(explode('\\', $e->getModel()), -1)[0]
) )
); );
case ApiErrorException::class: case V4ApiErrorException::class:
/** @var ApiErrorException $e */ /** @var \App\Exceptions\Api\V4\ApiErrorException $e */
return $this->getReponse(
$request,
$e->getStatusCode(),
$e->getCode(),
$e->getMessage()
);
case V2ApiErrorException::class:
/** @var \App\Exceptions\Api\V2\ApiErrorException $e */
return $this->getReponse( return $this->getReponse(
$request, $request,
$e->getStatusCode(), $e->getStatusCode(),
@ -125,36 +140,36 @@ class Handler extends ExceptionHandler
return $this->getReponse( return $this->getReponse(
$request, $request,
429, 429,
ApiError::throttleRequestsException()->value, ApiError::throttleRequestsException->value(),
ApiError::throttleRequestsException()->label ApiError::throttleRequestsException->label()
); );
case AuthorizationException::class: case AuthorizationException::class:
/** @var AuthorizationException $e */ /** @var AuthorizationException $e */
return $this->getReponse( return $this->getReponse(
$request, $request,
403, 403,
ApiError::authorizationException()->value, ApiError::authorizationException->value(),
ApiError::authorizationException()->label ApiError::authorizationException->label()
); );
} //end switch } //end switch
// @codeCoverageIgnoreStart // @codeCoverageIgnoreStart
if (config('app.env') === 'testing') { if (config('app.env') === 'testing') {
// phpcs:ignore Squiz.PHP.DiscouragedFunctions.Found // phpcs:ignore Squiz.PHP.DiscouragedFunctions.Found
print_r((string)$e); print_r((string) $e);
} }
return $this->getReponse($request, 500, $e->getCode(), $e->getMessage()); return $this->getReponse($request, 500, \intval($e->getCode()), $e->getMessage());
// @codeCoverageIgnoreEnd // @codeCoverageIgnoreEnd
} }
/** /**
* Generate an ApiV2 json response * Generate an ApiV4 json response.
* *
* @param Request $request * @param \Illuminate\Http\Request $request
* @param integer $httpCode * @param integer $httpCode
* @param integer $apiCode * @param integer $apiCode
* @param string $message * @param string $message
* @param array $data * @param array $data
* @return JsonResponse * @return \Illuminate\Http\JsonResponse
*/ */
private static function getReponse( private static function getReponse(
Request $request, Request $request,
@ -163,9 +178,15 @@ class Handler extends ExceptionHandler
string $message, string $message,
array $data = [] array $data = []
): JsonResponse { ): JsonResponse {
return (new V2ApiResponse( if (Helper::isRequestForApiV2($request)) {
(object)array_merge($data, ['message' => $message, 'code' => $apiCode]), return (new V2ApiResponse(
ApiStatus::KO() ApiStatus::KO,
(object) array_merge($data, ['message' => $message, 'code' => $apiCode]),
))->toResponse($request)->setStatusCode($httpCode);
}
return (new V4ApiResponse(
ApiStatus::KO,
(object) array_merge($data, ['message' => $message, 'code' => $apiCode]),
))->toResponse($request)->setStatusCode($httpCode); ))->toResponse($request)->setStatusCode($httpCode);
} }
} }

View file

@ -1,363 +0,0 @@
<?php
namespace App\Http\Controllers\Api\V1;
use App\Http\Controllers\Controller;
use App\Http\Resources\StatusResource;
use App\Mail\CourrierArcadiaError;
use App\Models\Customer;
use DOMDocument;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Validator;
/**
* @codeCoverageIgnore
*/
class ArcadiaController extends Controller
{
/**
* @param Request $request
* @return StatusResource
*/
public function outgoingMail(Request $request)
{
$validator = Validator::make($request->all(), [
'crm_id' => 'required',
'fields' => 'required|json',
'type' => 'required',
'shipping' => 'required_if:type,default|json'
]);
if ($validator->fails()) {
return new StatusResource([
'status' => 'KO',
'code' => 201,
'message' => 'Error on required fields.',
'errors' => $validator->errors()
]);
}
$brandId = $request->brand_id;
$crmId = $request->crm_id;
$fields = json_decode($request->fields);
$type = $request->type;
$shipping = $type === 'default' ? json_decode($request->shipping) : false;
$customer = Customer::where('id', $crmId)->first();
if (!$customer) {
return new StatusResource(['status' => 'KO', 'code' => 202, 'message' => 'User not found.']);
}
$CustomerInfo = $customer->infos()->where('brand_id', $brandId)->first();
if (!$CustomerInfo) {
return new StatusResource(['status' => 'KO', 'code' => 202, 'message' => 'User not found.']);
}
$check = $this->checkRequiredFields($fields, $type);
if ($check['status'] !== 'OK') {
return new StatusResource($check);
}
if ($type === 'default') {
$check = $this->checkShippingRequiredFields($shipping);
if ($check['status'] !== 'OK') {
return new StatusResource($check);
}
}
$xml = $this->makeXml($fields, $type, $customer, $CustomerInfo, $shipping);
try {
Storage::disk('arcadia')->put(date("Ymd_His") . microtime(true) . ".xml", $xml);
return new StatusResource(['status' => 'OK']);
} catch (\Exception $e) {
Log::critical("ARCADIA XML CREATION FAILED : " . $e->getMessage());
Mail::to(config('mail.MAIL_ADMIN'))->send(new CourrierArcadiaError(
"ARCADIA XML CREATION FAILED",
"Une erreur est survenue lors de la création du fichier XML : " . $e->getMessage(),
$xml ?: ''
));
return new StatusResource(['status' => 'KO', 'code' => 204, 'message' => 'Arcadia XML Creation failed.']);
}
}
/**
* @param mixed $fields
* @return array
*/
private function checkShippingRequiredFields($fields)
{
$test = [
'firstname',
'lastname',
'address',
'city',
'postcode'
];
foreach ($test as $t) {
if (!array_key_exists($t, $fields)) {
$errors[] = ['field' => $t, 'error' => 'required'];
}
}
if (!empty($errors)) {
return ['status' => 'KO', 'code' => 203, 'message' => $errors];
}
return ['status' => 'OK'];
}
/**
*
* @param mixed $fields
* @param mixed $type
* @return array
*/
// phpcs:ignore Generic.Metrics.CyclomaticComplexity.TooHigh, Squiz.Commenting.FunctionComment.Missing
private function checkRequiredFields($fields, $type): array
{
$errors = [];
$key = array_search('operation', array_column($fields->root, 'fieldName'));
if ($key === false) {
$errors[] = ['field' => 'operation', 'error' => 'required'];
}
$key = array_search('support_code', array_column($fields->orderLine, 'fieldName'));
if ($key === false) {
$errors[] = ['field' => 'support_code', 'error' => 'required'];
}
$key = array_search('label_type_support', array_column($fields->orderLine, 'fieldName'));
if ($key === false) {
$errors[] = ['field' => 'label_type_support', 'error' => 'required'];
}
$key = array_search('order_date', array_column($fields->root, 'fieldName'));
if ($key === false) {
$errors[] = ['field' => 'order_date', 'error' => 'required'];
}
$regex = join('', [
'/^(((0[1-9]|[12]\d|3[01])\/(0[13578]|1[02])\/((19|[2-9]\d)\d{2}))|',
'((0[1-9]|[12]\d|30)\/(0[13456789]|1[012])\/((19|[2-9]\d)\d{2}))',
'|((0[1-9]|1\d|2[0-8])\/02\/((19|[2-9]\d)\d{2}))|(29\/02\/((1[6-9]|[2-9]\d)(0[48]',
'|[2468][048]|[13579][26])|((16|[2468][048]|[3579][26])00))))$/'
]);
if (!preg_match($regex, $fields->root[$key]->fieldValue)) {
$errors[] = ['field' => 'order_date', 'error' => 'format must be DD/MM/YYYY'];
}
if ($type === 'default') {
$key = array_search('order_num', array_column($fields->root, 'fieldName'));
if ($key === false) {
$errors[] = ['field' => 'order_num', 'error' => 'required for default type'];
}
if (!is_numeric($fields->root[$key]->fieldValue) || strlen($fields->root[$key]->fieldValue) !== 9) {
$errors[] = ['field' => 'order_num', 'error' => 'format must be 9 numeric characters'];
}
$key = array_search('state_order_cb', array_column($fields->root, 'fieldName'));
if ($key === false) {
$errors[] = ['field' => 'state_order_cb', 'error' => 'required for default type'];
}
if ($fields->root[$key]->fieldValue !== 1 && $fields->root[$key]->fieldValue !== 'Null') {
$errors[] = ['field' => 'state_order_cb', 'error' => 'value must be 1 or Null (Yes Null not NULL)'];
}
$orderCB = $fields->root[$key]->fieldValue;
$key = array_search('state_order_check', array_column($fields->root, 'fieldName'));
if ($key === false) {
$errors[] = ['field' => 'state_order_check', 'error' => 'required for default type'];
}
if ($fields->root[$key]->fieldValue !== 1 && $fields->root[$key]->fieldValue !== 'Null') {
$errors[] = ['field' => 'state_order_check', 'error' => 'value must be 1 or Null (Yes Null not NULL)'];
}
$orderCheck = $fields->root[$key]->fieldValue;
if ($orderCB === $orderCheck) {
$errors[] = [
'field' => 'state_order',
'error' => 'state_order_cb and state_order_check values must be different'
];
}
$key = array_search('package_unit_weight', array_column($fields->orderLine, 'fieldName'));
if ($key === false) {
$errors[] = ['field' => 'package_unit_weight', 'error' => 'required for default type'];
}
} elseif ($type === 'game') {
$key = array_search('code_unique', array_column($fields->orderLine, 'fieldName'));
if ($key === false) {
$errors[] = ['field' => 'code_unique', 'error' => 'required for game type'];
}
}//end if
if (!empty($errors)) {
return ['status' => 'KO', 'code' => 203, 'message' => $errors];
}
return ['status' => 'OK'];
}
/**
* @param mixed $fields
* @param mixed $type
* @param mixed $customer
* @param mixed $customerInfos
* @param mixed $shipping
* @return string|false
*/
// phpcs:ignore Generic.Metrics.CyclomaticComplexity.TooHigh, Squiz.Commenting.FunctionComment.Missing
private function makeXml($fields, $type, $customer, $customerInfos, $shipping)
{
$pivot = [
'order_num' => 'Num_Commande',
// Required if type = default - format 9 numeric characters.
'order_date' => 'Date_Commande',
// Required - format : DD/MM/YYYY .
'operation' => 'Operation',
// Required.
'support_code' => 'Code_Support',
// Required.
'label_type_support' => 'Libelle_Type_Support',
// Required.
'package_unit_weight' => 'Poids_Unitaire_Colis',
// Required if type = default.
'qty' => 'Quantite',
'code_on_pack' => 'Code_on_Pack',
'code_unique' => 'Code_Unique',
// Required if type = game.
// Required if type = default - format 1 ou Null (oui Null et pas NULL).
'state_order_cb' => 'Etat_Commande_CB',
// Required if type = default - format 1 ou Null (oui Null et pas NULL).
'state_order_check' => 'Etat_Commande_Cheque',
];
$dom = new DOMDocument('1.0', 'UTF-8');
$dom->formatOutput = true;
$root = $dom->createElement('export_arcadia');
$dom->appendChild($root);
$order = $root->appendChild($dom->createElement('Commande'));
$key = array_search('order_num', array_column($fields->root, 'fieldName'));
if ($key === false) {
$order->appendChild($dom->createElement('Num_Commande'));
} else {
$order->appendChild($dom->createElement('Num_Commande', $fields->root[$key]->fieldValue));
}
foreach ($fields->root as $value) {
// phpcs:ignore PSR2.ControlStructures.ControlStructureSpacing.SpacingAfterOpenBrace
if (
isset($pivot[$value->fieldName]) && $value->fieldName !== 'state_order_cb' &&
$value->fieldName !== 'state_order_check' && $value->fieldName !== 'order_num'
) {
$order->appendChild($dom->createElement($pivot[$value->fieldName], $value->fieldValue));
}
}
if ($type === 'default') {
$key = array_search('state_order_cb', array_column($fields->root, 'fieldName'));
if ($key) {
$order->appendChild($dom->createElement('Etat_Commande_CB', $fields->root[$key]->fieldValue));
}
$key = array_search('state_order_check', array_column($fields->root, 'fieldName'));
if ($key) {
$order->appendChild($dom->createElement('Etat_Commande_Cheque', $fields->root[$key]->fieldValue));
}
}
$order->appendChild($dom->createElement('Email_conso'))
->appendChild($dom->createCDATASection("$customer->email"));
$order->appendChild($dom->createElement('Civilite'));
if ($type === 'default') {
$order->appendChild($dom->createElement('Nom'))
->appendChild($dom->createCDATASection("$shipping->lastname"));
$order->appendChild($dom->createElement('Prenom'))
->appendChild($dom->createCDATASection("$shipping->firstname"));
$order->appendChild($dom->createElement('Adresse'))
->appendChild($dom->createCDATASection("$shipping->address"));
$order->appendChild($dom->createElement('Adresse_Complementaire'));
$order->appendChild($dom->createElement('CP'))
->appendChild($dom->createCDATASection("$shipping->postcode"));
$order->appendChild($dom->createElement('Ville'))->appendChild($dom->createCDATASection("$shipping->city"));
if (isset($shipping->phone) && !empty($shipping->phone)) {
$order->appendChild($dom->createElement('Telephone'))
->appendChild($dom->createCDATASection("$shipping->phone"));
} else {
$order->appendChild($dom->createElement('Telephone'));
}
} else {
$order->appendChild($dom->createElement('Nom'))
->appendChild($dom->createCDATASection("$customerInfos->lastname"));
$order->appendChild($dom->createElement('Prenom'))
->appendChild($dom->createCDATASection("$customerInfos->firstname"));
$order->appendChild($dom->createElement('Adresse'))
->appendChild($dom->createCDATASection("$customerInfos->address"));
$order->appendChild($dom->createElement('Adresse_Complementaire'))
->appendChild($dom->createCDATASection("$customerInfos->address2"));
$order->appendChild($dom->createElement('CP'))
->appendChild($dom->createCDATASection("$customerInfos->postcode"));
$order->appendChild($dom->createElement('Ville'))
->appendChild($dom->createCDATASection("$customerInfos->city"));
$order->appendChild($dom->createElement('Telephone'))
->appendChild($dom->createCDATASection("$customerInfos->phone"));
}//end if
$orderLine = $order->appendChild($dom->createElement('Ligne_de_Commande'));
$key = array_search('qty', array_column($fields->orderLine, 'fieldName'));
if ($key === false) {
$orderLine->appendChild($dom->createElement('Quantite', '1'));
}
$key = array_search('package_unit_weight', array_column($fields->orderLine, 'fieldName'));
if ($key === false) {
$orderLine->appendChild($dom->createElement('Poids_Unitaire_Colis'));
}
foreach ($fields->orderLine as $value) {
// phpcs:ignore PSR2.ControlStructures.ControlStructureSpacing.SpacingAfterOpenBrace
if (
isset($pivot[$value->fieldName]) &&
$value->fieldName !== 'code_on_pack' &&
$value->fieldName !== 'code_unique'
) {
$orderLine->appendChild($dom->createElement($pivot[$value->fieldName], $value->fieldValue));
}
}
if ($type === 'game') {
$key = array_search('code_on_pack', array_column($fields->orderLine, 'fieldName'));
if ($key !== false) {
$orderLine->appendChild(
$dom->createElement('Code_on_Pack')
)->appendChild($dom->createCDATASection($fields->orderLine[$key]->fieldValue));
}
$key = array_search('code_unique', array_column($fields->orderLine, 'fieldName'));
if ($key !== false) {
$orderLine->appendChild(
$dom->createElement('Code_Unique')
)->appendChild($dom->createCDATASection($fields->orderLine[$key]->fieldValue));
}
}
return $dom->saveXML();
}
}

View file

@ -1,570 +0,0 @@
<?php
namespace App\Http\Controllers\Api\V1;
use App\Enums\Country;
use App\Http\Controllers\Controller;
use App\Http\Resources\CustomerResource;
use App\Http\Resources\StatusResource;
use App\Models\Customer;
use App\Models\CustomerInfo;
use App\Models\CustomerLogin;
use App\Models\CustomerOptInHistory;
use App\Models\SingleOptin;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Str;
/**
* @codeCoverageIgnore
*/
class CustomerController extends Controller
{
/**
* Store a newly created resource in storage
* @param Request $request
* @return CustomerResource|StatusResource
*/
// phpcs:ignore Generic.Metrics.CyclomaticComplexity.TooHigh, Squiz.Commenting.FunctionComment.Missing
public function store(Request $request)
{
$validator = Validator::make($request->all(), [
'email' => 'required|email',
'password' => 'required',
'firstname' => 'required',
'lastname' => 'required',
'opt_in' => 'required',
'birth_date' => 'date_format:Y-m-d'
]);
if ($validator->fails()) {
return new StatusResource([
'status' => 'KO', 'code' => 102,
'message' => 'Error on required fields.', 'errors' => $validator->errors()
]);
}
$brandId = $request->brand_id;
$email = strtolower(filter_var($request->email, FILTER_SANITIZE_EMAIL));
$firstname = filter_var($request->firstname, FILTER_SANITIZE_STRING);
$lastname = filter_var($request->lastname, FILTER_SANITIZE_STRING);
$optIn = filter_var($request->opt_in, FILTER_SANITIZE_STRING);
$hashcode = Str::uuid();
// Récupère le customer s'il existe déjà.
$customer = Customer::where('email', $email)->first();
// Si le customer n'existe pas déjà.
if (!$customer) {
$customer = new Customer();
$customer->email = $email;
$customer->save();
$singleOptin = SingleOptin::where([['email', $email], ['brand_id', $brandId]])->first();
if ($singleOptin) {
$singleOptin->delete();
}
} else {
// Récupère les infos s'ils existent déjà.
$CustomerInfo = CustomerInfo::where([['customer_id', $customer->id], ['brand_id', $brandId]])->first();
if ($CustomerInfo) {
return new StatusResource([
'status' => 'KO', 'code' => 103,
'message' => 'User already registered on this site.'
]);
}
}
$customerInfos = new CustomerInfo();
$customerInfos->firstname = ucfirst(strtolower($firstname));
$customerInfos->lastname = ucfirst(strtolower($lastname));
$customerInfos->birth_date = !empty($request->birth_date) ?
filter_var($request->birth_date, FILTER_SANITIZE_STRING) : null;
$customerInfos->address = !empty($request->address) ?
filter_var($request->address, FILTER_SANITIZE_STRING) : null;
$customerInfos->address2 = !empty($request->address2) ?
filter_var($request->address2, FILTER_SANITIZE_STRING) : null;
$customerInfos->phone = !empty($request->phone) ?
filter_var($request->phone, FILTER_SANITIZE_STRING) : null;
try {
$customerInfos->country = Country::make(filter_var($request->country, FILTER_SANITIZE_STRING));
} catch (\Exception $e) {
$customerInfos->country = Country::france();
}
$customerInfos->city = !empty($request->city) ?
filter_var($request->city, FILTER_SANITIZE_STRING) : null;
$customerInfos->postcode = !empty($request->postcode) ?
filter_var($request->postcode, FILTER_SANITIZE_STRING) : null;
$customerInfos->opt_in = $optIn;
$customerInfos->hashcode = $hashcode;
$customerInfos->brand_id = $brandId;
$customerInfos->customer_id = $customer->id;
$customer->infos()->save($customerInfos);
// Création du login pour cet utilisateur sur ce service.
$customerLogin = new CustomerLogin();
$customerLogin->customer_id = $customer->id;
$customerLogin->password = Hash::make(trim($request->password));
$customerLogin->last_connection = now();
$customerLogin->brand_id = $brandId;
$customerLogin->status = 0;
$customer->logins()->save($customerLogin);
// Ajout dans la table d'historique des opt-in de la nouvelle valeur.
$optInHistory = new CustomerOptInHistory();
$optInHistory->customer_id = $customer->id;
$optInHistory->brand_id = $brandId;
$optInHistory->new_value = $optIn;
$optInHistory->save();
return new CustomerResource($customer, $brandId);
}
/**
* Display the specified resource.
*
* @param Illuminate\Http\Request $request
* @param mixed $type
* @param mixed $value
* @return CustomerResource | StatusResource
*/
public function show(Request $request, $type, $value)
{
$brandId = $request->brand_id;
if ($type === 'id') {
$customer = Customer::find($value);
if (!$customer) {
return new StatusResource(['status' => 'KO', 'code' => 101, 'message' => 'User not found.']);
}
} elseif ($type === 'email') {
$customer = Customer::where('email', $value)->first();
if (!$customer) {
return new StatusResource(['status' => 'KO', 'code' => 101, 'message' => 'User not found.']);
}
$CustomerInfo = $customer->infos()->where('brand_id', $brandId)->first();
if (!$CustomerInfo) {
return new StatusResource(['status' => 'KO', 'code' => 101, 'message' => 'User not found.']);
}
} else {
return new StatusResource(['status' => 'KO', 'code' => 101, 'message' => 'User not found.']);
}
return new CustomerResource($customer, $brandId);
}
/**
* Update the specified resource in storage.
*
* @param \Illuminate\Http\Request $request
* @param int $id
* @return CustomerResource|StatusResource
*/
// phpcs:ignore Generic.Metrics.CyclomaticComplexity.MaxExceeded, Squiz.Commenting.FunctionComment.Missing
public function update(Request $request, $id)
{
$validator = Validator::make($request->all(), [
'birth_date' => 'date_format:Y-m-d'
]);
if ($validator->fails()) {
return new StatusResource([
'status' => 'KO', 'code' => 104,
'message' => 'Error on required fields.', 'errors' => $validator->errors()
]);
}
$brandId = $request->brand_id;
$customer = Customer::find($id);
if (!$customer) {
return new StatusResource(['status' => 'KO', 'code' => 105, 'message' => 'User not found.']);
}
$CustomerInfo = $customer->infos()->where('brand_id', $brandId)->first();
if (!$CustomerInfo) {
return new StatusResource(['status' => 'KO', 'code' => 105, 'message' => 'User not found.']);
}
if ($request->has('firstname')) {
$CustomerInfo->firstname = filter_var($request->firstname, FILTER_SANITIZE_STRING);
}
if ($request->has('lastname')) {
$CustomerInfo->lastname = filter_var($request->lastname, FILTER_SANITIZE_STRING);
}
if ($request->has('birth_date')) {
if (!empty($request->birth_date)) {
$CustomerInfo->birth_date = filter_var($request->birth_date, FILTER_SANITIZE_STRING);
} else {
$CustomerInfo->birth_date = null;
}
}
if ($request->has('address')) {
if (!empty($request->address)) {
$CustomerInfo->address = filter_var($request->address, FILTER_SANITIZE_STRING);
} else {
$CustomerInfo->address = null;
}
}
if ($request->has('address2')) {
if (!empty($request->address2)) {
$CustomerInfo->address2 = filter_var($request->address2, FILTER_SANITIZE_STRING);
} else {
$CustomerInfo->address2 = null;
}
}
if ($request->has('phone')) {
if (!empty($request->phone)) {
$CustomerInfo->phone = filter_var($request->phone, FILTER_SANITIZE_STRING);
} else {
$CustomerInfo->phone = null;
}
}
if ($request->has('country')) {
try {
$CustomerInfo->country = Country::make(filter_var($request->country, FILTER_SANITIZE_STRING));
} catch (\Exception $e) {
$CustomerInfo->country = Country::france();
}
}
if ($request->has('city')) {
if (!empty($request->city)) {
$CustomerInfo->city = filter_var($request->city, FILTER_SANITIZE_STRING);
} else {
$CustomerInfo->city = null;
}
}
if ($request->has('postcode')) {
if (!empty($request->postcode)) {
$CustomerInfo->postcode = filter_var($request->postcode, FILTER_SANITIZE_STRING);
} else {
$CustomerInfo->postcode = null;
}
}
if ($request->has('opt_in')) {
$optIn = filter_var($request->opt_in, FILTER_SANITIZE_STRING);
if ($CustomerInfo->opt_in != $optIn) {
// Ajout dans la table d'historique des opt-in de la nouvelle valeur.
$optInHistory = new CustomerOptInHistory();
$optInHistory->customer_id = $customer->id;
$optInHistory->brand_id = $brandId;
$optInHistory->new_value = $optIn;
$optInHistory->save();
}
$CustomerInfo->opt_in = $optIn;
}
$CustomerInfo->save();
return new CustomerResource($customer, $brandId);
}
/**
* Authenticate the user
* @param Request $request
* @return CustomerResource|StatusResource
*/
public function authenticate(Request $request)
{
$validator = Validator::make($request->all(), [
'email' => 'required|email',
'password' => 'required',
]);
if ($validator->fails()) {
return new StatusResource([
'status' => 'KO', 'code' => 115,
'message' => 'Error on required fields.', 'errors' => $validator->errors()
]);
}
$brandId = $request->brand_id;
$email = filter_var($request->email, FILTER_SANITIZE_EMAIL);
$password = trim($request->password);
$customer = Customer::where('email', $email)->first();
if ($customer) {
$login = $customer->logins()->where('brand_id', $brandId)->first();
if ($login) {
if (Hash::check($password, $login->password)) {
if ($login->status === false) {
$infos = $customer->infos()->where('brand_id', $brandId)->first();
// "Le compte de l'utilisateur n'est pas activé"
return new StatusResource(
[
'status' => 'KO',
'code' => 116,
'message' => 'Account not activated.',
'user' => [
'crm_id' => $customer->id,
'firstname' => $infos->firstname,
'lastname' => $infos->lastname,
'email' => $customer->email,
'birth_date' => $infos->birth_date,
'address' => $infos->address,
'address2' => $infos->address2,
'phone' => $infos->phone,
'country' => $infos->country->label,
'city' => $infos->city,
'postcode' => $infos->postcode,
'opt_in' => $infos->opt_in,
'hashcode' => $infos->hashcode,
'status' => $login->status,
]
]
);
}//end if
$login->last_connection = now();
$customer->logins()->save($login);
return new CustomerResource($customer, $brandId);
}//end if
// "Mot de passe incorrect"
return new StatusResource(['status' => 'KO', 'code' => 117, 'message' => 'Authentication failed.']);
} else {
// "Ce compte n'est pas enregistré pour cette marque"
return new StatusResource(['status' => 'KO', 'code' => 118, 'message' => 'User not found.']);
}//end if
} else {
// "Ce compte n'existe pas dans la CRM"
return new StatusResource(['status' => 'KO', 'code' => 119, 'message' => 'User not found.']);
}//end if
}
/**
* Change the password of the user for the brand
* @param Request $request
* @param mixed $id
* @return StatusResource
*/
public function changePassword(Request $request, $id)
{
$validator = Validator::make($request->all(), [
'password' => 'required',
]);
if ($validator->fails()) {
return new StatusResource([
'status' => 'KO', 'code' => 107,
'message' => 'Error on required fields.', 'errors' => $validator->errors()
]);
}
$brandId = $request->brand_id;
$customer = Customer::find($id);
$password = trim($request->password);
if ($customer) {
$customerLogin = $customer->logins()->where('brand_id', $brandId)->first();
if ($customerLogin) {
// Check if same password.
if (Hash::check($password, $customerLogin->password)) {
return new StatusResource(['status' => 'KO', 'code' => 123, 'message' => 'Same password.']);
}
$customerLogin->password = Hash::make($password);
$customerLogin->save();
} else {
// "Ce compte n'est pas enregistré pour cette marque"
return new StatusResource(['status' => 'KO', 'code' => 108, 'message' => 'User not found.']);
}
} else {
// "Ce compte n'existe pas"
return new StatusResource(['status' => 'KO', 'code' => 109, 'message' => 'User not found.']);
}
return new StatusResource(['status' => 'OK', 'message' => 'Update complete.']);
}
/**
* Activate the account of the user
* @param Request $request
* @param mixed $id
* @return StatusResource
*/
public function activateAccount(Request $request, $id)
{
$brandId = $request->brand_id;
$customer = Customer::find($id);
if ($customer) {
$login = $customer->logins()->where('brand_id', $brandId)->first();
if ($login) {
if ($login->status === false) {
$login->status = true;
$login->save();
// "Le compte a bien été activé"
return new StatusResource(['status' => 'OK', 'message' => 'Update complete.']);
} else {
// "Ce compte est déjà activé"
return new StatusResource(['status' => 'KO', 'code' => 110, 'message' => 'User already active.']);
}
} else {
// "Ce compte n'est pas enregistré pour cette marque"
return new StatusResource(['status' => 'KO', 'code' => 111, 'message' => 'User not found.']);
}
} else {
// "Cet utilisateur n'existe pas dans la CRM"
return new StatusResource(['status' => 'KO', 'code' => 112, 'message' => 'User not found.']);
}
}
/**
* Delete the user account for the brand
* @param Request $request
* @param mixed $id
*/
// phpcs:disable Squiz.PHP.NonExecutableCode.Unreachable, Squiz.Commenting.FunctionComment.Missing
public function delete(Request $request, $id)
{
die("Not implemented yet");
// TODO : Gestion avec mesBR.
$brandId = $request->brand_id;
$customer = Customer::find($id);
if ($customer) {
$customer->infos()->where('brand_id', $brandId)->first()->delete();
$customer->logins()->where('brand_id', $brandId)->first()->delete();
if (count($customer->infos) === 0 && count($customer->logins) === 0) {
$customer->optInHistory()->delete();
$customer->delete();
// Suppression du compte de la CRM terminée.
return new StatusResource(['status' => 'OK', 'message' => 'Deletion complete.']);
} else {
// Suppression des informations du compte pour la marque terminée.
return new StatusResource(['status' => 'OK', 'message' => 'Deletion complete.']);
}
} else {
return new StatusResource(['status' => 'KO', 'code' => 106, 'message' => 'User not found.']);
}
// phpcs:enable Squiz.PHP.NonExecutableCode.Unreachable, Squiz.Commenting.FunctionComment.Missing
}
/**
* Change the status of the opt-in for the customer on this brand
* @param Request $request
* @param mixed $id
* @return StatusResource
*/
public function toggleOptin(Request $request, $id)
{
// TODO : Liaison des opt-in avec MailJet.
$brandId = $request->brand_id;
$customer = Customer::find($id);
if ($customer) {
$infos = $customer->infos()->where('brand_id', $brandId)->first();
if ($infos) {
$infos->opt_in = $infos->opt_in === 0 ? 1 : 0;
// On enregistre l'historique du changement d'optin.
$optinHistory = new CustomerOptInHistory();
$optinHistory->customer_id = $customer->id;
$optinHistory->new_value = $infos->opt_in;
$optinHistory->brand_id = $brandId;
$optinHistory->save();
$infos->save();
// "Changement effectué"
return new StatusResource(['status' => 'OK', 'message' => 'Update complete.']);
} else {
// "Ce compte n'a pas d'information liés à cette marque"
return new StatusResource(['status' => 'KO', 'code' => 113, 'message' => 'User not found.']);
}
} else {
// "Cet utilisateur n'existe pas dans la CRM"
return new StatusResource(['status' => 'KO', 'code' => 114, 'message' => 'User not found.']);
}//end if
}
/**
* Remove opt-in for the customer on this brand
* @param Request $request
* @param mixed $hashcode
* @return StatusResource
*/
public function removeOptin(Request $request, $hashcode)
{
// TODO : Liaison des opt-in avec MailJet.
$brandId = $request->brand_id;
$infos = CustomerInfo::where([
['brand_id', $brandId],
['hashcode', $hashcode]
])->first();
if ($infos) {
$infos->opt_in = 0;
// On enregistre l'historique du changement d'optin.
$optinHistory = new CustomerOptInHistory();
$optinHistory->customer_id = $infos->customer_id;
$optinHistory->new_value = 0;
$optinHistory->brand_id = $brandId;
$optinHistory->save();
$infos->save();
// "Changement effectué"
return new StatusResource(['status' => 'OK', 'message' => 'Update complete.']);
} else {
// "Ce compte n'a pas d'information liés à cette marque"
return new StatusResource(['status' => 'KO', 'code' => 122, 'message' => 'User not found.']);
}
}
/**
* @param Request $request
* @return void
*/
// phpcs:ignore Squiz.PHP.NonExecutableCode.Unreachable, Squiz.Commenting.FunctionComment.Missing
public function addSingleOptin(Request $request)
{
$validator = Validator::make($request->all(), [
'email' => 'required|email',
]);
if ($validator->fails()) {
return new StatusResource([
'status' => 'KO', 'code' => 120,
'message' => 'Error on required fields.', 'errors' => $validator->errors()
]);
}
$brandId = $request->brand_id;
$email = filter_var($request->email, FILTER_SANITIZE_EMAIL);
// Check si le customer ou l'optin simple existe avant de l'ajouter en optin simple.
if (
!SingleOptin::where([['email', $email], ['brand_id', $brandId]])->exists() &&
!CustomerInfo::join("customers", "customer_infos.customer_id", "=", "customers.id")->where([
['brand_id', $brandId], ['email', $email]
])->exists()
) {
$singleOptin = new SingleOptin();
$singleOptin->email = $email;
$singleOptin->brand_id = $brandId;
$singleOptin->save();
} else {
// "Cet utilisateur est déjà inscrit à la newsletter pour cette marque"
return new StatusResource([
'status' => 'KO', 'code' => 121,
'message' => 'This user is already subscribed to the newsletter for this brand.'
]);
}
return new StatusResource(['status' => 'OK', 'message' => 'Optin added.']);
}
}

View file

@ -4,32 +4,43 @@ namespace App\Http\Controllers\Api\V2;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Http\Requests\Api\V2\StoreCustomerAnswers; use App\Http\Requests\Api\V2\StoreCustomerAnswers;
use App\Models\Answer;
use App\Models\Customer; use App\Models\Customer;
use App\Models\Service; use App\Models\ModelAnswer;
use Illuminate\Http\JsonResponse; use App\Models\Question;
class AnswerController extends Controller class AnswerController extends Controller
{ {
/** /**
* Store the customer answers * Store the customer answers.
* *
* @param StoreCustomerAnswers $request * @param \App\Http\Requests\Api\V2\StoreCustomerAnswers $request
* @param Service $service * @param \App\Models\Customer $customer
* @param Customer $customer * @return \Illuminate\Http\JsonResponse
* @return JsonResponse
*/ */
public function storeCustomerAnswers( public function storeCustomerAnswers(
StoreCustomerAnswers $request, StoreCustomerAnswers $request,
Service $service,
Customer $customer Customer $customer
): JsonResponse { ): \Illuminate\Http\JsonResponse {
// * Make sure we are accessing a customer that belongs to our service. // * Make sure we are accessing a customer that belongs to our service.
$this->authorize('customerBelongsToService', $customer); $this->authorize('customerBelongsToService', $customer);
$existingAnswerIds = $customer->getAnswers($service)->pluck('id'); $question = Question::find($request->validated('question'));
$answersID = Answer::whereIn('id', $request->answers)->pluck('id');
$customer->answers()->sync($answersID->merge($existingAnswerIds)); // Column model_id is varchar because of singleoptin uuid, so force cast in string.
ModelAnswer::query()
->where('model_type', Customer::class)
->where('model_id', (string) $customer->getKey())
->whereIn('answer_id', $question->answers->modelKeys())
->delete();
foreach ($request->validated('answers') as $answerId) {
ModelAnswer::create([
'model_type' => Customer::class,
'model_id' => (string) $customer->getKey(),
'answer_id' => $answerId,
]);
}
return response()->json(); return response()->json();
} }
} }

View file

@ -8,9 +8,9 @@ use Illuminate\Http\JsonResponse;
class ApiController extends Controller class ApiController extends Controller
{ {
/** /**
* Just returns HTTP 200 response * Just returns HTTP 200 response.
* *
* @return JsonResponse * @return \Illuminate\Http\JsonResponse
*/ */
public function ping(): JsonResponse public function ping(): JsonResponse
{ {

View file

@ -29,34 +29,34 @@ use Illuminate\Support\Str;
class ArcadiaController extends Controller class ArcadiaController extends Controller
{ {
/** /**
* Send arcadia default mail (reclamations) * Send arcadia default mail (reclamations).
* *
* @param SendArchadiaDefaultMailRequest $request * @param \App\Http\Requests\Api\V2\SendArchadiaDefaultMailRequest $request
* @return JsonResponse * @return \Illuminate\Http\JsonResponse
*/ */
public function outgoingMailDefault(SendArchadiaDefaultMailRequest $request): JsonResponse public function outgoingMailDefault(SendArchadiaDefaultMailRequest $request): JsonResponse
{ {
return $this->handleArcadiaMail($request, ArcadiaMailType::default()); return $this->handleArcadiaMail($request, ArcadiaMailType::default);
} }
/** /**
* Send arcadia game mail (games) * Send arcadia game mail (games).
* *
* @param SendArchadiaGameMailRequest $request * @param \App\Http\Requests\Api\V2\SendArchadiaGameMailRequest $request
* @return JsonResponse * @return \Illuminate\Http\JsonResponse
*/ */
public function outgoingMailGame(SendArchadiaGameMailRequest $request): JsonResponse public function outgoingMailGame(SendArchadiaGameMailRequest $request): JsonResponse
{ {
return $this->handleArcadiaMail($request, ArcadiaMailType::game()); return $this->handleArcadiaMail($request, ArcadiaMailType::game);
} }
/** /**
* Form xml message and save it in the apropriate storage location * Form xml message and save it in the apropriate storage location
* to be picked up by the arcadia job * to be picked up by the arcadia job.
* *
* @param BaseApiRequest $request * @param \App\Http\Requests\Api\V2\BaseApiRequest $request
* @param ArcadiaMailType $type * @param \App\Enums\ArcadiaMailType $type
* @return JsonResponse * @return \Illuminate\Http\JsonResponse
* @throws \Exception If an error occurs while generating and saving the xml file. * @throws \Exception If an error occurs while generating and saving the xml file.
*/ */
private function handleArcadiaMail(BaseApiRequest $request, ArcadiaMailType $type): JsonResponse private function handleArcadiaMail(BaseApiRequest $request, ArcadiaMailType $type): JsonResponse
@ -67,14 +67,14 @@ class ArcadiaController extends Controller
$xml = 'none'; $xml = 'none';
try { try {
$customerInfo = \array_intersect_key($request->all()['deliver'], [ $customerInfo = \array_intersect_key($request->all()['deliver'], [
'email' => '', 'email' => '',
'firstname' => '', 'firstname' => '',
'lastname' => '', 'lastname' => '',
'address' => '', 'address' => '',
'address2' => '', 'address2' => '',
'city' => '', 'city' => '',
'postcode' => '', 'postcode' => '',
'phone' => '' 'phone' => ''
]); ]);
$orderType = ArcadiaOrderType::make(intval($request->state_order)); $orderType = ArcadiaOrderType::make(intval($request->state_order));
$xml = $this->makeXml($request, $type, $orderType, $customerInfo); $xml = $this->makeXml($request, $type, $orderType, $customerInfo);
@ -90,7 +90,7 @@ class ArcadiaController extends Controller
Storage::disk('arcadia')->put(Str::slug(sprintf( Storage::disk('arcadia')->put(Str::slug(sprintf(
'v2_order_%s_%s_%s%s', 'v2_order_%s_%s_%s%s',
$request->order_num, $request->order_num,
$type->label, $type->label(),
date('Ymd_His'), date('Ymd_His'),
\microtime(true) \microtime(true)
)) . '.xml', $xml); )) . '.xml', $xml);
@ -109,16 +109,16 @@ class ArcadiaController extends Controller
} }
throw $e; throw $e;
// @codeCoverageIgnoreEnd // @codeCoverageIgnoreEnd
}//end try } //end try
} }
/** /**
* render Arcadia xml for order * Render Arcadia xml for order.
* *
* @param BaseApiRequest $request * @param \App\Http\Requests\Api\V2\BaseApiRequest $request
* @param ArcadiaMailType $type * @param \App\Enums\ArcadiaMailType $type
* @param ArcadiaOrderType $orderType * @param \App\Enums\ArcadiaOrderType $orderType
* @param array $customerInfo * @param array $customerInfo
* @return string * @return string
*/ */
private function makeXml( private function makeXml(
@ -127,12 +127,12 @@ class ArcadiaController extends Controller
ArcadiaOrderType $orderType, ArcadiaOrderType $orderType,
array $customerInfo array $customerInfo
): string { ): string {
$arcadiaMailType = $type->equals(ArcadiaMailType::default()) ? $arcadiaMailType = $type->equals(ArcadiaMailType::default) ?
'mail.arcadiaOrders.default' : 'mail.arcadiaOrders.game'; 'mail.arcadiaOrders.default' : 'mail.arcadiaOrders.game';
return view($arcadiaMailType, [ return view($arcadiaMailType, [
'request' => $request, 'request' => $request,
'type' => $type, 'type' => $type,
'orderType' => $orderType, 'orderType' => $orderType,
'customerInfo' => $customerInfo 'customerInfo' => $customerInfo
])->render(); ])->render();
} }

View file

@ -14,6 +14,7 @@ use App\Jobs\UpdateCustomerLastConnection;
use App\Models\Customer; use App\Models\Customer;
use App\Models\Service; use App\Models\Service;
use App\Models\SingleOptin; use App\Models\SingleOptin;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\ModelNotFoundException; use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
use Illuminate\Http\Resources\Json\JsonResource; use Illuminate\Http\Resources\Json\JsonResource;
@ -24,18 +25,19 @@ class CustomerController extends Controller
/** /**
* Creates a new customer for the specified service. * Creates a new customer for the specified service.
* *
* @param StoreCustomerRequest $request * @param \App\Http\Requests\Api\V2\StoreCustomerRequest $request
* @param Service $service * @param \App\Models\Service $service
* @return JsonResource * @return \Illuminate\Http\Resources\Json\JsonResource
* @throws ApiErrorException With ApiError::customerExists if the customer with this email already exists. * @throws \App\Exceptions\Api\V2\ApiErrorException With ApiError::customerExists if
* @throws CrmException If saving the customer to DB failed. * the customer with this email already exists.
* @throws \App\Exceptions\Api\V2\CrmException If saving the customer to DB failed.
*/ */
public function store(StoreCustomerRequest $request, Service $service): JsonResource public function store(StoreCustomerRequest $request, Service $service): JsonResource
{ {
$data = (object)$request->validated(); $data = (object) $request->validated();
/** @var \App\Models\Customer */ /** @var \App\Models\Customer */
$customer = Customer::where('email', $data->email)->firstOrCreate([ $customer = Customer::query()->where('email', $data->email)->firstOrCreate([
'email' => $data->email 'email' => $data->email
]); ]);
@ -46,8 +48,10 @@ class CustomerController extends Controller
// * Delete Single optin if exists. // * Delete Single optin if exists.
/** @var \App\Models\SingleOptin|null $singleOptin */ /** @var \App\Models\SingleOptin|null $singleOptin */
$singleOptin = SingleOptin::where('email', $data->email) $singleOptin = SingleOptin::query()->where('email', $data->email)
->where('brand_id', $service->brand->id)->first(); ->whereHas('brand', function (Builder $q) use ($service) {
$q->where('id', $service->brand->id);
})->first();
if ($singleOptin) { if ($singleOptin) {
$saved = $singleOptin->delete() && $saved; $saved = $singleOptin->delete() && $saved;
} }
@ -55,7 +59,7 @@ class CustomerController extends Controller
/** @var \App\Models\CustomerInfo */ /** @var \App\Models\CustomerInfo */
$customerInfo = $customer->info($service->brand, Customer::FIRST_OR_NEW); $customerInfo = $customer->info($service->brand, Customer::FIRST_OR_NEW);
if ($customerInfo->exists) { if ($customerInfo->exists) {
throw new ApiErrorException(ApiError::customerExists()); throw new ApiErrorException(ApiError::customerExists);
} }
$customerInfo->fill($request->validated()); $customerInfo->fill($request->validated());
$customerInfo->brand()->associate($service->brand); $customerInfo->brand()->associate($service->brand);
@ -80,11 +84,11 @@ class CustomerController extends Controller
} }
/** /**
* Renders the specific customer infos * Renders the specific customer infos.
* *
* @param Service $service * @param \App\Models\Service $service
* @param Customer $customer * @param \App\Models\Customer $customer
* @return JsonResource * @return \Illuminate\Http\Resources\Json\JsonResource
*/ */
public function show(Service $service, Customer $customer): JsonResource public function show(Service $service, Customer $customer): JsonResource
{ {
@ -95,13 +99,13 @@ class CustomerController extends Controller
} }
/** /**
* Updates information for a specific customer * Updates information for a specific customer.
* *
* @param UpdateCustomerRequest $request * @param \App\Http\Requests\Api\V2\UpdateCustomerRequest $request
* @param Service $service * @param \App\Models\Service $service
* @param Customer $customer * @param \App\Models\Customer $customer
* @return JsonResource * @return \Illuminate\Http\Resources\Json\JsonResource
* @throws CrmException If saving the customer to DB failed. * @throws \App\Exceptions\Api\V2\CrmException If saving the customer to DB failed.
*/ */
public function update(UpdateCustomerRequest $request, Service $service, Customer $customer): JsonResource public function update(UpdateCustomerRequest $request, Service $service, Customer $customer): JsonResource
{ {
@ -137,19 +141,18 @@ class CustomerController extends Controller
} }
/** /**
* Deletes customer information for a specific service * Deletes customer information for a specific service.
* *
* @param Service $service * @param \App\Models\Service $service
* @param Customer $customer * @param \App\Models\Customer $customer
* @return JsonResponse * @return \Illuminate\Http\JsonResponse
* @throws CrmException If removing the customer off the DB failed. * @throws \App\Exceptions\Api\V2\CrmException If removing the customer off the DB failed.
*/ */
public function destroy(Service $service, Customer $customer): JsonResponse public function destroy(Service $service, Customer $customer): JsonResponse
{ {
// * Make sure we are accessing a customer that belongs to our service. // * Make sure we are accessing a customer that belongs to our service.
$this->authorize('customerBelongsToService', $customer); $this->authorize('customerBelongsToService', $customer);
// TODO: Gestion avec mesBR.
/** @var \App\Models\CustomerInfo */ /** @var \App\Models\CustomerInfo */
$customerInfo = $customer->info($service->brand, Customer::FIRST_OR_FAIL); $customerInfo = $customer->info($service->brand, Customer::FIRST_OR_FAIL);
/** @var \App\Models\CustomerLogin */ /** @var \App\Models\CustomerLogin */
@ -189,27 +192,30 @@ class CustomerController extends Controller
} }
/** /**
* Authenticate the user * Authenticate the user.
* @param AuthenticateRequest $request *
* @param Service $service * @param \App\Http\Requests\Api\V2\AuthenticateRequest $request
* @return JsonResource * @param \App\Models\Service $service
* @throws ApiErrorException With ApiError::authFailed() in case of wrong credentials. * @return \Illuminate\Http\Resources\Json\JsonResource
* @throws ApiErrorException With ApiError::customerNotActivated() if customer account is not activated. * @throws \App\Exceptions\Api\V2\ApiErrorException With ApiError::authFailed in case
* of wrong credentials.
* @throws \App\Exceptions\Api\V2\ApiErrorException With ApiError::customerNotActivated if
* customer account is not activated.
*/ */
public function authenticate(AuthenticateRequest $request, Service $service): JsonResource public function authenticate(AuthenticateRequest $request, Service $service): JsonResource
{ {
try { try {
/** @var \App\Models\Customer */ /** @var \App\Models\Customer */
$customer = Customer::where('email', $request->email)->firstOrFail(); $customer = Customer::query()->where('email', $request->email)->firstOrFail();
/** @var \App\Models\CustomerLogin $customerLogin */ /** @var \App\Models\CustomerLogin $customerLogin */
$customerLogin = $customer->login($service->brand, Customer::FIRST_OR_FAIL); $customerLogin = $customer->login($service->brand, Customer::FIRST_OR_FAIL);
if (!$customerLogin->passwordMatch($request->password)) { if (!$customerLogin->passwordMatch($request->password)) {
throw new ApiErrorException(ApiError::authFailed()); throw new ApiErrorException(ApiError::authFailed);
} }
if (!$customerLogin->status) { if (!$customerLogin->status) {
throw new ApiErrorException(ApiError::customerNotActivated()); throw new ApiErrorException(ApiError::customerNotActivated);
} }
UpdateCustomerLastConnection::dispatch($customer, $customerLogin, now()) UpdateCustomerLastConnection::dispatch($customer, $customerLogin, now())
@ -217,19 +223,21 @@ class CustomerController extends Controller
return new CustomerResource($service->brand, $customer); return new CustomerResource($service->brand, $customer);
} catch (ModelNotFoundException $e) { } catch (ModelNotFoundException $e) {
throw new ApiErrorException(ApiError::authFailed()); throw new ApiErrorException(ApiError::authFailed);
}//end try } //end try
} }
/** /**
* Activate the account of the user * Activate the account of the user.
* @param Customer $customer *
* @param Service $service * @param \App\Models\Customer $customer
* @param \App\Models\Service $service
* @return \Illuminate\Http\JsonResponse * @return \Illuminate\Http\JsonResponse
* @throws ApiErrorException With ApiError::customerAlreadyActive if customer account is already active. * @throws \App\Exceptions\Api\V2\ApiErrorException With ApiError::customerAlreadyActive
* @throws CrmException If saving the customer to DB failed. * if customer account is already active.
* @throws \App\Exceptions\Api\V2\CrmException If saving the customer to DB failed.
*/ */
public function activateAccount(Customer $customer, Service $service) public function activateAccount(Customer $customer, Service $service): JsonResponse
{ {
// * Make sure we are accessing a customer that belongs to our service. // * Make sure we are accessing a customer that belongs to our service.
$this->authorize('customerBelongsToService', $customer); $this->authorize('customerBelongsToService', $customer);
@ -238,7 +246,7 @@ class CustomerController extends Controller
$login = $customer->login($service->brand, Customer::FIRST_OR_FAIL); $login = $customer->login($service->brand, Customer::FIRST_OR_FAIL);
if ($login->status) { if ($login->status) {
throw new ApiErrorException(ApiError::customerAlreadyActive()); throw new ApiErrorException(ApiError::customerAlreadyActive);
} }
$login->status = true; $login->status = true;

View file

@ -6,7 +6,9 @@ use App\Enums\ApiError;
use App\Exceptions\Api\V2\ApiErrorException; use App\Exceptions\Api\V2\ApiErrorException;
use App\Exceptions\Api\V2\CrmException; use App\Exceptions\Api\V2\CrmException;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Http\Requests\Api\V2\BaseApiRequest;
use App\Http\Requests\Api\V2\SingleOptinRequest; use App\Http\Requests\Api\V2\SingleOptinRequest;
use App\Http\Requests\Api\V2\SingleOptinRemoveWithHashcodeRequest;
use App\Models\CustomerInfo; use App\Models\CustomerInfo;
use App\Models\Service; use App\Models\Service;
use App\Models\SingleOptin; use App\Models\SingleOptin;
@ -14,35 +16,64 @@ use App\Models\SingleOptin;
class OptinController extends Controller class OptinController extends Controller
{ {
/** /**
* Creates a new Single optin or destroys it if exists * Creates a new Single optin or destroys it if exists.
* *
* @param SingleOptinRequest $request * @param \App\Http\Requests\Api\V2\SingleOptinRequest $request
* @param Service $service * @param \App\Models\Service $service
* @return JsonResponse * @return \Illuminate\Http\JsonResponse
* @throws ApiErrorException With ApiError::singleOptinCustomerExists if this email already has a full account. * @throws \App\Exceptions\Api\V2\ApiErrorException With ApiError::singleOptinCustomerExists
* @throws CrmException If the single optin could not be added or removed. * if this email already has a full account.
* @throws \App\Exceptions\Api\V2\CrmException If the single optin could not be added or removed.
*/ */
public function optin(SingleOptinRequest $request, Service $service) public function optin(SingleOptinRequest $request, Service $service): \Illuminate\Http\JsonResponse
{ {
if (CustomerInfo::getFromEmail($service->brand, $request->email)->exists()) { $validatedData = $request->validated();
throw new ApiErrorException(ApiError::singleOptinCustomerExists()); if (CustomerInfo::getFromEmail($service->brand, $validatedData['email'])->exists()) {
throw new ApiErrorException(ApiError::singleOptinCustomerExists);
} }
/** @var \App\Models\SingleOptin */ /** @var \App\Models\SingleOptin */
$singleOptin = SingleOptin::where('email', $request->email) $singleOptin = SingleOptin::query()
->whereHas('brand', function ($q) use ($service) { ->where('email', $validatedData['email'])
$q->whereId($service->brand->id); ->whereHas('brand', function ($q) use ($service) {
})->firstOrNew(); $q->where('id', $service->brand->id);
})
->firstOrNew();
if ($request->opt_in) { if (!$this->changeOptin($service, $request, \boolval($validatedData['opt_in']), $singleOptin)) {
$singleOptin->fill($request->validated()); // @codeCoverageIgnoreStart
$singleOptin->brand()->associate($service->brand); throw new CrmException('Api failed to update single optin.');
$success = $singleOptin->save(); // @codeCoverageIgnoreEnd
} else {
$success = $singleOptin->delete() !== false;
} }
if (!$success) { if (isset($validatedData['answer_id'])) {
$singleOptin->answers()->attach($validatedData['answer_id']);
}
return response()->json();
}
/**
* Creates a new Single optin or destroys it if exists using hashcode.
*
* @param \App\Http\Requests\Api\V2\SingleOptinRemoveWithHashcodeRequest $request
* @param \App\Models\Service $service
* @return \Illuminate\Http\JsonResponse
* @throws \App\Exceptions\Api\V2\CrmException If the single optin could not be removed.
*/
public function optoutWithHashcode(
SingleOptinRemoveWithHashcodeRequest $request,
Service $service
): \Illuminate\Http\JsonResponse {
/** @var \App\Models\SingleOptin */
$singleOptin = SingleOptin::query()
->where('hashcode', $request->hashcode)
->whereHas('brand', function ($q) use ($service) {
$q->whereId($service->brand->id);
})
->firstOrFail();
if ($singleOptin and !$this->changeOptin($service, $request, \boolval($request->opt_in), $singleOptin)) {
// @codeCoverageIgnoreStart // @codeCoverageIgnoreStart
throw new CrmException('Api failed to update single optin.'); throw new CrmException('Api failed to update single optin.');
// @codeCoverageIgnoreEnd // @codeCoverageIgnoreEnd
@ -50,4 +81,23 @@ class OptinController extends Controller
return response()->json(); return response()->json();
} }
/**
* Changes the optin state.
*
* @param \App\Models\Service $service
* @param \App\Http\Requests\Api\V2\BaseApiRequest $request
* @param boolean $optin
* @param \App\Models\SingleOptin $singleOptin
* @return boolean Returns false in case of failure.
*/
private function changeOptin(Service $service, BaseApiRequest $request, bool $optin, SingleOptin $singleOptin): bool
{
if ($optin) {
$singleOptin->fill($request->validated());
$singleOptin->brand()->associate($service->brand);
return $singleOptin->save();
}
return $singleOptin->delete() !== false;
}
} }

View file

@ -16,8 +16,8 @@ class QuestionController extends Controller
/** /**
* Returns all service questions. * Returns all service questions.
* *
* @param Service $service * @param \App\Models\Service $service
* @return JsonResource * @return \Illuminate\Http\Resources\Json\ResourceCollection
*/ */
public function index(Service $service): ResourceCollection public function index(Service $service): ResourceCollection
{ {
@ -27,9 +27,9 @@ class QuestionController extends Controller
/** /**
* Returns all service questions with user answers for all of them. * Returns all service questions with user answers for all of them.
* *
* @param Service $service * @param \App\Models\Service $service
* @param Customer $customer * @param \App\Models\Customer $customer
* @return JsonResource * @return \Illuminate\Http\Resources\Json\ResourceCollection
*/ */
public function indexWithResponse(Service $service, Customer $customer): ResourceCollection public function indexWithResponse(Service $service, Customer $customer): ResourceCollection
{ {
@ -43,19 +43,19 @@ class QuestionController extends Controller
/** /**
* Returns all service questions that was not answered by a user. * Returns all service questions that was not answered by a user.
* *
* @param Service $service * @param \App\Models\Service $service
* @param Customer $customer * @param \App\Models\Customer $customer
* @return JsonResource * @return \Illuminate\Http\Resources\Json\JsonResource
*/ */
public function unAnsweredBy(Service $service, Customer $customer): JsonResource public function unAnsweredBy(Service $service, Customer $customer): JsonResource
{ {
// * Make sure we are accessing a customer that belongs to our service. // * Make sure we are accessing a customer that belongs to our service.
$this->authorize('customerBelongsToService', $customer); $this->authorize('customerBelongsToService', $customer);
$questions = Question::whereHas('brand', function ($q) use ($service) { $questions = Question::query()->whereHas('brand', function ($q) use ($service) {
$q->where('id', $service->brand->id); $q->where('id', $service->brand->id);
})->whereDoesntHave('answers', function ($q) use ($customer) { })->whereDoesntHave('answers', function ($q) use ($customer) {
$q->whereIn('id', Answer::whereHas('customers', function ($q) use ($customer) { $q->whereIn('id', Answer::query()->whereHas('customers', function ($q) use ($customer) {
$q->where('id', $customer->id); $q->where('id', $customer->id);
})->pluck('id')); })->pluck('id'));
})->with('answers')->get(); })->with('answers')->get();

View file

@ -8,39 +8,42 @@ use App\Http\Requests\Api\V2\GetQuestionStatsRequest;
use App\Http\Resources\Api\V2\BrandStatsResource; use App\Http\Resources\Api\V2\BrandStatsResource;
use App\Http\Resources\Api\V2\QuestionStatsResource; use App\Http\Resources\Api\V2\QuestionStatsResource;
use App\Models\BrandStat; use App\Models\BrandStat;
use App\Models\CustomerAnswer; use App\Models\ModelAnswer;
use App\Models\Question; use App\Models\Question;
use App\Models\Service; use App\Models\Service;
use Illuminate\Http\Resources\Json\ResourceCollection;
use Illuminate\Support\Carbon; use Illuminate\Support\Carbon;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
class StatsController extends Controller class StatsController extends Controller
{ {
/** /**
* Get specific brand statistics * Get specific brand statistics.
* *
* @param GetBrandStatsRequest $request * @param \App\Http\Requests\Api\V2\GetBrandStatsRequest $request
* @param Service $service * @param \App\Models\Service $service
* @return ResourceCollection * @return \Illuminate\Http\Resources\Json\ResourceCollection
*/ */
public function brandStats(GetBrandStatsRequest $request, Service $service): ResourceCollection public function brandStats(
{ GetBrandStatsRequest $request,
$stats = BrandStat::whereDate('created_at', '>=', $request->start) Service $service
): \Illuminate\Http\Resources\Json\ResourceCollection {
$stats = BrandStat::query()->whereDate('created_at', '>=', $request->start)
->whereDate('created_at', '<=', $request->end) ->whereDate('created_at', '<=', $request->end)
->whereBrandId($service->brand->id)->get(); ->whereBrandId($service->brand->id)->get();
return new BrandStatsResource($stats); return new BrandStatsResource($stats);
} }
/** /**
* Get statistics for a specific question * Get statistics for a specific question.
* *
* @param GetQuestionStatsRequest $request * @param \App\Http\Requests\Api\V2\GetQuestionStatsRequest $request
* @param Question $question * @param \App\Models\Question $question
* @return QuestionStatsResource * @return \App\Http\Resources\Api\V2\QuestionStatsResource
*/ */
public function questionStats(GetQuestionStatsRequest $request, Question $question): QuestionStatsResource public function questionStats(
{ GetQuestionStatsRequest $request,
Question $question
): \App\Http\Resources\Api\V2\QuestionStatsResource {
// * Make sure we are accessing a customer that belongs to our service. // * Make sure we are accessing a customer that belongs to our service.
$this->authorize('questionBelongsToService', $question); $this->authorize('questionBelongsToService', $question);
@ -55,32 +58,36 @@ class StatsController extends Controller
} }
/** /**
* Total réponses par dates * Total réponses par dates.
* *
* @param Collection $answers * @param \Illuminate\Support\Collection $answers
* @param Carbon $start * @param \Illuminate\Support\Carbon $start
* @param Carbon $end * @param \Illuminate\Support\Carbon $end
* @return Collection * @return \Illuminate\Support\Collection
*/ */
private function answersByDays(Collection $answers, Carbon $start, Carbon $end): Collection private function answersByDays(Collection $answers, Carbon $start, Carbon $end): \Illuminate\Support\Collection
{ {
// * Totalize by date all answers. // * Totalize by date all answers.
$answersByDays = CustomerAnswer::whereDate('created_at', '>=', $start) $answersByDays = ModelAnswer::query()
->whereDate('created_at', '>=', $start)
->whereDate('created_at', '<=', $end) ->whereDate('created_at', '<=', $end)
->whereIn('answer_id', $answers->pluck('id')) ->whereIn('answer_id', $answers->pluck('id'))
->get()->map(function ($custAnswer) { ->get()
->map(function ($custAnswer) {
return [ return [
'answer_id' => $custAnswer->answer_id, 'answer_id' => $custAnswer->answer_id,
'created_at' => $custAnswer->created_at->format('Y-m-d'), 'created_at' => $custAnswer->created_at->isoFormat('YYYY-MM-DD'),
]; ];
})->groupBy(['created_at', 'answer_id'])->map(function ($day) { })
->groupBy(['created_at', 'answer_id'])
->map(function ($day) {
return $day->map(function ($custAnswers) { return $day->map(function ($custAnswers) {
return $custAnswers->count(); return $custAnswers->count();
}); });
}); });
do { do {
$currentDate = $start->format('Y-m-d'); $currentDate = $start->isoFormat('YYYY-MM-DD');
/** @var \Illuminate\Support\Collection $answersByDays */ /** @var \Illuminate\Support\Collection $answersByDays */
if (!$answersByDays->has($currentDate)) { if (!$answersByDays->has($currentDate)) {
$answersByDays->put( $answersByDays->put(

View file

@ -0,0 +1,46 @@
<?php
namespace App\Http\Controllers\Api\V4;
use App\Http\Controllers\Controller;
use App\Http\Requests\Api\V4\StoreCustomerAnswers;
use App\Models\Customer;
use App\Models\ModelAnswer;
use App\Models\Question;
class AnswerController extends Controller
{
/**
* Store the customer answers.
*
* @param \App\Http\Requests\Api\V4\StoreCustomerAnswers $request
* @param \App\Models\Customer $customer
* @return \Illuminate\Http\JsonResponse
*/
public function storeCustomerAnswers(
StoreCustomerAnswers $request,
Customer $customer
): \Illuminate\Http\JsonResponse {
// * Make sure we are accessing a customer that belongs to our service.
$this->authorize('customerBelongsToService', $customer);
$question = Question::find($request->validated('question'));
// Column model_id is varchar because of singleoptin uuid, so force cast in string.
ModelAnswer::query()
->where('model_type', Customer::class)
->where('model_id', (string) $customer->getKey())
->whereIn('answer_id', $question->answers->modelKeys())
->delete();
foreach ($request->validated('answers') as $answerId) {
ModelAnswer::create([
'model_type' => Customer::class,
'model_id' => (string) $customer->getKey(),
'answer_id' => $answerId,
]);
}
return response()->json();
}
}

View file

@ -0,0 +1,19 @@
<?php
namespace App\Http\Controllers\Api\V4;
use App\Http\Controllers\Controller;
use Illuminate\Http\JsonResponse;
class ApiController extends Controller
{
/**
* Just returns HTTP 200 response.
*
* @return \Illuminate\Http\JsonResponse
*/
public function ping(): JsonResponse
{
return response()->json();
}
}

View file

@ -0,0 +1,139 @@
<?php
namespace App\Http\Controllers\Api\V4;
use App\Enums\ArcadiaMailType;
use App\Enums\ArcadiaOrderType;
use App\Http\Controllers\Controller;
use App\Http\Requests\Api\V4\BaseApiRequest;
use App\Http\Requests\Api\V4\SendArchadiaDefaultMailRequest;
use App\Http\Requests\Api\V4\SendArchadiaGameMailRequest;
use App\Mail\CourrierArcadiaError;
use DOMDocument;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
/**
* ! Ici c'est du sale à cause d'arcadia
* Ils ont fait le choix de rajouter une fonctionnalité
* sur une methode existante en rajoutant des conditions
* sales. Ils ont donc une seule méthode pour envoyer plusieurs
* types de réclas avec des champs différents...
*
* Donc une séparation est faite sur les endpoints CRM et dans le client
* pour éviter une confusion de nos utilisateurs
*/
class ArcadiaController extends Controller
{
/**
* Send arcadia default mail (reclamations).
*
* @param \App\Http\Requests\Api\V4\SendArchadiaDefaultMailRequest $request
* @return \Illuminate\Http\JsonResponse
*/
public function outgoingMailDefault(SendArchadiaDefaultMailRequest $request): JsonResponse
{
return $this->handleArcadiaMail($request, ArcadiaMailType::default);
}
/**
* Send arcadia game mail (games).
*
* @param \App\Http\Requests\Api\V4\SendArchadiaGameMailRequest $request
* @return \Illuminate\Http\JsonResponse
*/
public function outgoingMailGame(SendArchadiaGameMailRequest $request): JsonResponse
{
return $this->handleArcadiaMail($request, ArcadiaMailType::game);
}
/**
* Form xml message and save it in the apropriate storage location
* to be picked up by the arcadia job.
*
* @param \App\Http\Requests\Api\V4\BaseApiRequest $request
* @param \App\Enums\ArcadiaMailType $type
* @return \Illuminate\Http\JsonResponse
* @throws \Exception If an error occurs while generating and saving the xml file.
*/
private function handleArcadiaMail(BaseApiRequest $request, ArcadiaMailType $type): JsonResponse
{
$request = $request->merge([
'order_num' => str_pad(\intval($request->order_num), 9, '0', STR_PAD_LEFT)
]);
$xml = 'none';
try {
$customerInfo = \array_intersect_key($request->all()['deliver'], [
'email' => '',
'firstname' => '',
'lastname' => '',
'address' => '',
'address2' => '',
'city' => '',
'postcode' => '',
'phone' => ''
]);
$orderType = ArcadiaOrderType::make(intval($request->state_order));
$xml = $this->makeXml($request, $type, $orderType, $customerInfo);
// * Format xml.
$doc = new DOMDocument('1.0', 'utf-8');
$doc->xmlStandalone = true;
$doc->preserveWhiteSpace = false;
$doc->formatOutput = true;
$doc->loadXML($xml);
$xml = $doc->saveXML();
Storage::disk('arcadia')->put(Str::slug(sprintf(
'v4_order_%s_%s_%s%s',
$request->order_num,
$type->label(),
date('Ymd_His'),
\microtime(true)
)) . '.xml', $xml);
return response()->json();
// @codeCoverageIgnoreStart
} catch (\Exception $e) {
Log::critical("ARCADIA XML CREATION FAILED FOR $request->order_num : " . $e->getMessage());
if (!config('app.debug')) {
dispatch(function () use ($xml, $e, $request) {
Mail::to(config('mail.MAIL_ADMIN'))->send(new CourrierArcadiaError(
"ARCADIA XML CREATION FAILED FOR $request->order_num",
"Une erreur est survenue lors de la création du fichier XML : " . $e->getMessage(),
$xml ?: ''
));
})->afterResponse();
}
throw $e;
// @codeCoverageIgnoreEnd
} //end try
}
/**
* Render Arcadia xml for order.
*
* @param \App\Http\Requests\Api\V4\BaseApiRequest $request
* @param \App\Enums\ArcadiaMailType $type
* @param \App\Enums\ArcadiaOrderType $orderType
* @param array $customerInfo
* @return string
*/
private function makeXml(
BaseApiRequest $request,
ArcadiaMailType $type,
ArcadiaOrderType $orderType,
array $customerInfo
): string {
$arcadiaMailType = $type->equals(ArcadiaMailType::default) ?
'mail.arcadiaOrders.default' : 'mail.arcadiaOrders.game';
return view($arcadiaMailType, [
'request' => $request,
'type' => $type,
'orderType' => $orderType,
'customerInfo' => $customerInfo
])->render();
}
}

View file

@ -0,0 +1,51 @@
<?php
namespace App\Http\Controllers\Api\V4;
use App\Exceptions\Api\V4\CrmException;
use App\Http\Controllers\Controller;
use App\Http\Requests\Api\V4\SendClaimRequest;
use App\Models\Claim;
use App\Models\CustomerInfo;
use App\Models\Service;
use Illuminate\Support\Arr;
use Uwil\LaravelCommon\Helpers\FileHelper;
class ClaimController extends Controller
{
/**
* Just returns HTTP 200 response.
*
* @param \App\Http\Requests\Api\V4\SendClaimRequest $request
* @param \App\Models\Service $service
* @return \Illuminate\Http\JsonResponse
* @throws \App\Exceptions\Api\V4\CrmException If the single optin could not be added or removed.
*/
public function store(SendClaimRequest $request, Service $service): \Illuminate\Http\JsonResponse
{
$claimModel = (new Claim())->fill(Arr::except($request->validated(), 'file'));
if ($request->filled('file')) {
$claimModel->file = FileHelper::storeBase64File(
tableName: 'claim',
fileBase64: $request->file,
private: true,
);
}
// Add brand id from service.
$claimModel->brand_id = $service->brand->getKey();
// Add customer relation if exist.
$crmCustomer = CustomerInfo::getFromEmail($claimModel->brand, $claimModel->email)->first();
if (!is_null($crmCustomer)) {
$claimModel->hashcode = $crmCustomer->hashcode;
}
if (!$claimModel->saveOrFail()) {
throw new CrmException('Api failed to store claim.');
}
$claimModel->brand()->associate($service->brand);
return response()->json();
}
}

View file

@ -0,0 +1,260 @@
<?php
namespace App\Http\Controllers\Api\V4;
use App\Enums\ApiError;
use App\Exceptions\Api\V4\ApiErrorException;
use App\Exceptions\Api\V4\CrmException;
use App\Http\Controllers\Controller;
use App\Http\Requests\Api\V4\AuthenticateRequest;
use App\Http\Requests\Api\V4\StoreCustomerRequest;
use App\Http\Requests\Api\V4\UpdateCustomerRequest;
use App\Http\Resources\Api\V4\CustomerResource;
use App\Jobs\UpdateCustomerLastConnection;
use App\Models\Customer;
use App\Models\Service;
use App\Models\SingleOptin;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Support\Facades\DB;
class CustomerController extends Controller
{
/**
* Creates a new customer for the specified service.
*
* @param \App\Http\Requests\Api\V4\StoreCustomerRequest $request
* @param \App\Models\Service $service
* @return \Illuminate\Http\Resources\Json\JsonResource
* @throws \App\Exceptions\Api\V4\ApiErrorException With ApiError::customerExists if
* the customer with this email already exists.
* @throws \App\Exceptions\Api\V4\CrmException If saving the customer to DB failed.
*/
public function store(StoreCustomerRequest $request, Service $service): JsonResource
{
$data = (object) $request->validated();
/** @var \App\Models\Customer */
$customer = Customer::query()->where('email', $data->email)->firstOrCreate([
'email' => $data->email
]);
DB::transaction(function () use (&$customer, $data, $request, $service) {
$saved = $customer->save();
$customer->setOptIn($service->brand, $data->opt_in);
// * Delete Single optin if exists.
/** @var \App\Models\SingleOptin|null $singleOptin */
$singleOptin = SingleOptin::query()->where('email', $data->email)
->whereHas('brand', function (Builder $q) use ($service) {
$q->where('id', $service->brand->id);
})->first();
if ($singleOptin) {
$saved = $singleOptin->delete() && $saved;
}
/** @var \App\Models\CustomerInfo */
$customerInfo = $customer->info($service->brand, Customer::FIRST_OR_NEW);
if ($customerInfo->exists) {
throw new ApiErrorException(ApiError::customerExists);
}
$customerInfo->fill($request->validated());
$customerInfo->brand()->associate($service->brand);
$customerInfo->customer()->associate($customer);
$saved = $customerInfo->save() && $saved;
/** @var \App\Models\CustomerLogin */
$customerLogin = $customer->login($service->brand, Customer::FIRST_OR_NEW);
$customerLogin->fill($request->validated());
$customerLogin->brand()->associate($service->brand);
$customerLogin->customer()->associate($customer);
$saved = $customerLogin->save() && $saved;
if (!$saved) {
// @codeCoverageIgnoreStart
throw new CrmException('Api failed to store customer.');
// @codeCoverageIgnoreEnd
}
});
return new CustomerResource($service->brand, $customer);
}
/**
* Renders the specific customer infos.
*
* @param \App\Models\Service $service
* @param \App\Models\Customer $customer
* @return \Illuminate\Http\Resources\Json\JsonResource
*/
public function show(Service $service, Customer $customer): JsonResource
{
// * Make sure we are accessing a customer that belongs to our service.
$this->authorize('customerBelongsToService', $customer);
return new CustomerResource($service->brand, $customer);
}
/**
* Updates information for a specific customer.
*
* @param \App\Http\Requests\Api\V4\UpdateCustomerRequest $request
* @param \App\Models\Service $service
* @param \App\Models\Customer $customer
* @return \Illuminate\Http\Resources\Json\JsonResource
* @throws \App\Exceptions\Api\V4\CrmException If saving the customer to DB failed.
*/
public function update(UpdateCustomerRequest $request, Service $service, Customer $customer): JsonResource
{
// * Make sure we are accessing a customer that belongs to our service.
$this->authorize('customerBelongsToService', $customer);
DB::transaction(function () use ($request, $customer, $service) {
// To correct with create.
$customer->fill($request->validated());
$saved = $customer->save();
if ($request->has('opt_in')) {
$customer->setOptIn($service->brand, $request->opt_in);
}
/** @var \App\Models\CustomerInfo */
$customerInfo = $customer->info($service->brand, Customer::FIRST_OR_FAIL);
$customerInfo->fill($request->validated());
$saved = $customerInfo->save() && $saved;
/** @var \App\Models\CustomerLogin */
$customerLogin = $customer->login($service->brand, Customer::FIRST_OR_FAIL);
$customerLogin->fill($request->validated());
$saved = $customerLogin->save() && $saved;
if (!$saved) {
// @codeCoverageIgnoreStart
throw new CrmException('Api failed to update customer.');
// @codeCoverageIgnoreEnd
}
});
return new CustomerResource($service->brand, $customer);
}
/**
* Deletes customer information for a specific service.
*
* @param \App\Models\Service $service
* @param \App\Models\Customer $customer
* @return \Illuminate\Http\JsonResponse
* @throws \App\Exceptions\Api\V4\CrmException If removing the customer off the DB failed.
*/
public function destroy(Service $service, Customer $customer): JsonResponse
{
// * Make sure we are accessing a customer that belongs to our service.
$this->authorize('customerBelongsToService', $customer);
/** @var \App\Models\CustomerInfo */
$customerInfo = $customer->info($service->brand, Customer::FIRST_OR_FAIL);
/** @var \App\Models\CustomerLogin */
$customerLogin = $customer->login($service->brand, Customer::FIRST_OR_FAIL);
DB::transaction(function () use ($service, $customer, $customerLogin, $customerInfo) {
if (
$customerInfo->delete() === false ||
$customerLogin->delete() === false
) {
// @codeCoverageIgnoreStart
throw new CrmException('Api failed to delete customer.');
// @codeCoverageIgnoreEnd
};
$customer->optInHistory()->whereHas('brand', function ($q) use ($service) {
$q->where('id', $service->brand_id);
})->delete();
// * Force loading real relation counts.
$customer->unsetRelation('logins');
$customer->unsetRelation('infos');
$logins = $customer->logins()->count();
$infos = $customer->infos()->count();
// If customer is not associated with any brand then delete.
if (!$logins and !$infos) {
if ($customer->delete() === false) {
// @codeCoverageIgnoreStart
throw new CrmException('Api failed to update customer.');
// @codeCoverageIgnoreEnd
}
}
});
return response()->json();
}
/**
* Authenticate the user.
*
* @param \App\Http\Requests\Api\V4\AuthenticateRequest $request
* @param \App\Models\Service $service
* @return \Illuminate\Http\Resources\Json\JsonResource
* @throws \App\Exceptions\Api\V4\ApiErrorException With ApiError::authFailed in case
* of wrong credentials.
* @throws \App\Exceptions\Api\V4\ApiErrorException With ApiError::customerNotActivated if
* customer account is not activated.
*/
public function authenticate(AuthenticateRequest $request, Service $service): JsonResource
{
try {
/** @var \App\Models\Customer */
$customer = Customer::query()->where('email', $request->email)->firstOrFail();
/** @var \App\Models\CustomerLogin $customerLogin */
$customerLogin = $customer->login($service->brand, Customer::FIRST_OR_FAIL);
if (!$customerLogin->passwordMatch($request->password)) {
throw new ApiErrorException(ApiError::authFailed);
}
if (!$customerLogin->status) {
throw new ApiErrorException(ApiError::customerNotActivated);
}
UpdateCustomerLastConnection::dispatch($customer, $customerLogin, now())
->onConnection('database')->afterResponse();
return new CustomerResource($service->brand, $customer);
} catch (ModelNotFoundException $e) {
throw new ApiErrorException(ApiError::authFailed);
} //end try
}
/**
* Activate the account of the user.
*
* @param \App\Models\Customer $customer
* @param \App\Models\Service $service
* @return \Illuminate\Http\JsonResponse
* @throws \App\Exceptions\Api\V4\ApiErrorException With ApiError::customerAlreadyActive
* if customer account is already active.
* @throws \App\Exceptions\Api\V4\CrmException If saving the customer to DB failed.
*/
public function activateAccount(Customer $customer, Service $service): JsonResponse
{
// * Make sure we are accessing a customer that belongs to our service.
$this->authorize('customerBelongsToService', $customer);
/** @var \App\Models\CustomerLogin $login */
$login = $customer->login($service->brand, Customer::FIRST_OR_FAIL);
if ($login->status) {
throw new ApiErrorException(ApiError::customerAlreadyActive);
}
$login->status = true;
if (!$login->save()) {
// @codeCoverageIgnoreStart
throw new CrmException('Failed to save login');
// @codeCoverageIgnoreEnd
}
return response()->json();
}
}

View file

@ -0,0 +1,130 @@
<?php
namespace App\Http\Controllers\Api\V4;
use App\Exceptions\Api\V4\CrmException;
use App\Http\Controllers\Controller;
use App\Http\Requests\Api\V4\BaseApiRequest;
use App\Http\Requests\Api\V4\SingleOptinRequest;
use App\Http\Requests\Api\V4\SingleOptinRemoveWithHashcodeRequest;
use App\Models\CustomerInfo;
use App\Models\Service;
use App\Models\SingleOptin;
class OptinController extends Controller
{
/**
* Creates a new Single optin or destroys it if exists.
*
* @param \App\Http\Requests\Api\V4\SingleOptinRequest $request
* @param \App\Models\Service $service
* @return \Illuminate\Http\JsonResponse
* @throws \App\Exceptions\Api\V4\CrmException If the single optin could not be added or removed.
*/
public function optin(SingleOptinRequest $request, Service $service): \Illuminate\Http\JsonResponse
{
$validatedData = $request->validated();
$crmCustomer = CustomerInfo::getFromEmail($service->brand, $validatedData['email'])->first();
if ($crmCustomer) {
// Update the optin field.
$crmCustomer->opt_in = \boolval($validatedData['opt_in']);
// Check the saving.
if (!$crmCustomer->saveOrFail()) {
// @codeCoverageIgnoreStart
throw new CrmException('Api failed to update customer optin.');
// @codeCoverageIgnoreEnd
}
// Associate answers if presents.
if (isset($validatedData['answer_id'])) {
$crmCustomer->answers()->attach($validatedData['answer_id']);
}
} else {
// Get the single optin if exist or create it.
/** @var \App\Models\SingleOptin */
$singleOptin = SingleOptin::query()
->where('email', $validatedData['email'])
->whereHas('brand', function ($q) use ($service) {
$q->whereKey($service->brand->getKey());
})
->firstOrNew();
// Update the optin field and check the saving.
if (!$this->changeOptin($service, $request, \boolval($validatedData['opt_in']), $singleOptin)) {
// @codeCoverageIgnoreStart
throw new CrmException('Api failed to update single optin.');
// @codeCoverageIgnoreEnd
}
// Associate answers if presents.
if (isset($validatedData['answer_id'])) {
$singleOptin->answers()->attach($validatedData['answer_id']);
}
} //end if
return response()->json();
}
/**
* Creates a new Single optin or destroys it if exists using hashcode.
*
* @param \App\Http\Requests\Api\V4\SingleOptinRemoveWithHashcodeRequest $request
* @param \App\Models\Service $service
* @return \Illuminate\Http\JsonResponse
* @throws \App\Exceptions\Api\V4\CrmException If the single optin could not be removed.
*/
public function optoutWithHashcode(
SingleOptinRemoveWithHashcodeRequest $request,
Service $service
): \Illuminate\Http\JsonResponse {
$validatedData = $request->validated();
$crmCustomer = CustomerInfo::getFromHashcode($service->brand, $validatedData['hashcode'])->first();
if ($crmCustomer) {
// Update the optin field.
$crmCustomer->opt_in = false;
// Check the saving.
if (!$crmCustomer->saveOrFail()) {
// @codeCoverageIgnoreStart
throw new CrmException('Api failed to update customer optin.');
// @codeCoverageIgnoreEnd
}
} else {
// Get the single optin if exist or create it.
/** @var \App\Models\SingleOptin */
$singleOptin = SingleOptin::query()->where('hashcode', $validatedData['hashcode'])
->whereHas('brand', function ($q) use ($service) {
$q->whereId($service->brand->getKey());
})->firstOrFail();
// Update the optin field and check the saving.
if ($singleOptin and !$this->changeOptin($service, $request, false, $singleOptin)) {
// @codeCoverageIgnoreStart
throw new CrmException('Api failed to update single optin.');
// @codeCoverageIgnoreEnd
}
} //end if
return response()->json();
}
/**
* Changes the optin state.
*
* @param \App\Models\Service $service
* @param \App\Http\Requests\Api\V4\BaseApiRequest $request
* @param boolean $optin
* @param \App\Models\SingleOptin $singleOptin
* @return boolean Returns false in case of failure.
*/
private function changeOptin(Service $service, BaseApiRequest $request, bool $optin, SingleOptin $singleOptin): bool
{
if ($optin) {
$singleOptin->fill($request->validated());
$singleOptin->brand()->associate($service->brand);
return $singleOptin->save();
}
return $singleOptin->delete() !== false;
}
}

View file

@ -0,0 +1,68 @@
<?php
namespace App\Http\Controllers\Api\V4;
use App\Http\Controllers\Controller;
use App\Http\Resources\Api\V4\QuestionsResource;
use App\Models\Answer;
use App\Models\Customer;
use App\Models\Question;
use App\Models\Service;
class QuestionController extends Controller
{
/**
* Returns all service questions.
*
* @param \App\Models\Service $service
* @return \Illuminate\Http\Resources\Json\ResourceCollection
*/
public function index(Service $service): \Illuminate\Http\Resources\Json\ResourceCollection
{
return new QuestionsResource($service->brand->questions);
}
/**
* Returns all service questions with user answers for all of them.
*
* @param \App\Models\Service $service
* @param \App\Models\Customer $customer
* @return \Illuminate\Http\Resources\Json\ResourceCollection
*/
public function indexWithResponse(
Service $service,
Customer $customer
): \Illuminate\Http\Resources\Json\ResourceCollection {
// * Make sure we are accessing a customer that belongs to our service.
$this->authorize('customerBelongsToService', $customer);
$customerAnswers = $customer->getAnswers($service);
return new QuestionsResource($service->brand->questions, $customerAnswers);
}
/**
* Returns all service questions that was not answered by a user.
*
* @param \App\Models\Service $service
* @param \App\Models\Customer $customer
* @return \Illuminate\Http\Resources\Json\JsonResource
*/
public function unAnsweredBy(Service $service, Customer $customer): \Illuminate\Http\Resources\Json\JsonResource
{
// * Make sure we are accessing a customer that belongs to our service.
$this->authorize('customerBelongsToService', $customer);
$questions = Question::query()
->whereHas('brand', function ($q) use ($service) {
$q->where('id', $service->brand->id);
})
->whereDoesntHave('answers', function ($q) use ($customer) {
$q->whereIn('id', Answer::query()->whereHas('customers', function ($q) use ($customer) {
$q->where('id', $customer->id);
})->pluck('id'));
})
->with('answers')
->get();
return new QuestionsResource($questions);
}
}

View file

@ -0,0 +1,116 @@
<?php
namespace App\Http\Controllers\Api\V4;
use App\Http\Controllers\Controller;
use App\Http\Requests\Api\V4\GetBrandStatsRequest;
use App\Http\Requests\Api\V4\GetQuestionStatsRequest;
use App\Http\Resources\Api\V4\BrandStatsResource;
use App\Http\Resources\Api\V4\QuestionStatsResource;
use App\Models\BrandStat;
use App\Models\ModelAnswer;
use App\Models\Question;
use App\Models\Service;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
class StatsController extends Controller
{
/**
* Get specific brand statistics.
*
* @param \App\Http\Requests\Api\V4\GetBrandStatsRequest $request
* @param \App\Models\Service $service
* @return \Illuminate\Http\Resources\Json\ResourceCollection
*/
public function brandStats(
GetBrandStatsRequest $request,
Service $service
): \Illuminate\Http\Resources\Json\ResourceCollection {
$stats = BrandStat::query()->whereDate('created_at', '>=', $request->start)
->whereDate('created_at', '<=', $request->end)
->whereBrandId($service->brand->id)->get();
return new BrandStatsResource($stats);
}
/**
* Get statistics for a specific question.
*
* @param \App\Http\Requests\Api\V4\GetQuestionStatsRequest $request
* @param \App\Models\Question $question
* @return \App\Http\Resources\Api\V4\QuestionStatsResource
*/
public function questionStats(
GetQuestionStatsRequest $request,
Question $question
): \App\Http\Resources\Api\V4\QuestionStatsResource {
// * Make sure we are accessing a customer that belongs to our service.
$this->authorize('questionBelongsToService', $question);
$answers = $question->answers;
$start = Carbon::parse($request->start);
$end = Carbon::parse($request->end);
return new QuestionStatsResource(
$this->answersByDays($answers, $start, $end),
$question
);
}
/**
* Total réponses par dates.
*
* @param \Illuminate\Support\Collection $answers
* @param \Illuminate\Support\Carbon $start
* @param \Illuminate\Support\Carbon $end
* @return \Illuminate\Support\Collection
*/
private function answersByDays(Collection $answers, Carbon $start, Carbon $end): \Illuminate\Support\Collection
{
// * Totalize by date all answers.
$answersByDays = ModelAnswer::query()
->whereDate('created_at', '>=', $start)
->whereDate('created_at', '<=', $end)
->whereIn('answer_id', $answers->pluck('id'))
->get()
->map(function ($custAnswer) {
return [
'answer_id' => $custAnswer->answer_id,
'created_at' => $custAnswer->created_at->isoFormat('YYYY-MM-DD'),
];
})
->groupBy(['created_at', 'answer_id'])
->map(function ($day) {
return $day->map(function ($custAnswers) {
return $custAnswers->count();
});
});
do {
$currentDate = $start->isoFormat('YYYY-MM-DD');
/** @var \Illuminate\Support\Collection $answersByDays */
if (!$answersByDays->has($currentDate)) {
$answersByDays->put(
$currentDate,
$answers->mapWithKeys(function ($answer) {
return [$answer->id => 0];
})
);
} else {
$curentAnswers = $answersByDays->pull($currentDate);
$answersByDays->put(
$currentDate,
$answers->mapWithKeys(function ($answer) use ($curentAnswers) {
return [$answer->id =>
$curentAnswers->has($answer->id) ?
$curentAnswers->get($answer->id) : 0];
})
);
}
$start = $start->addDay();
} while ($start->lessThanOrEqualTo($end));
return $answersByDays;
}
}

View file

@ -0,0 +1,79 @@
<?php
namespace App\Http\Controllers\Bo;
use App\Http\Controllers\Controller;
use App\Models\ActivityLog;
use App\Traits\Controllers\ChangesModelOrder;
use App\Traits\Controllers\UpdateModelPublished;
use Illuminate\Http\Request;
class ActivityLogsController extends Controller
{
use ChangesModelOrder;
use UpdateModelPublished;
/**
* Create the controller instance.
*/
public function __construct()
{
$this->authorizeResource(ActivityLog::class);
}
/**
* Display a listing of the resource.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Contracts\View\View
*/
public function index(Request $request): \Illuminate\Contracts\View\View
{
$this->authorize('isConceptor');
/** @var \Illuminate\Database\Eloquent\Builder $query */
$query = ActivityLog::query()->with('user');
/** @var string $search Search field */
$search = $request->search;
if ($search) {
$this->searchQuery(
$query,
$search,
null,
'model_class',
);
}
$searchFields = \implode(', ', [
trans('validation.custom.target'),
]);
/** Sort columns with a query */
$this->sortQuery($query);
/** Custom pagination */
$activitylogModels = $query->paginate($this->getPagination());
return view('bo.pages.activity_logs.index', compact('activitylogModels', 'search', 'searchFields'));
}
/**
* Show the form for editing the specified resource.
*
* @param App\Models\ActivityLog $activity_log
* @return \Illuminate\Contracts\View\View
*/
public function show(ActivityLog $activity_log): \Illuminate\Contracts\View\View
{
$this->authorize('isConceptor');
$targetModelClass = $activity_log->model_class;
$targetModel = $activity_log->model_class::where(
(new $targetModelClass())->getRouteKeyName(),
$activity_log->model_id
)->first();
return view('bo.pages.activity_logs.show', [
'activitylogModel' => $activity_log,
'targetModel' => $targetModel,
]);
}
}

View file

@ -1,50 +1,55 @@
<?php <?php
namespace App\Http\Controllers; namespace App\Http\Controllers\Bo;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Http\Requests\Answers\StoreAnswerRequest; use App\Http\Requests\Bo\Answers\StoreAnswerRequest;
use App\Http\Requests\Answers\UpdateAnswerRequest; use App\Http\Requests\Bo\Answers\UpdateAnswerRequest;
use App\Models\Answer; use App\Models\Answer;
use App\Models\Question; use App\Models\Question;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str; use Illuminate\Support\Str;
/**
* @codeCoverageIgnore
*/
class AnswerController extends Controller class AnswerController extends Controller
{ {
/**
* Create the controller instance.
*/
public function __construct()
{
$this->authorizeResource(Answer::class);
}
/** /**
* Display a listing of the resource. * Display a listing of the resource.
* *
* @param \Illuminate\Http\Request $request * @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response * @return \Illuminate\Contracts\View\View
*/ */
public function index(Request $request) public function index(Request $request): \Illuminate\Contracts\View\View
{ {
/** @var \App\Models\Question $questionModel */ /** @var \App\Models\Question $questionModel */
$questionModel = $request->question; $questionModel = $request->question;
/** @var \Illuminate\Database\Eloquent\Builder $answers */ /** @var \Illuminate\Database\Eloquent\Builder $query */
$answers = Answer::query()->where('question_id', $questionModel->id); $query = Answer::query()->where('question_id', $questionModel->id);
if ($request->search) { if ($request->search) {
$this->searchQuery( $this->searchQuery(
$answers, $query,
$request->search, $request->search,
null, null,
'answer', 'answer',
); );
} }
$this->sortQuery($answers); $this->sortQuery($query);
$searchFields = trans('Réponse'); $searchFields = trans('Réponse');
$answerModels = $answers->paginate(12); $answerModels = $query->paginate($this->getPagination());
return view('answers.index', compact('answerModels', 'questionModel', 'searchFields')); return view('bo.pages.answers.index', compact('answerModels', 'questionModel', 'searchFields'));
} }
/** /**
@ -52,33 +57,35 @@ class AnswerController extends Controller
* *
* @param \Illuminate\Http\Request $request * @param \Illuminate\Http\Request $request
* @param \App\Models\Answer $answer * @param \App\Models\Answer $answer
* @return \Illuminate\Http\Response * @return \Illuminate\Contracts\View\View
*/ */
public function create(Request $request, Answer $answer) public function create(Request $request, Answer $answer): \Illuminate\Contracts\View\View
{ {
/** @var \App\Models\Question $questionModel */ /** @var \App\Models\Question $questionModel */
$questionModel = $request->question; $questionModel = $request->question;
return view('answers.create', [ return view('bo.pages.answers.create', [
'answerModel' => $answer, 'answerModel' => $answer,
'questionModel' => $questionModel 'questionModel' => $questionModel,
]); ]);
} }
/** /**
* Store a newly created resource in storage. * Store a newly created resource in storage.
* *
* @param \App\Http\Requests\Answers\StoreAnswerRequest $request * @param \App\Http\Requests\Bo\Answers\StoreAnswerRequest $request
* @return \Illuminate\Http\Response * @return \Illuminate\Http\RedirectResponse
*/ */
public function store(StoreAnswerRequest $request) public function store(StoreAnswerRequest $request): \Illuminate\Http\RedirectResponse
{ {
return DB::transaction(function () use ($request) { return DB::transaction(function () use ($request) {
$answer = new Answer(); $answer = new Answer();
$answer->fill($request->validated()); $answer->fill($request->validated());
$questionModel = $request->route()->parameter('question');
$answer->question()->associate($questionModel->getKey());
if ($answer->saveOrFail()) { if ($answer->saveOrFail()) {
return redirect() return redirect()
->route('answers.edit', $answer) ->route('bo.questions.answers.edit', ['question' => $questionModel, 'answer' => $answer])
->with('success', trans( ->with('success', trans(
':model a été créé.', ':model a été créé.',
['model' => Str::singular(trans('models.classes.answers'))] ['model' => Str::singular(trans('models.classes.answers'))]
@ -98,31 +105,34 @@ class AnswerController extends Controller
* *
* @param \App\Models\Question $question * @param \App\Models\Question $question
* @param \App\Models\Answer $answers * @param \App\Models\Answer $answers
* @return \Illuminate\Http\Response * @return \Illuminate\Contracts\View\View
*/ */
public function edit(Question $question, Answer $answers) public function edit(Question $question, Answer $answers): \Illuminate\Contracts\View\View
{ {
return view('answers.edit', [ return view('bo.pages.answers.edit', [
'answerModel' => $answers, 'answerModel' => $answers,
'questionModel' => $question 'questionModel' => $question,
]); ]);
} }
/** /**
* Update a spacific resource in storage. * Update a spacific resource in storage.
* *
* @param \App\Http\Requests\Answers\UpdateAnswerRequest $request * @param \App\Http\Requests\Bo\Answers\UpdateAnswerRequest $request
* @param \App\Models\Question $question * @param \App\Models\Question $question
* @param \App\Models\Answer $answer * @param \App\Models\Answer $answer
* @return \Illuminate\Http\Response * @return \Illuminate\Http\RedirectResponse
*/ */
public function update(UpdateAnswerRequest $request, Question $question, Answer $answer) public function update(
{ UpdateAnswerRequest $request,
Question $question,
Answer $answer
): \Illuminate\Http\RedirectResponse {
return DB::transaction(function () use ($request, $question, $answer) { return DB::transaction(function () use ($request, $question, $answer) {
$answer->fill($request->validated()); $answer->fill($request->validated());
if ($answer->saveOrFail()) { if ($answer->saveOrFail()) {
return redirect() return redirect()
->route('questions.answers.edit', ['question' => $question, 'answer' => $answer]) ->route('bo.questions.answers.edit', ['question' => $question, 'answer' => $answer])
->with('success', trans( ->with('success', trans(
':model a été mis à jour.', ':model a été mis à jour.',
['model' => Str::singular(trans('models.classes.answers'))] ['model' => Str::singular(trans('models.classes.answers'))]
@ -146,12 +156,13 @@ class AnswerController extends Controller
*/ */
public function destroy(Question $question, Answer $answer): \Illuminate\Http\RedirectResponse public function destroy(Question $question, Answer $answer): \Illuminate\Http\RedirectResponse
{ {
$answer->singleOptins()->detach();
if ($answer->deleteOrFail()) { if ($answer->deleteOrFail()) {
return redirect()->route('questions.answers.index', $question) return redirect()->route('bo.questions.answers.index', $question)
->with('success', trans( ->with('success', trans(
':model a été supprimé.', ':model a été supprimé.',
['model' => Str::singular(trans('models.classes.answers'))] ['model' => Str::singular(trans('models.classes.answers'))]
)); ));
} }
return redirect()->back() return redirect()->back()
->with('error', trans( ->with('error', trans(

View file

@ -0,0 +1,72 @@
<?php
namespace App\Http\Controllers\Bo\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Contracts\Auth\StatefulGuard;
use Illuminate\Contracts\View\View;
use Illuminate\Foundation\Auth\AuthenticatesUsers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class LoginController extends Controller
{
use AuthenticatesUsers;
/**
* Create a new controller instance.
*
* @return void
*/
public function __construct()
{
$this->middleware('guest:backend')->except('logout');
}
/**
* Get the guard to be used during authentication.
*
* @return \Illuminate\Contracts\Auth\StatefulGuard
*/
protected function guard(): StatefulGuard
{
return Auth::guard('backend');
}
/**
* Show the application's login form.
*
* @return \Illuminate\Contracts\View\View
*/
public function showLoginForm(): View
{
return view('bo.auth.login');
}
/**
* Where to redirect users after login.
*
* @return string
*/
protected function redirectTo(): string
{
return response()->redirectToIntended()->getTargetUrl();
}
/**
* Attempt to log the user into the application.
*
* @param \Illuminate\Http\Request $request
* @return boolean
*/
protected function attemptLogin(Request $request)
{
$credentials = $this->credentials($request);
// * Attempt login only using published users.
$credentials['published'] = true;
return $this->guard()->attempt(
$credentials,
$request->boolean('remember')
);
}
}

View file

@ -0,0 +1,33 @@
<?php
namespace App\Http\Controllers\Bo\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Contracts\Auth\StatefulGuard;
use Illuminate\Foundation\Auth\ResetsPasswords;
use Illuminate\Support\Facades\Auth;
class ResetPasswordController extends Controller
{
use ResetsPasswords;
/**
* Get the guard to be used during authentication.
*
* @return \Illuminate\Contracts\Auth\StatefulGuard
*/
protected function guard(): StatefulGuard
{
return Auth::guard('backend');
}
/**
* Where to redirect users after login.
*
* @return string
*/
protected function redirectTo(): string
{
return \route('bo.login');
}
}

View file

@ -1,28 +1,35 @@
<?php <?php
namespace App\Http\Controllers; namespace App\Http\Controllers\Bo;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Http\Requests\Brands\StoreBrandRequest; use App\Http\Requests\Bo\Brands\StoreBrandRequest;
use App\Http\Requests\Brands\UpdateBrandRequest; use App\Http\Requests\Bo\Brands\UpdateBrandRequest;
use App\Http\Requests\Bo\PaginateModelsRequest;
use App\Models\Brand; use App\Models\Brand;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Uwil\LaravelCommon\Helpers\CollectionHelper;
/**
* @codeCoverageIgnore
*/
class BrandController extends Controller class BrandController extends Controller
{ {
/**
* Create the controller instance.
*/
public function __construct()
{
$this->authorizeResource(Brand::class);
}
/** /**
* Display a listing of the resource. * Display a listing of the resource.
* *
* @param \Illuminate\Http\Request $request * @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response * @return \Illuminate\Contracts\View\View
*/ */
public function index(Request $request) public function index(Request $request): \Illuminate\Contracts\View\View
{ {
/** @var \Illuminate\Database\Eloquent\Builder $query */ /** @var \Illuminate\Database\Eloquent\Builder $query */
$query = Brand::query(); $query = Brand::query();
@ -43,20 +50,20 @@ class BrandController extends Controller
$q->where('opt_in', true); $q->where('opt_in', true);
}]); }]);
$brandModels = $query->paginate(12); $brandModels = $query->paginate($this->getPagination());
return view('brands.index', compact('brandModels', 'searchFields')); return view('bo.pages.brands.index', compact('brandModels', 'searchFields'));
} }
/** /**
* Show the form for creating a new resource. * Show the form for creating a new resource.
* *
* @param \App\Models\Brand $brands * @param \App\Models\Brand $brands
* @return \Illuminate\Http\Response * @return \Illuminate\Contracts\View\View
*/ */
public function create(Brand $brands) public function create(Brand $brands): \Illuminate\Contracts\View\View
{ {
return view('brands.create', [ return view('bo.pages.brands.create', [
'brandModel' => $brands 'brandModel' => $brands
]); ]);
} }
@ -64,17 +71,17 @@ class BrandController extends Controller
/** /**
* Store a newly created resource in storage. * Store a newly created resource in storage.
* *
* @param \App\Http\Requests\Brands\StoreBrandRequest $request * @param \App\Http\Requests\Brands\Bo\StoreBrandRequest $request
* @return \Illuminate\Http\Response * @return \Illuminate\Http\RedirectResponse
*/ */
public function store(StoreBrandRequest $request) public function store(StoreBrandRequest $request): \Illuminate\Http\RedirectResponse
{ {
return DB::transaction(function () use ($request) { return DB::transaction(function () use ($request) {
$brand = new Brand(); $brand = new Brand();
$brand->fill($request->validated()); $brand->fill($request->validated());
if ($brand->saveOrFail()) { if ($brand->saveOrFail()) {
return redirect() return redirect()
->route('brands.edit', $brand) ->route('bo.brands.edit', $brand)
->with('success', trans( ->with('success', trans(
':model a été créé.', ':model a été créé.',
['model' => Str::singular(trans('models.classes.brands'))] ['model' => Str::singular(trans('models.classes.brands'))]
@ -93,11 +100,11 @@ class BrandController extends Controller
* Show the form for editing the specified resource. * Show the form for editing the specified resource.
* *
* @param \App\Models\Brand $brands * @param \App\Models\Brand $brands
* @return \Illuminate\Http\Response * @return \Illuminate\Contracts\View\View
*/ */
public function edit(Brand $brands) public function edit(Brand $brands): \Illuminate\Contracts\View\View
{ {
return view('brands.edit', [ return view('bo.pages.brands.edit', [
'brandModel' => $brands 'brandModel' => $brands
]); ]);
} }
@ -105,17 +112,17 @@ class BrandController extends Controller
/** /**
* Update a spacific resource in storage. * Update a spacific resource in storage.
* *
* @param \App\Http\Requests\Brands\UpdateBrandRequest $request * @param \App\Http\Requests\Brands\Bo\UpdateBrandRequest $request
* @param \App\Models\Brand $brand * @param \App\Models\Brand $brand
* @return \Illuminate\Http\Response * @return \Illuminate\Http\RedirectResponse
*/ */
public function update(UpdateBrandRequest $request, Brand $brand) public function update(UpdateBrandRequest $request, Brand $brand): \Illuminate\Http\RedirectResponse
{ {
return DB::transaction(function () use ($request, $brand) { return DB::transaction(function () use ($request, $brand) {
$brand->fill($request->validated()); $brand->fill($request->validated());
if ($brand->saveOrFail()) { if ($brand->saveOrFail()) {
return redirect() return redirect()
->route('brands.edit', $brand) ->route('bo.brands.edit', $brand)
->with('success', trans( ->with('success', trans(
':model a été mis à jour.', ':model a été mis à jour.',
['model' => Str::singular(trans('models.classes.brands'))] ['model' => Str::singular(trans('models.classes.brands'))]
@ -131,16 +138,18 @@ class BrandController extends Controller
} }
/** /**
* Store a newly created resource in storage. * Get a paginate resource.
* *
* @return \Illuminate\Http\Response * @param \App\Http\Requests\Bo\PaginateModelsRequest $request
* @return \Illuminate\Http\JsonResponse
*/ */
public function jsonPaginate() public function jsonPaginateModels(PaginateModelsRequest $request): \Illuminate\Http\JsonResponse
{ {
return DB::transaction(function () { $models = CollectionHelper::paginateManually(
/** @var \App\Models\Tag */ Brand::getUserAssociatedBrands(),
$brandModels = Brand::paginate(12); $this->jsonItemPerPagination,
return \response()->json($brandModels ?? 500); (int) $request->validated('page'),
}); );
return \response()->json($models);
} }
} }

View file

@ -0,0 +1,298 @@
<?php
namespace App\Http\Controllers\Bo;
use App\Enums\Claims\ClaimResponseType;
use App\Enums\Claims\ClaimStatus;
use App\Enums\ExportFormat;
use App\Enums\Users\RoleEnum;
use App\Events\ClaimModelChanged;
use App\Http\Controllers\Controller;
use App\Http\Requests\Bo\Claims\ExportClaimRequest;
use App\Http\Requests\Bo\Claims\SendClaimAnswerRequest;
use App\Http\Requests\Bo\Claims\UpdateClaimRequest;
use App\Http\Requests\Bo\Claims\ViewClaimAttachmentRequest;
use App\Http\Requests\Bo\PaginateModelsRequest;
use App\Lib\Helpers\FileHelper;
use App\Mail\ClaimAnswerMail;
use App\Models\Brand;
use App\Models\Claim;
use App\Models\ClaimMailTemplate;
use App\Models\ClaimMessage;
use App\Models\ClaimSubject;
use App\Traits\Controllers\ExportDatas;
use App\Traits\Controllers\UpdateModelPublished;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Storage;
use Symfony\Component\HttpFoundation\StreamedResponse;
use Uwil\LaravelCommon\Helpers\CollectionHelper;
class ClaimController extends Controller
{
use UpdateModelPublished;
use ExportDatas;
/**
* Create the controller instance.
*/
public function __construct()
{
$this->authorizeResource(Claim::class);
}
/**
* Display a listing of the resource.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Contracts\View\View
*/
public function index(Request $request): \Illuminate\Contracts\View\View
{
/** @var \App\Models\User $userModel */
$userModel = auth('backend')->user();
/** @var \Illuminate\Database\Eloquent\Builder $query */
$query = Claim::query()
->when(
$request->filled('brand'),
fn($q) => $q->whereHas('brand', fn($q) => $q->where('id', $request->brand))
)->when(
$userModel->role->equals(RoleEnum::marketing),
fn($q) => $q->whereHas('brand', fn($q) => $q->whereIn('id', $userModel->brands->modelKeys()))
)
->when(
$request->filled('subject'),
fn($q) => $q->whereHas('claimSubject', fn($q) => $q->where('id', $request->subject))
)
->when(
$request->filled('status'),
fn($q) => $q->whereIn('status', $request->status),
fn($q) => $q->whereNotIn('status', [ClaimStatus::treated, ClaimStatus::refused])
);
/** @var string $search Search field */
$search = $request->search;
if ($search) {
$this->searchQuery(
$query,
$search,
null,
'reference',
'first_name',
'last_name',
'email',
);
}
$searchFields = \implode(', ', [
trans('validation.custom.reference'),
trans('validation.attributes.first_name'),
trans('validation.attributes.last_name'),
trans('validation.attributes.email'),
]);
/** Sort columns with a query */
$this->sortQuery($query);
/** Custom pagination */
$claimModels = $query->simplePaginate($this->getPagination());
/** Define request brand filter if user only has 1 brand */
if (auth('backend')->user()->brands()->count() === 1) {
$request->merge(['brand' => auth('backend')->user()->brands()->value('id')]);
};
$claimSubjectModels = ClaimSubject::query()
->when(
$request->filled('brand'),
fn($q) => $q->whereHas('brand', fn($q) => $q->where('id', $request->brand)),
)
->when(
auth('backend')->user()->brands()->count() === 1,
fn($q) => $q->whereHas(
'brand',
fn($q) => $q->where('id', auth('backend')->user()->brands()->pluck('id'))
)
);
$claimSubjectModels = $request->filled('brand') || (auth('backend')->user()->brands()->count() === 1) ?
$claimSubjectModels->orderBy('label', 'ASC')->get() :
null;
return view('bo.pages.claims.index', compact('claimModels', 'search', 'searchFields', 'claimSubjectModels'));
}
/**
* Show the form for editing the specified resource.
*
* @param App\Models\Claim $claim
* @return \Illuminate\Contracts\View\View
*/
public function edit(Claim $claim): \Illuminate\Contracts\View\View
{
$customerClaimModels = !is_null($claim->customerInfos) ? Claim::query()
->where('hashcode', $claim->customerInfos->hashcode)
->whereHas('brand', fn($q) => $q->whereKey($claim->brand->getKey()))
->whereNot('id', $claim->getKey())
->paginate($this->jsonItemPerPagination) : collect();
$brandModels = CollectionHelper::paginate(
Brand::getUserAssociatedBrands(),
$this->jsonItemPerPagination,
);
$claimMessageModels = ClaimMessage::getAllModelsFromClaim($claim->getKey());
$claimSubjectModels = CollectionHelper::paginate(
ClaimSubject::getAllModelsFromBrand($claim->brand->getKey()),
$this->jsonItemPerPagination,
);
$claimTemplateModels = CollectionHelper::paginate(
ClaimMailTemplate::getAllModelsFromBrand($claim->brand->getKey()),
$this->jsonItemPerPagination,
);
return view('bo.pages.claims.edit', [
'claimModel' => $claim->load(['brand', 'claimSubject']),
'customerClaimModels' => $customerClaimModels,
'brandModels' => $brandModels,
'claimMessageModels' => $claimMessageModels,
'claimSubjectModels' => $claimSubjectModels,
'claimTemplateModels' => $claimTemplateModels,
'customerInfoModel' => $claim->customerInfos,
]);
}
/**
* Update the specified resource.
*
* @param \App\Http\Requests\Bo\Claims\UpdateClaimRequest $request
* @param \App\Models\Claim $claim
* @return \Illuminate\Http\JsonResponse
*/
public function update(UpdateClaimRequest $request, Claim $claim): \Illuminate\Http\JsonResponse
{
return DB::transaction(function () use ($request, $claim) {
$claim->fill($request->validated());
if ($claim->saveOrFail()) {
$claimMessageModels = ClaimMessage::getAllModelsFromClaim($claim->getKey());
return response()->json($claimMessageModels);
}
return response()->json([], 500);
});
}
/**
* Send the claim answer.
*
* @param \App\Http\Requests\Bo\Claims\SendClaimAnswerRequest $request
* @param \App\Models\Claim $claim
* @return \Illuminate\Http\JsonResponse
*/
public function answer(SendClaimAnswerRequest $request, Claim $claim): \Illuminate\Http\JsonResponse
{
return DB::transaction(function () use ($request, $claim) {
/** @var \App\Models\User $userModel */
$userModel = auth()->user();
$enum = ClaimResponseType::manual;
$claim->response_type = $enum;
$claim->status = ClaimStatus::treated;
if ($claim->saveOrFail()) {
// Get only the content of the response.
$content = $request->validated()['content'];
// Send the response to the claim author.
Mail::to($claim->email)->send(new ClaimAnswerMail($claim, $content));
// Create custom event with content message.
// @phpcs:disable Generic.Files.LineLength.TooLong
$content = sprintf(
"<b>%s %s</b> %s <i class='badge' style='background-color:{$enum->color()}'>%s</i><hr class='my-2'>%s",
$userModel->first_name,
$userModel->last_name,
trans('a modifié :field en', ['field' => trans('validation.custom.response_type')]),
$claim->response_type->label(),
clean($content),
);
event(new ClaimModelChanged($claim, '', '', null, $content));
// @phpcs:enable
// Return conversation.
$claimMessageModels = ClaimMessage::getAllModelsFromClaim($claim->getKey());
return response()->json($claimMessageModels);
}
return response()->json([], 500);
});
}
/**
* Undocumented function
*
* @param ViewClaimAttachmentRequest $request
* @param Claim $claim
* @return \Symfony\Component\HttpFoundation\BinaryFileResponse
*
* @phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter
*/
public function viewAttachment(
ViewClaimAttachmentRequest $request,
Claim $claim
): \Symfony\Component\HttpFoundation\BinaryFileResponse {
// phpcs:enable
return response()->file(Storage::disk('private')->path(FileHelper::getPrivateFilePath($claim->file)));
}
/**
* Get a paginate resource.
* Needed to overheight the controller method and add a brand condition.
*
* @param \App\Http\Requests\Bo\PaginateModelsRequest $request
* @return \Illuminate\Http\JsonResponse
*/
public function jsonClaimPaginateModels(PaginateModelsRequest $request): \Illuminate\Http\JsonResponse
{
$data = $request->validated();
$brandModelKey = $request->route()->parameter('brand')->getKey();
$models = CollectionHelper::paginateManually(
$data['targetModel']::getAllModelsFromBrand($brandModelKey),
$this->jsonItemPerPagination,
$request->validated('page'),
);
return \response()->json($models);
}
// * EXPORT
/**
* Export claim datas to a given format
*
* @param ExportClaimRequest $request
* @return \Illuminate\Http\Response|Symfony\Component\HttpFoundation\StreamedResponse|\Illuminate\Http\RedirectResponse
*/
public function export(ExportClaimRequest $request)
{
/** @var \Illuminate\Database\Eloquent\Collection<int, \App\Models\Claim> $claims*/
$claims = Claim::query()->whereIn('id', $request->validated('claimList'))->get();
$format = ExportFormat::from($request->validated('format'));
// * CSV
if ($format->equals(ExportFormat::csvWithoutAttachment, ExportFormat::csv)) {
$csv = $this->generateCsv($claims);
if (!$this->needsZip($format, $claims)) {
return new StreamedResponse(function () use ($format, $csv) {
echo $csv;
}, 200, $this->getFileHeaders($format));
}
return $this->returnExportZip($format, $claims, $csv);
// * PDF
} elseif ($format->equals(ExportFormat::pdf)) {
$pdf = $this->generatePdf($claims);
if (!$this->needsZip(ExportFormat::pdf, $claims)) {
return $pdf->download('export.pdf');
}
return $this->returnExportZip($format, $claims, $pdf->output());
}
}
}

View file

@ -0,0 +1,183 @@
<?php
namespace App\Http\Controllers\Bo;
use App\Enums\Users\RoleEnum;
use App\Http\Controllers\Controller;
use App\Http\Requests\Bo\ClaimMailTemplates\ClaimMailTemplateRequest;
use App\Models\Brand;
use App\Models\ClaimMailTemplate;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Uwil\LaravelCommon\Helpers\CollectionHelper;
class ClaimMailTemplateController extends Controller
{
/**
* Create the controller instance.
*/
public function __construct()
{
$this->authorizeResource(ClaimMailTemplate::class);
}
/**
* Display a listing of the resource.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Contracts\View\View
*/
public function index(Request $request): \Illuminate\Contracts\View\View
{
/** @var \App\Models\User $userModel */
$userModel = auth('backend')->user();
/** @var \Illuminate\Database\Eloquent\Builder $query */
$query = ClaimMailTemplate::query()
->when(
$request->filled('brand'),
fn($q) => $q->whereHas('brand', fn($q) => $q->where('id', $request->brand))
)->when(
$userModel->role->equals(RoleEnum::marketing),
fn($q) => $q->whereHas('brand', fn($q) => $q->whereIn('id', $userModel->brands->modelKeys()))
);
if ($request->search) {
$this->searchQuery(
$query,
$request->search,
null,
'label',
);
}
$this->sortQuery($query);
$searchFields = trans('validation.custom.label');
$claimMailTemplateModels = $query->paginate($this->getPagination());
return view('bo.pages.claim_mail_templates.index', compact('claimMailTemplateModels', 'searchFields'));
}
/**
* Show the specified resource.
*
* @param \App\Models\ClaimMailTemplate $claim_mail_template
* @return \Illuminate\Contracts\View\View
*/
public function show(ClaimMailTemplate $claim_mail_template): \Illuminate\Contracts\View\View
{
return view('bo.pages.claim_mail_templates.show', [
'claimMailTemplateModel' => $claim_mail_template,
]);
}
/**
* Show the form for creating a new resource.
*
* @param \App\Models\ClaimMailTemplate $claim_mail_template
* @return \Illuminate\Contracts\View\View
*/
public function create(ClaimMailTemplate $claim_mail_template): \Illuminate\Contracts\View\View
{
$brandModels = CollectionHelper::paginate(Brand::getUserAssociatedBrands(), $this->jsonItemPerPagination);
return view('bo.pages.claim_mail_templates.create', [
'claimMailTemplateModel' => $claim_mail_template,
'brandModels' => $brandModels,
]);
}
/**
* Store a newly created resource in storage.
*
* @param \App\Http\Requests\Bo\ClaimMailTemplates\ClaimMailTemplateRequest $request
* @return \Illuminate\Http\RedirectResponse
*/
public function store(ClaimMailTemplateRequest $request): \Illuminate\Http\RedirectResponse
{
return DB::transaction(function () use ($request) {
$claimMailTemplateModel = new ClaimMailTemplate();
$claimMailTemplateModel->fill($request->validated());
if ($claimMailTemplateModel->saveOrFail()) {
return redirect()
->route('bo.claim_mail_templates.edit', $claimMailTemplateModel)
->with('success', trans(
':model a été créé.',
['model' => Str::singular(trans('models.classes.claimmailtemplates'))]
));
}
return redirect()
->back()
->with('error', trans(
':model n\'a pas été créé.',
['model' => Str::singular(trans('models.classes.claimmailtemplates'))]
));
});
}
/**
* Show the form for editing the specified resource.
*
* @param \App\Models\ClaimMailTemplate $claim_mail_template
* @return \Illuminate\Contracts\View\View
*/
public function edit(ClaimMailTemplate $claim_mail_template): \Illuminate\Contracts\View\View
{
$brandModels = CollectionHelper::paginate(Brand::getUserAssociatedBrands(), $this->jsonItemPerPagination);
return view('bo.pages.claim_mail_templates.edit', [
'claimMailTemplateModel' => $claim_mail_template,
'brandModels' => $brandModels,
]);
}
/**
* Update a spacific resource in storage.
*
* @param \App\Http\Requests\Bo\ClaimSubjects\ClaimMailTemplateRequest $request
* @param \App\Models\ClaimMailTemplate $claim_mail_template
* @return \Illuminate\Http\RedirectResponse
*/
public function update(
ClaimMailTemplateRequest $request,
ClaimMailTemplate $claim_mail_template
): \Illuminate\Http\RedirectResponse {
return DB::transaction(function () use ($request, $claim_mail_template) {
$claim_mail_template->fill($request->validated());
if ($claim_mail_template->saveOrFail()) {
return redirect()
->route('bo.claim_mail_templates.edit', $claim_mail_template)
->with('success', trans(
':model a été mis à jour.',
['model' => Str::singular(trans('models.classes.claimmailtemplates'))]
));
}
return redirect()
->back()
->with('error', trans(
':model n\'a pas été mis à jour.',
['model' => Str::singular(trans('models.classes.claimmailtemplates'))]
));
});
}
/**
* Remove the specified resource from storage.
*
* @param \App\Models\ClaimMailTemplate $claim_mail_template
* @return \Illuminate\Http\RedirectResponse
*/
public function destroy(ClaimMailTemplate $claim_mail_template): \Illuminate\Http\RedirectResponse
{
if ($claim_mail_template->deleteOrFail()) {
return redirect()->route('bo.claim_mail_templates.index')
->with('success', __('crud.messages.has_been_deleted', [
'model' => Str::of(__('models.classes.claimmailtemplates'))->singular()
]));
}
return redirect()->back()
->with('error', __('crud.messages.cannot_be_deleted', [
'model' => Str::of(__('models.classes.claimmailtemplates'))->singular()
]));
}
}

View file

@ -0,0 +1,53 @@
<?php
namespace App\Http\Controllers\Bo;
use App\Http\Controllers\Controller;
use App\Http\Requests\Bo\ClaimMessages\StoreMessageRequest;
use App\Http\Requests\Bo\ClaimMessages\UpdateMessageRequest;
use App\Models\ClaimMessage;
use App\Traits\Controllers\UpdateModelPublished;
use Illuminate\Support\Facades\DB;
class ClaimMessageController extends Controller
{
use UpdateModelPublished;
/**
* Store a new specific resource.
*
* @param \App\Http\Requests\Bo\ClaimMessages\StoreMessageRequest $request
* @return \Illuminate\Http\JsonResponse
*/
public function store(StoreMessageRequest $request): \Illuminate\Http\JsonResponse
{
$this->authorize('store', ClaimMessage::class);
$validatedData = $request->validated();
return DB::transaction(function () use ($validatedData) {
$claimMessage = (new ClaimMessage())->fill($validatedData);
if ($claimMessage->saveOrFail()) {
$claimMessageModels = ClaimMessage::getAllModelsFromClaim($claimMessage->claim->getKey());
return response()->json($claimMessageModels);
}
return response()->json([], 500);
});
}
/**
* Update a specified resource.
*
* @param \App\Http\Requests\Bo\ClaimMessages\UpdateMessageRequest $request
* @param \App\Models\ClaimMessage $claim_message
* @return \Illuminate\Http\JsonResponse
*/
public function update(UpdateMessageRequest $request, ClaimMessage $claim_message): \Illuminate\Http\JsonResponse
{
$this->authorize('update', ClaimMessage::class);
$claim_message->content = $request->validated()['content'];
$claim_message->saveOrFail();
return response()->json($claim_message);
}
}

View file

@ -0,0 +1,173 @@
<?php
namespace App\Http\Controllers\Bo;
use App\Http\Controllers\Controller;
use App\Http\Requests\Bo\ClaimSubjects\ClaimSubjectRequest;
use App\Models\Brand;
use App\Models\ClaimSubject;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Uwil\LaravelCommon\Helpers\CollectionHelper;
class ClaimSubjectController extends Controller
{
/**
* Create the controller instance.
*/
public function __construct()
{
$this->authorizeResource(ClaimSubject::class);
}
/**
* Display a listing of the resource.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Contracts\View\View
*/
public function index(Request $request): \Illuminate\Contracts\View\View
{
/** @var \Illuminate\Database\Eloquent\Builder $query */
$query = ClaimSubject::query()->when(
$request->filled('brand'),
fn($q) => $q->whereHas('brand', fn($q) => $q->where('id', $request->brand))
);
if ($request->search) {
$this->searchQuery(
$query,
$request->search,
null,
'label',
);
}
$this->sortQuery($query);
$searchFields = trans('validation.custom.label');
$claimSubjectModels = $query->paginate($this->getPagination());
return view('bo.pages.claim_subjects.index', compact('claimSubjectModels', 'searchFields'));
}
/**
* Show the specified resource.
*
* @param \App\Models\ClaimSubject $claim_subject
* @return \Illuminate\Contracts\View\View
*/
public function show(ClaimSubject $claim_subject): \Illuminate\Contracts\View\View
{
return view('bo.pages.claim_subjects.show', [
'claimSubjectModel' => $claim_subject
]);
}
/**
* Show the form for creating a new resource.
*
* @param \App\Models\ClaimSubject $claim_subject
* @return \Illuminate\Contracts\View\View
*/
public function create(ClaimSubject $claim_subject): \Illuminate\Contracts\View\View
{
$brandModels = CollectionHelper::paginate(Brand::getUserAssociatedBrands(), $this->jsonItemPerPagination);
return view('bo.pages.claim_subjects.create', [
'claimSubjectModel' => $claim_subject,
'brandModels' => $brandModels,
]);
}
/**
* Store a newly created resource in storage.
*
* @param \App\Http\Requests\Bo\ClaimSubjects\ClaimSubjectRequest $request
* @return \Illuminate\Http\RedirectResponse
*/
public function store(ClaimSubjectRequest $request): \Illuminate\Http\RedirectResponse
{
return DB::transaction(function () use ($request) {
$claimSubjectModel = new ClaimSubject();
$claimSubjectModel->fill($request->validated());
if ($claimSubjectModel->saveOrFail()) {
return redirect()
->route('bo.claim_subjects.edit', $claimSubjectModel)
->with('success', trans(
':model a été créé.',
['model' => Str::singular(trans('models.classes.claimsubjects'))]
));
}
return redirect()
->back()
->with('error', trans(
':model n\'a pas été créé.',
['model' => Str::singular(trans('models.classes.claimsubjects'))]
));
});
}
/**
* Show the form for editing the specified resource.
*
* @param \App\Models\ClaimSubject $claim_subject
* @return \Illuminate\Contracts\View\View
*/
public function edit(ClaimSubject $claim_subject): \Illuminate\Contracts\View\View
{
$brandModels = CollectionHelper::paginate(Brand::getUserAssociatedBrands(), $this->jsonItemPerPagination);
return view('bo.pages.claim_subjects.edit', [
'claimSubjectModel' => $claim_subject,
'brandModels' => $brandModels,
]);
}
/**
* Update a spacific resource in storage.
*
* @param \App\Http\Requests\Bo\ClaimSubjects\ClaimSubjectRequest $request
* @param \App\Models\ClaimSubject $claim_subject
* @return \Illuminate\Http\RedirectResponse
*/
public function update(ClaimSubjectRequest $request, ClaimSubject $claim_subject): \Illuminate\Http\RedirectResponse
{
return DB::transaction(function () use ($request, $claim_subject) {
$claim_subject->fill($request->validated());
if ($claim_subject->saveOrFail()) {
return redirect()
->route('bo.claim_subjects.edit', $claim_subject)
->with('success', trans(
':model a été mis à jour.',
['model' => Str::singular(trans('models.classes.claimsubjects'))]
));
}
return redirect()
->back()
->with('error', trans(
':model n\'a pas été mis à jour.',
['model' => Str::singular(trans('models.classes.claimsubjects'))]
));
});
}
/**
* Remove the specified resource from storage.
*
* @param \App\Models\ClaimSubject $claim_subject
* @return \Illuminate\Http\RedirectResponse
*/
public function destroy(ClaimSubject $claim_subject): \Illuminate\Http\RedirectResponse
{
if ($claim_subject->deleteOrFail()) {
return redirect()->route('bo.claim_subjects.index')
->with('success', __('crud.messages.has_been_deleted', [
'model' => Str::of(__('models.classes.claimsubjects'))->singular()
]));
}
return redirect()->back()
->with('error', __('crud.messages.cannot_be_deleted', [
'model' => Str::of(__('models.classes.claimsubjects'))->singular()
]));
}
}

View file

@ -0,0 +1,422 @@
<?php
namespace App\Http\Controllers\Bo;
use App\Enums\CustomerExportType;
use App\Http\Controllers\Controller;
use App\Http\Requests\Bo\CustomerExport;
use App\Models\Answer;
use App\Models\Brand;
use App\Models\Customer;
use App\Models\CustomerInfo;
use App\Models\CustomerLogin;
use App\Models\Question;
use App\Models\SingleOptin;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Query\JoinClause;
use Illuminate\Http\Request;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\Session;
use Illuminate\Support\Str;
use League\Csv\Writer;
use Symfony\Component\HttpFoundation\StreamedResponse;
class CustomerController extends Controller
{
/**
* Create the controller instance.
*/
public function __construct()
{
$this->authorizeResource(Customer::class);
}
/**
* View all customers.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Contracts\View\View
*/
public function index(Request $request): \Illuminate\Contracts\View\View
{
$modelClass = Customer::class;
$modelTable = (new Customer())->getTable();
$questionModels = null;
$answerModels = null;
$searchFields = trans('Email, Prénom, Nom');
// Reset session sorts.
if ($request->rstSrts) {
$this->resetSessionSorts($modelTable);
}
// Reset session filters.
if ($request->rstFltrs) {
$this->resetSessionFilters($modelTable);
}
if ($request->filled('search') || Session::get("crud.$modelTable.search")) {
$columns = ['customer_infos.lastname', 'customer_infos.firstname'];
$query = $this->buildCustomerQuery();
// Apply filters.
$this->applyBrandFilter($request, $query, $modelTable, $questionModels, $answerModels);
$this->applySearchFilter($request, $query, $modelTable, $columns);
$this->applySort($request, $query, $modelTable);
$customerModels = $query->with(['customer', 'brand'])->simplePaginate($this->getPagination());
} //end if
return view('bo.pages.customers.index', [
'modelClass' => $modelClass,
'modelTable' => $modelTable,
'customerModels' => $customerModels ?? collect(),
'searchFields' => $searchFields,
'questionModels' => $questionModels,
'answerModels' => $answerModels,
]);
}
/**
* Show a specific custmer.
*
* @param \App\Models\Customer $customer
* @param \App\Models\Brand $brand
* @return \Illuminate\Contracts\View\View
*/
public function show(Customer $customer, Brand $brand): \Illuminate\Contracts\View\View
{
$customerInfos = CustomerInfo::query()->where('customer_id', $customer->id)
->where('brand_id', $brand->id)->firstOrFail();
$customerLogin = CustomerLogin::query()->where('customer_id', $customer->id)
->where('brand_id', $brand->id)->first();
return view('bo.pages.customers.show', compact(
'customerInfos',
'customerLogin'
));
}
/**
* Delete a specific customer info.
*
* @param \App\Models\Customer $customer
* @param \App\Models\Brand $brand
* @return \Illuminate\Http\RedirectResponse
*/
public function delete(Customer $customer, Brand $brand): \Illuminate\Http\RedirectResponse
{
try {
/** @var \App\Models\CustomerInfo */
$customerInfo = $customer->info($brand, Customer::FIRST_OR_FAIL);
/** @var \App\Models\CustomerLogin */
$customerLogin = $customer->login($brand, Customer::FIRST_OR_FAIL);
DB::transaction(function () use (
$brand,
$customer,
$customerLogin,
$customerInfo
) {
if (
$customerInfo->delete() === false ||
$customerLogin->delete() === false
) {
throw new \ErrorException('Api failed to delete customer.');
};
$customer->optInHistory()->whereHas('brand', function ($q) use ($brand) {
$q->where('id', $brand->id);
})->delete();
$logins = $customer->logins()->count();
$infos = $customer->infos()->count();
$history = $customer->optInHistory()->count();
// If customer is not associated with any brand then delete.
if (!$logins and !$infos and !$history) {
if ($customer->delete() === false) {
throw new \ErrorException('Api failed to update customer.');
}
}
});
} catch (\ErrorException $e) {
return redirect()->route('bo.customers.index')
->with('error', trans('Le compte n\'a pas pu être supprimé.'));
} //end try
return redirect()->route('bo.customers.index')->with('warning', trans('Compte supprimé.'));
}
/**
* Show export the customers form.
*
* @return \Illuminate\Contracts\View\View
*/
public function export(): \Illuminate\Contracts\View\View
{
$this->authorize('export', Customer::class);
$brands = Brand::all();
return view('bo.pages.customers.export', compact(
'brands'
));
}
/**
* Exports customers following the requested export type.
*
* @param \App\Http\Requests\Bo\CustomerExport $request
* @return \Symfony\Component\HttpFoundation\StreamedResponse
* @throws \Exception If the request export type in not handled.
*/
public function exportSubmit(CustomerExport $request): \Symfony\Component\HttpFoundation\StreamedResponse
{
$pagination = 2000;
$header = '"lastname";"firstname";"email";"hashcode"';
$w = Writer::createFromString($header . PHP_EOL);
$w->setDelimiter(';');
$w->setEnclosure('"');
$w->setOutputBOM(Writer::BOM_UTF8);
$exportType = CustomerExportType::make(\intval($request->export_type));
// * We send the CSV using Symfony StreamedResponse.
$response = new StreamedResponse();
$response->headers->set('Content-Encoding', 'none');
$response->headers->set('Content-Type', 'text/csv; charset=UTF-8');
$response->headers->set('Description', 'File Transfer');
$response->headers->set(
'Content-Disposition',
sprintf(
'attachment; filename="Export pour newsletter %s %s.csv"',
Carbon::now()->isoFormat('DD-MM-YYYY_HH-mm'),
Str::slug($exportType->label())
)
);
switch ($exportType) {
case CustomerExportType::both:
$continue = true;
// ! Continue
case CustomerExportType::customer:
$page = 1;
/** @var \Illuminate\Database\Eloquent\Builder $req */
$req = CustomerInfo::query()->where('brand_id', $request->brand)
->where('opt_in', true);
$total = $req->count();
$pages = \ceil($total / $pagination);
do {
/** @var \Illuminate\Pagination\LengthAwarePaginator $items */
$items = $req->with('customer', function ($q) {
$q->select(['id', 'email']);
})->paginate(
$pagination,
['lastname', 'firstname', 'hashcode', 'customer_id'],
'page',
$page
);
$items->map(function (CustomerInfo $customerInfo) use ($w) {
$w->insertOne([
'lastname' => $customerInfo->lastname,
'firstname' => $customerInfo->firstname,
'email' => $customerInfo->customer->email,
'hashcode' => $customerInfo->hashcode
]);
});
} while ($page++ <= $pages);
if (!isset($continue)) {
break;
}
// ! Continue
case CustomerExportType::singleOptin:
SingleOptin::query()->whereHas('brand', function (Builder $q) use ($request) {
$q->where('id', $request->brand);
})->get()->map(function (SingleOptin $singleOptin) use ($w) {
$w->insertOne([
'lastname' => '',
'firstname' => '',
'email' => $singleOptin->email,
'hashcode' => $singleOptin->hashcode
]);
});
break;
default:
throw new \Exception(sprintf('Unhandled CustomerExportType %s', $exportType->label()));
} //end switch
/*
We create a callable to output the CSV in chunk
with Symfony StreamResponse you can flush the body content if necessary
see Symfony documentation for more information
*/
// * The flush value should depend on your CSV size.
$flush_threshold = 1000;
$response->setCallback(function () use ($w, $flush_threshold) {
foreach ($w->chunk(1024) as $offset => $chunk) {
echo $chunk;
if ($offset % $flush_threshold === 0) {
flush();
}
}
});
return $response;
}
/**
* Reset all session sorts.
*
* @param string $table
* @return void
*/
private function resetSessionSorts(string $table): void
{
Session::forget([
"crud.$table.sort_col",
"crud.$table.sort_way",
]);
}
/**
* Reset all session filters.
*
* @param string $table
* @return void
*/
private function resetSessionFilters(string $table): void
{
Session::forget([
"crud.$table.brand",
"crud.$table.question",
"crud.$table.answer",
"crud.$table.search",
]);
}
/**
* Build the customer query.
*
* @return Builder
*/
private function buildCustomerQuery(): Builder
{
return CustomerInfo::query()
->join('customer_logins', function (JoinClause $join) {
$join->on('customer_logins.brand_id', '=', 'customer_infos.brand_id')
->on('customer_logins.customer_id', '=', 'customer_infos.customer_id');
})
->join('customers', fn($join) => $join->on('customers.id', '=', 'customer_infos.customer_id'))
->join('brands', fn($join) => $join->on('brands.id', '=', 'customer_infos.brand_id'));
}
/**
* Apply all filters (brand, question, answer).
*
* @param \Illuminate\Http\Request $request
* @param \Illuminate\Database\Eloquent\Builder $query
* @param string $table
* @param \Illuminate\Support\Collection|null $questionModels
* @param \Illuminate\Support\Collection|null $answerModels
* @return void
*/
private function applyBrandFilter(
Request $request,
Builder &$query,
string $table,
?Collection &$questionModels,
?Collection &$answerModels
): void {
$brand = $request->brand ?? Session::get("crud.$table.brand");
if (!(int) $brand) {
return;
}
Session::put("crud.$table.brand", $brand);
$query->where('customer_infos.brand_id', $brand);
$questionModels = Question::getAllModelsFromBrand($brand);
if (($request->filled('question') || Session::has("crud.$table.question")) && $questionModels->count()) {
$question = $request->question ?? Session::get("crud.$table.question");
Session::put("crud.$table.question", $question);
$answerModels = Answer::getAllAnswersFromQuestion($question);
$questionAnswers = Question::find($question)?->answers->modelKeys() ?? [];
$query->whereHas('customer.answers', fn($q) => $q->whereIn('answer_id', $questionAnswers));
if (
($request->filled('answer') || Session::has("crud.$table.answer"))
&& $questionModels->count()
&& $answerModels->count()
) {
$answer = $request->answer ?? Session::get("crud.$table.answer");
Session::put("crud.$table.answer", $answer);
$query->whereHas('customer.answers', fn($q) => $q->whereKey($answer));
}
}
}
/**
* Apply the search filter.
*
* @param \Illuminate\Http\Request $request
* @param \Illuminate\Database\Eloquent\Builder $query
* @param string $table
* @param array $columns
* @return void
*/
private function applySearchFilter(Request $request, Builder &$query, string $table, array $columns): void
{
$search = $request->search ?? Session::get("crud.$table.search");
Session::put("crud.$table.search", $search);
$query->where(function ($q) use ($columns, $search) {
foreach ($columns as $column) {
$q->orWhere($column, 'LIKE', "%$search%");
}
$q->orWhere('customers.email', 'LIKE', "%$search%")
->orWhere('customer_logins.last_connection', 'LIKE', "%$search%");
});
}
/**
* Apply sorts.
*
* @param \Illuminate\Http\Request $request
* @param \Illuminate\Database\Eloquent\Builder $query
* @param string $table
* @return void
*/
private function applySort(Request $request, Builder &$query, string $table): void
{
$sortCol = $request->sort_col ?? Session::get("crud.$table.sort_col");
$sortWay = $request->sort_way ?? Session::get("crud.$table.sort_way");
if ($sortCol) {
Session::put("crud.$table.sort_col", $sortCol);
}
if ($sortWay) {
Session::put("crud.$table.sort_way", $sortWay);
}
$sortable = [
'brands.name',
'customers.email',
'customer_infos.firstname',
'customer_infos.lastname',
'customer_infos.opt_in',
'customer_logins.last_connection',
'customer_logins.status',
];
if ($sortCol && $sortWay && (Schema::hasColumn($table, $sortCol) || in_array($sortCol, $sortable))) {
$query->orderBy($sortCol, $sortWay);
}
}
}

View file

@ -0,0 +1,18 @@
<?php
namespace App\Http\Controllers\Bo;
use App\Http\Controllers\Controller;
class DashboardController extends Controller
{
/**
* Dashboard page.
*
* @return \Illuminate\Contracts\View\View
*/
public function index(): \Illuminate\Contracts\View\View
{
return view('bo.pages.dashboard');
}
}

View file

@ -1,12 +1,13 @@
<?php <?php
namespace App\Http\Controllers; namespace App\Http\Controllers\Bo;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Http\Requests\QuestionExport; use App\Http\Requests\Bo\QuestionExport;
use App\Http\Requests\Questions\StoreQuestionRequest; use App\Http\Requests\Bo\Questions\StoreQuestionRequest;
use App\Http\Requests\Questions\UpdateQuestionRequest; use App\Http\Requests\Bo\Questions\UpdateQuestionRequest;
use App\Models\Brand; use App\Models\Brand;
use App\Models\Customer;
use App\Models\Question; use App\Models\Question;
use Carbon\Carbon; use Carbon\Carbon;
use Illuminate\Http\Request; use Illuminate\Http\Request;
@ -15,25 +16,30 @@ use Illuminate\Support\Str;
use League\Csv\Writer; use League\Csv\Writer;
use Symfony\Component\HttpFoundation\StreamedResponse; use Symfony\Component\HttpFoundation\StreamedResponse;
/**
* @codeCoverageIgnore
*/
class QuestionController extends Controller class QuestionController extends Controller
{ {
/**
* Create the controller instance.
*/
public function __construct()
{
$this->authorizeResource(Question::class);
}
/** /**
* Display a listing of the resource. * Display a listing of the resource.
* *
* @param Request $request * @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response * @return \Illuminate\Contracts\View\View
*/ */
public function index(Request $request) public function index(Request $request): \Illuminate\Contracts\View\View
{ {
/** @var \Illuminate\Database\Eloquent\Builder $query */ /** @var \Illuminate\Database\Eloquent\Builder $query */
$query = Question::query(); $query = Question::query()->when(
$request->filled('brand'),
fn($q) => $q->whereHas('brand', fn($q) => $q->where('id', $request->brand))
);
if ($request->brand) {
$query = $query->where('brand_id', '=', request()->brand);
}
if ($request->search) { if ($request->search) {
$this->searchQuery( $this->searchQuery(
$query, $query,
@ -46,25 +52,26 @@ class QuestionController extends Controller
$this->sortQuery($query); $this->sortQuery($query);
$searchFields = trans('Question'); $searchFields = trans('Question');
/** @var \Illuminate\Pagination\AbstractPaginator */ $query->with(['brand', 'answers']);
$questionModels = $query->paginate(12);
return view('questions.index', compact('questionModels', 'searchFields')); /** @var \Illuminate\Pagination\AbstractPaginator */
$questionModels = $query->paginate($this->getPagination());
return view('bo.pages.questions.index', compact('questionModels', 'searchFields'));
} }
/** /**
* Show the form for creating a new resource. * Show the form for creating a new resource.
* *
* @param \App\Models\Question $question * @param \App\Models\Question $question
* @return \Illuminate\Http\Response * @return \Illuminate\Contracts\View\View
*/ */
public function create(Question $question) public function create(Question $question): \Illuminate\Contracts\View\View
{ {
$brandModels = Brand::get(); $brandModels = Brand::query()->orderBy('name', 'ASC')->paginate($this->jsonItemPerPagination);
return view('bo.pages.questions.create', [
return view('questions.create', [
'questionModel' => $question, 'questionModel' => $question,
'brandModels' => $brandModels 'brandModels' => $brandModels
]); ]);
} }
@ -72,16 +79,16 @@ class QuestionController extends Controller
* Store a newly created resource in storage. * Store a newly created resource in storage.
* *
* @param \App\Http\Requests\Questions\StoreQuestionRequest $request * @param \App\Http\Requests\Questions\StoreQuestionRequest $request
* @return \Illuminate\Http\Response * @return \Illuminate\Http\RedirectResponse
*/ */
public function store(StoreQuestionRequest $request) public function store(StoreQuestionRequest $request): \Illuminate\Http\RedirectResponse
{ {
return DB::transaction(function () use ($request) { return DB::transaction(function () use ($request) {
$question = new Question(); $question = new Question();
$question->fill($request->validated()); $question->fill($request->validated());
if ($question->saveOrFail()) { if ($question->saveOrFail()) {
return redirect() return redirect()
->route('questions.edit', $question) ->route('bo.questions.edit', $question)
->with('success', trans( ->with('success', trans(
':model a été créé.', ':model a été créé.',
['model' => Str::singular(trans('models.classes.questions'))] ['model' => Str::singular(trans('models.classes.questions'))]
@ -100,13 +107,13 @@ class QuestionController extends Controller
* Show the form for editing the specified resource. * Show the form for editing the specified resource.
* *
* @param \App\Models\Question $question * @param \App\Models\Question $question
* @return \Illuminate\Http\Response * @return \Illuminate\Contracts\View\View
*/ */
public function edit(Question $question) public function edit(Question $question): \Illuminate\Contracts\View\View
{ {
$brandModels = Brand::get(); $brandModels = Brand::query()->orderBy('name', 'ASC')->paginate($this->jsonItemPerPagination);
$question->load('brand');
return view('questions.edit', [ return view('bo.pages.questions.edit', [
'questionModel' => $question, 'questionModel' => $question,
'brandModels' => $brandModels 'brandModels' => $brandModels
]); ]);
@ -117,15 +124,15 @@ class QuestionController extends Controller
* *
* @param \App\Http\Requests\Questions\UpdateQuestionRequest $request * @param \App\Http\Requests\Questions\UpdateQuestionRequest $request
* @param \App\Models\Question $question * @param \App\Models\Question $question
* @return \Illuminate\Http\Response * @return \Illuminate\Http\RedirectResponse
*/ */
public function update(UpdateQuestionRequest $request, Question $question) public function update(UpdateQuestionRequest $request, Question $question): \Illuminate\Http\RedirectResponse
{ {
return DB::transaction(function () use ($request, $question) { return DB::transaction(function () use ($request, $question) {
$question->fill($request->validated()); $question->fill($request->validated());
if ($question->saveOrFail()) { if ($question->saveOrFail()) {
return redirect() return redirect()
->route('questions.edit', $question) ->route('bo.questions.edit', $question)
->with('success', trans( ->with('success', trans(
':model a été mis à jour.', ':model a été mis à jour.',
['model' => Str::singular(trans('models.classes.questions'))] ['model' => Str::singular(trans('models.classes.questions'))]
@ -144,11 +151,11 @@ class QuestionController extends Controller
* Show the specified resource. * Show the specified resource.
* *
* @param \App\Models\Question $question * @param \App\Models\Question $question
* @return \Illuminate\Http\Response * @return \Illuminate\Contracts\View\View
*/ */
public function show(Question $question) public function show(Question $question): \Illuminate\Contracts\View\View
{ {
return view('questions.show', [ return view('bo.pages.questions.show', [
'questionModel' => $question 'questionModel' => $question
]); ]);
} }
@ -162,11 +169,11 @@ class QuestionController extends Controller
public function destroy(Question $question): \Illuminate\Http\RedirectResponse public function destroy(Question $question): \Illuminate\Http\RedirectResponse
{ {
if ($question->deleteOrFail()) { if ($question->deleteOrFail()) {
return redirect()->route('questions.index') return redirect()->route('bo.questions.index')
->with('success', trans( ->with('success', trans(
':model a été supprimé.', ':model a été supprimé.',
['model' => Str::singular(trans('models.classes.questions'))] ['model' => Str::singular(trans('models.classes.questions'))]
)); ));
} }
return redirect()->back() return redirect()->back()
->with('error', trans( ->with('error', trans(
@ -178,33 +185,37 @@ class QuestionController extends Controller
/** /**
* Shows the question export form * Shows the question export form
* *
* @return \Illuminate\Http\Response * @return \Illuminate\Contracts\View\View
*/ */
public function export() public function export(): \Illuminate\Contracts\View\View
{ {
$this->authorize('export', Question::class);
$brands = Brand::all(); $brands = Brand::all();
return view('questions.export', compact('brands')); return view('bo.pages.questions.export', compact('brands'));
} }
/** /**
* Exports questions as csv the requested export type * Exports questions as csv the requested export type
* *
* @param QuestionExport $request * @param \App\Http\Requests\QuestionExport $request
* @return \Symfony\Component\HttpFoundation\StreamedResponse * @return \Symfony\Component\HttpFoundation\StreamedResponse
*/ */
public function exportSubmit(QuestionExport $request) public function exportSubmit(QuestionExport $request): \Symfony\Component\HttpFoundation\StreamedResponse
{ {
$results = []; $results = [];
$brand = Brand::findOrFail($request->brand); /** @var \App\Models\Brand $brand */
$brand = Brand::query()->where('id', $request->brand)->firstOrFail();
$questions = $brand->questions->load('answers'); $questions = $brand->questions->load('answers');
foreach ($questions as $question) { foreach ($questions as $question) {
$answersCounts = DB::query() $answersCounts = DB::query()
->selectRaw('count(*) as total, answer_id') ->selectRaw('count(*) as total, answer_id')
->from('customer_answer') ->from('model_answer')
->whereIn('answer_id', $question->answers->pluck('id')) ->where('model_type', '=', Customer::class)
->whereIn('answer_id', $question->answers->modelKeys())
->groupBy('answer_id') ->groupBy('answer_id')
->get()->keyBy('answer_id'); ->get()
->keyBy('answer_id');
$results[$question->id] = [ $results[$question->id] = [
'question' => $question->question, 'question' => $question->question,
@ -217,7 +228,7 @@ class QuestionController extends Controller
$answersCounts[$answer->id]->total ?? 0 $answersCounts[$answer->id]->total ?? 0
]; ];
} }
} } //end foreach
$w = Writer::createFromString(''); $w = Writer::createFromString('');
$w->setDelimiter(';'); $w->setDelimiter(';');
@ -233,7 +244,7 @@ class QuestionController extends Controller
'Content-Disposition', 'Content-Disposition',
sprintf( sprintf(
'attachment; filename="Export pour newsletter %s %s.csv"', 'attachment; filename="Export pour newsletter %s %s.csv"',
Carbon::now()->format('d-m-Y_H-i'), Carbon::now()->isoFormat('DD-MM-YYYY_HH-mm'),
Str::slug($brand->name) Str::slug($brand->name)
) )
); );
@ -248,6 +259,13 @@ class QuestionController extends Controller
$w->insertOne([]); $w->insertOne([]);
} }
// * Push empty line if nothing to export
if (empty($results)) {
$w->insertOne([
'Aucune question pour cette marque'
]);
}
/* /*
We create a callable to output the CSV in chunk We create a callable to output the CSV in chunk
with Symfony StreamResponse you can flush the body content if necessary with Symfony StreamResponse you can flush the body content if necessary

View file

@ -1,10 +1,11 @@
<?php <?php
namespace App\Http\Controllers; namespace App\Http\Controllers\Bo;
use App\Http\Requests\GenerateServiceToken; use App\Http\Controllers\Controller;
use App\Http\Requests\Services\StoreServiceRequest; use App\Http\Requests\Bo\GenerateServiceToken;
use App\Http\Requests\Services\UpdateServiceRequest; use App\Http\Requests\Bo\Services\StoreServiceRequest;
use App\Http\Requests\Bo\Services\UpdateServiceRequest;
use App\Mail\ServiceCredentialRenewed; use App\Mail\ServiceCredentialRenewed;
use App\Models\Brand; use App\Models\Brand;
use App\Models\Service; use App\Models\Service;
@ -13,26 +14,32 @@ use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Mail; use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Uwil\LaravelCommon\Helpers\CollectionHelper;
/**
* @codeCoverageIgnore
*/
class ServiceController extends Controller class ServiceController extends Controller
{ {
/**
* Create the controller instance.
*/
public function __construct()
{
$this->authorizeResource(Service::class);
}
/** /**
* Display a listing of the resource. * Display a listing of the resource.
* *
* @param \Illuminate\Http\Request $request * @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response * @return \Illuminate\Contracts\View\View
*/ */
public function index(Request $request) public function index(Request $request): \Illuminate\Contracts\View\View
{ {
/** @var \Illuminate\Database\Eloquent\Builder $query */ /** @var \Illuminate\Database\Eloquent\Builder $query */
$query = Service::query(); $query = Service::query()->when(
$request->filled('brand'),
fn($q) => $q->whereHas('brand', fn($q) => $q->where('id', $request->brand))
);
if ($request->brand) {
$query = $query->where('brand_id', '=', request()->brand);
}
if ($request->search) { if ($request->search) {
$this->searchQuery( $this->searchQuery(
$query, $query,
@ -45,22 +52,23 @@ class ServiceController extends Controller
$this->sortQuery($query); $this->sortQuery($query);
$searchFields = trans('Nom'); $searchFields = trans('Nom');
$serviceModels = $query->paginate(12); $query->with(['brand']);
return view('services.index', compact('serviceModels', 'searchFields')); $serviceModels = $query->paginate($this->getPagination());
return view('bo.pages.services.index', compact('serviceModels', 'searchFields'));
} }
/** /**
* Show the form for creating a new resource. * Show the form for creating a new resource.
* *
* @param \App\Models\Service $service * @param \App\Models\Service $service
* @return \Illuminate\Http\Response * @return \Illuminate\Contracts\View\View
*/ */
public function create(Service $service) public function create(Service $service): \Illuminate\Contracts\View\View
{ {
$brandModels = Brand::get(); $brandModels = CollectionHelper::paginate(Brand::getUserAssociatedBrands(), $this->jsonItemPerPagination);
return view('bo.pages.services.create', [
return view('services.create', [
'serviceModel' => $service, 'serviceModel' => $service,
'brandModels' => $brandModels 'brandModels' => $brandModels
]); ]);
@ -69,10 +77,10 @@ class ServiceController extends Controller
/** /**
* Store a newly created resource in storage. * Store a newly created resource in storage.
* *
* @param \App\Http\Requests\Services\StoreServiceRequest $request * @param \App\Http\Requests\Bo\Services\StoreServiceRequest $request
* @return \Illuminate\Http\Response * @return \Illuminate\Http\RedirectResponse
*/ */
public function store(StoreServiceRequest $request) public function store(StoreServiceRequest $request): \Illuminate\Http\RedirectResponse
{ {
return DB::transaction(function () use ($request) { return DB::transaction(function () use ($request) {
$service = new Service(); $service = new Service();
@ -81,7 +89,7 @@ class ServiceController extends Controller
$this->generateServicePassword($service); $this->generateServicePassword($service);
if ($service->saveOrFail()) { if ($service->saveOrFail()) {
return redirect() return redirect()
->route('services.edit', $service) ->route('bo.services.edit', $service)
->with('success', trans( ->with('success', trans(
':model a été créé.', ':model a été créé.',
['model' => Str::singular(trans('models.classes.services'))] ['model' => Str::singular(trans('models.classes.services'))]
@ -100,13 +108,12 @@ class ServiceController extends Controller
* Show the form for editing the specified resource. * Show the form for editing the specified resource.
* *
* @param \App\Models\Service $service * @param \App\Models\Service $service
* @return \Illuminate\Http\Response * @return \Illuminate\Contracts\View\View
*/ */
public function edit(Service $service) public function edit(Service $service): \Illuminate\Contracts\View\View
{ {
$brandModels = Brand::get(); $brandModels = CollectionHelper::paginate(Brand::getUserAssociatedBrands(), $this->jsonItemPerPagination);
return view('bo.pages.services.edit', [
return view('services.edit', [
'serviceModel' => $service, 'serviceModel' => $service,
'brandModels' => $brandModels 'brandModels' => $brandModels
]); ]);
@ -115,17 +122,17 @@ class ServiceController extends Controller
/** /**
* Update a spacific resource in storage. * Update a spacific resource in storage.
* *
* @param \App\Http\Requests\Services\UpdateServiceRequest $request * @param \App\Http\Requests\Services\Bo\UpdateServiceRequest $request
* @param \App\Models\Service $service * @param \App\Models\Service $service
* @return \Illuminate\Http\Response * @return \Illuminate\Http\RedirectResponse
*/ */
public function update(UpdateServiceRequest $request, Service $service) public function update(UpdateServiceRequest $request, Service $service): \Illuminate\Http\RedirectResponse
{ {
return DB::transaction(function () use ($request, $service) { return DB::transaction(function () use ($request, $service) {
$service->fill($request->validated()); $service->fill($request->validated());
if ($service->saveOrFail()) { if ($service->saveOrFail()) {
return redirect() return redirect()
->route('services.edit', $service) ->route('bo.services.edit', $service)
->with('success', trans( ->with('success', trans(
':model a été mis à jour.', ':model a été mis à jour.',
['model' => Str::singular(trans('models.classes.services'))] ['model' => Str::singular(trans('models.classes.services'))]
@ -144,11 +151,11 @@ class ServiceController extends Controller
* Show the specified resource. * Show the specified resource.
* *
* @param \App\Models\Service $service * @param \App\Models\Service $service
* @return \Illuminate\Http\Response * @return \Illuminate\Contracts\View\View
*/ */
public function show(Service $service) public function show(Service $service): \Illuminate\Contracts\View\View
{ {
return view('services.show', [ return view('bo.pages.services.show', [
'serviceModel' => $service 'serviceModel' => $service
]); ]);
} }
@ -162,11 +169,11 @@ class ServiceController extends Controller
public function destroy(Service $service): \Illuminate\Http\RedirectResponse public function destroy(Service $service): \Illuminate\Http\RedirectResponse
{ {
if ($service->deleteOrFail()) { if ($service->deleteOrFail()) {
return redirect()->route('services.index') return redirect()->route('bo.services.index')
->with('success', trans( ->with('success', trans(
':model a été supprimé.', ':model a été supprimé.',
['model' => Str::singular(trans('models.classes.services'))] ['model' => Str::singular(trans('models.classes.services'))]
)); ));
} }
return redirect()->back() return redirect()->back()
->with('error', trans( ->with('error', trans(
@ -176,17 +183,19 @@ class ServiceController extends Controller
} }
/** /**
* Regen service credentials * Regen service credentials.
* *
* @param Service $service * @param \App\Http\Requests\Bo\GenerateServiceToken $request
* @param GenerateServiceToken $request * @param \App\Models\Service $service
* @return \Illuminate\Http\RedirectResponse * @return \Illuminate\Http\RedirectResponse
*/ */
// phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClassAfterLastUsed, Squiz.Commenting.FunctionComment.Missing // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClassAfterLastUsed, Squiz.Commenting.FunctionComment.Missing
public function generateServicePassword(Service $service, GenerateServiceToken $request = null) public function generateServicePassword(
{ Service $service,
GenerateServiceToken $request = null
): \Illuminate\Http\RedirectResponse {
/** @var \App\Models\User $user */ /** @var \App\Models\User $user */
$user = auth()->user(); $user = auth('backend')->user();
// * Special Condition for migration from v2 to v3. // * Special Condition for migration from v2 to v3.
$service->bearer_token = !Hash::needsRehash($service->bearer_token) ? $service->bearer_token = !Hash::needsRehash($service->bearer_token) ?

View file

@ -0,0 +1,133 @@
<?php
namespace App\Http\Controllers\Bo;
use App\Http\Controllers\Controller;
use App\Models\Answer;
use App\Models\Question;
use App\Models\SingleOptin;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\Request;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
class SingleOptinController extends Controller
{
/**
* Create the controller instance.
*/
public function __construct()
{
$this->authorizeResource(SingleOptin::class);
}
/**
* Display a listing of the resource.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Contracts\View\View
*/
public function index(Request $request): \Illuminate\Contracts\View\View
{
$questionModels = null;
$answerModels = null;
$searchFields = trans('E-mail');
if ($request->filled('search')) {
$query = SingleOptin::query();
$this->applyBrandAndAnswerFilters($request, $query, $questionModels, $answerModels);
$this->applySearch($request, $query);
$this->sortQuery($query);
$query->with('brand');
$singleoptinModels = $query->simplePaginate($this->getPagination());
}
return view('bo.pages.singleoptins.index', [
'singleoptinModels' => $singleoptinModels ?? collect(),
'searchFields' => $searchFields,
'questionModels' => $questionModels,
'answerModels' => $answerModels,
]);
}
/**
* Remove the specified resource from storage.
*
* @param \App\Models\SingleOptin $singleoptin
* @return \Illuminate\Http\RedirectResponse
*/
public function destroy(SingleOptin $singleoptin): \Illuminate\Http\RedirectResponse
{
$singleoptin->answers()->detach();
if ($singleoptin->deleteOrFail()) {
return redirect()->route('bo.single_optins.index')
->with('success', trans(
':model a été supprimé.',
['model' => Str::singular(trans('models.classes.singleoptins'))]
));
}
return redirect()->back()
->with('error', trans(
':model n\'a pas pu être supprimé.',
['model' => Str::singular(trans('models.classes.singleoptins'))]
));
}
/**
* Undocumented function
*
* @param \Illuminate\Http\Request $request
* @param \Illuminate\Database\Eloquent\Builder $query
* @param \Illuminate\Support\Collection|null $questionModels
* @param \Illuminate\Support\Collection|null $answerModels
* @return void
*/
private function applyBrandAndAnswerFilters(
Request $request,
Builder $query,
?Collection &$questionModels,
?Collection &$answerModels
): void {
if (!$request->filled('brand') || !(int) $request->brand) {
return;
}
$brandId = $request->brand;
$query->whereHas('brand', fn($q) => $q->whereKey($brandId));
$questionModels = Question::getAllModelsFromBrand($brandId);
if (!$request->filled('question')) {
return;
}
$questionId = $request->question;
$question = Question::find($questionId);
if ($question) {
$questionAnswers = $question->answers->modelKeys();
$query->whereHas('answers', fn($q) => $q->whereIn('answer_id', $questionAnswers));
$answerModels = Answer::getAllAnswersFromQuestion($questionId);
}
if ($request->filled('answer')) {
$query->whereHas('answers', fn($q) => $q->whereKey($request->answer));
}
}
/**
* Apply search filter.
*
* @param \Illuminate\Http\Request $request
* @param \Illuminate\Database\Eloquent\Builder $query
* @return void
*/
private function applySearch(Request $request, Builder $query): void
{
if ($request->filled('search')) {
$this->searchQuery($query, $request->search, null, 'email');
}
}
}

View file

@ -1,50 +1,50 @@
<?php <?php
namespace App\Http\Controllers; namespace App\Http\Controllers\Bo;
use App\Http\Requests\StatsByBrandRequest; use App\Http\Controllers\Controller;
use App\Http\Requests\StatsByQuestionRequest; use App\Http\Requests\Bo\StatsByBrandRequest;
use App\Http\Requests\Bo\StatsByQuestionRequest;
use App\Models\Brand; use App\Models\Brand;
use App\Models\BrandStat; use App\Models\BrandStat;
use App\Models\CustomerAnswer; use App\Models\ModelAnswer;
use App\Models\Question; use App\Models\Question;
use Illuminate\Support\Carbon; use Illuminate\Support\Carbon;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Schema; use Illuminate\Support\Facades\Schema;
use League\Csv\Writer; use League\Csv\Writer;
/**
* @codeCoverageIgnore
*/
class StatsController extends Controller class StatsController extends Controller
{ {
/** /**
* Shows stats by brand * Shows stats by brand.
* *
* @param StatsByBrandRequest $request * @param \App\Http\Requests\Bo\StatsByBrandRequest $request
* @return \Illuminate\Http\Response * @return \Illuminate\Http\Response
*/ */
public function viewStatsByBrand(StatsByBrandRequest $request) public function viewStatsByBrand(StatsByBrandRequest $request)
{ {
Gate::authorize('viewStatsByBrand');
$stats = null; $stats = null;
if ($request->post()) { if ($request->post()) {
$stats = BrandStat::whereDate('created_at', '>=', $request->start) $stats = BrandStat::query()->whereDate('created_at', '>=', $request->start)
->whereDate('created_at', '<=', $request->end) ->whereDate('created_at', '<=', $request->end)
->whereBrandId($request->brand)->get(); ->whereBrandId($request->brand)->get();
} }
return view('stats.view-stats-by-brands', \compact('stats')); return view('bo.pages.stats.view-stats-by-brands', \compact('stats'));
} }
/** /**
* Exports stats by brand as a csv file * Exports stats by brand as a csv file.
* *
* @param StatsByBrandRequest $request * @param \App\Http\Requests\Bo\StatsByBrandRequest $request
* @return \Illuminate\Http\Response * @return \Illuminate\Http\Response
*/ */
public function exportByBrandCsv(StatsByBrandRequest $request) public function exportByBrandCsv(StatsByBrandRequest $request): \Illuminate\Http\Response
{ {
$brand = Brand::findOrFail($request->brand); $brand = Brand::query()->findOrFail($request->brand);
$stats = BrandStat::with('brand')->whereDate('created_at', '>=', $request->start) $stats = BrandStat::query()->with('brand')->whereDate('created_at', '>=', $request->start)
->whereDate('created_at', '<=', $request->end) ->whereDate('created_at', '<=', $request->end)
->whereBrandId($request->brand)->get(); ->whereBrandId($request->brand)->get();
@ -62,7 +62,7 @@ class StatsController extends Controller
foreach ($stats as $stat) { foreach ($stats as $stat) {
$cols = [ $cols = [
$stat->created_at->format('Y-m-d'), $stat->created_at->isoFormat('YYYY-MM-DD'),
$stat->brand->name $stat->brand->name
]; ];
foreach ($columns as $colname) { foreach ($columns as $colname) {
@ -70,7 +70,7 @@ class StatsController extends Controller
} }
$w->insertOne($cols); $w->insertOne($cols);
} }
return response((string)$w, 200, [ return response((string) $w, 200, [
'Content-Encoding' => 'none', 'Content-Encoding' => 'none',
'Content-Type' => 'text/csv; charset=UTF-8', 'Content-Type' => 'text/csv; charset=UTF-8',
// phpcs:ignore Generic.Files.LineLength.TooLong // phpcs:ignore Generic.Files.LineLength.TooLong
@ -80,18 +80,19 @@ class StatsController extends Controller
} }
/** /**
* Views stats for a specific question * Views stats for a specific question.
* *
* @param StatsByQuestionRequest $request * @param \App\Http\Requests\Bo\StatsByQuestionRequest $request
* @return \Illuminate\Http\Response * @return \Illuminate\Contracts\View\View
*/ */
public function viewByQuestion(StatsByQuestionRequest $request) public function viewByQuestion(StatsByQuestionRequest $request): \Illuminate\Contracts\View\View
{ {
Gate::authorize('viewByQuestion');
$question = null; $question = null;
$answers = null; $answers = null;
$answersByDays = null; $answersByDays = null;
if ($request->post()) { if ($request->post()) {
$question = Question::findOrFail($request->question); $question = Question::query()->findOrFail($request->question);
$answers = $question->answers; $answers = $question->answers;
$start = Carbon::parse($request->start); $start = Carbon::parse($request->start);
$end = Carbon::parse($request->end); $end = Carbon::parse($request->end);
@ -99,18 +100,18 @@ class StatsController extends Controller
$answersByDays = $this->answersByDays($answers, $start, $end); $answersByDays = $this->answersByDays($answers, $start, $end);
} }
return view('stats.view-stats-by-questions', \compact('question', 'answers', 'answersByDays')); return view('bo.pages.stats.view-stats-by-questions', \compact('question', 'answers', 'answersByDays'));
} }
/** /**
* Exports stats for a specific question as csv * Exports stats for a specific question as csv.
* *
* @param StatsByQuestionRequest $request * @param \App\Http\Requests\Bo\StatsByQuestionRequest $request
* @return \Illuminate\Http\Response * @return \Illuminate\Http\Response
*/ */
public function exportByQuestionCsv(StatsByQuestionRequest $request) public function exportByQuestionCsv(StatsByQuestionRequest $request): \Illuminate\Http\Response
{ {
$question = Question::findOrFail($request->question); $question = Question::query()->findOrFail($request->question);
$answers = $question->answers; $answers = $question->answers;
$start = Carbon::parse($request->start); $start = Carbon::parse($request->start);
$end = Carbon::parse($request->end); $end = Carbon::parse($request->end);
@ -134,7 +135,7 @@ class StatsController extends Controller
$dayAnswers->values()->all() $dayAnswers->values()->all()
)); ));
} }
return response((string)$w, 200, [ return response((string) $w, 200, [
'Content-Encoding' => 'none', 'Content-Encoding' => 'none',
'Content-Type' => 'text/csv; charset=UTF-8', 'Content-Type' => 'text/csv; charset=UTF-8',
// phpcs:ignore Generic.Files.LineLength.TooLong // phpcs:ignore Generic.Files.LineLength.TooLong
@ -144,23 +145,26 @@ class StatsController extends Controller
} }
/** /**
* Makes a collection of total users answers by day * Makes a collection of total users answers by day.
* *
* @param Collection $answers A collection of Answer. * @param \Illuminate\Support\Collection $answers A collection of Answer.
* @param Carbon $start The wanted start period. * @param \Illuminate\Support\Carbon $start The wanted start period.
* @param Carbon $end The wanted end period. * @param \Illuminate\Support\Carbon $end The wanted end period.
* @return Collection A collection with date as Y_m_d index with array of answers and their total. * @return \Illuminate\Support\Collection A collection with date as Y_m_d index
* with array of answers and their total.
*/ */
private function answersByDays(Collection $answers, Carbon $start, Carbon $end): Collection private function answersByDays(Collection $answers, Carbon $start, Carbon $end): \Illuminate\Support\Collection
{ {
Gate::authorize('answersByDays');
// * Totalize by date all answers. // * Totalize by date all answers.
$answersByDays = CustomerAnswer::whereDate('created_at', '>=', $start) $answersByDays = ModelAnswer::query()
->whereDate('created_at', '>=', $start)
->whereDate('created_at', '<=', $end) ->whereDate('created_at', '<=', $end)
->whereIn('answer_id', $answers->pluck('id')) ->whereIn('answer_id', $answers->modelKeys())
->get()->map(function ($custAnswer) { ->get()->map(function ($custAnswer) {
return [ return [
'answer_id' => $custAnswer->answer_id, 'answer_id' => $custAnswer->answer_id,
'created_at' => $custAnswer->created_at->format('Y-m-d'), 'created_at' => $custAnswer->created_at->isoFormat('YYYY-MM-DD'),
]; ];
})->groupBy(['created_at', 'answer_id'])->map(function ($day) { })->groupBy(['created_at', 'answer_id'])->map(function ($day) {
return $day->map(function ($custAnswers) { return $day->map(function ($custAnswers) {
@ -169,7 +173,7 @@ class StatsController extends Controller
}); });
do { do {
$currentDate = $start->format('Y-m-d'); $currentDate = $start->isoFormat('YYYY-MM-DD');
/** @var \Illuminate\Support\Collection $answersByDays */ /** @var \Illuminate\Support\Collection $answersByDays */
if (!$answersByDays->has($currentDate)) { if (!$answersByDays->has($currentDate)) {
$answersByDays->put( $answersByDays->put(
@ -185,7 +189,7 @@ class StatsController extends Controller
$answers->mapWithKeys(function ($answer) use ($curentAnswers) { $answers->mapWithKeys(function ($answer) use ($curentAnswers) {
return [$answer->answer => return [$answer->answer =>
$curentAnswers->has($answer->id) ? $curentAnswers->has($answer->id) ?
$curentAnswers->get($answer->id) : 0]; $curentAnswers->get($answer->id) : 0];
}) })
); );
} }

View file

@ -0,0 +1,175 @@
<?php
namespace App\Http\Controllers\Bo;
use App\Http\Controllers\Controller;
use App\Http\Requests\Bo\Users\StoreUserRequest;
use App\Http\Requests\Bo\Users\UpdateUserRequest;
use App\Models\Brand;
use App\Models\User;
use App\Traits\Controllers\UpdateModelPublished;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Uwil\LaravelCommon\Helpers\CollectionHelper;
class UserController extends Controller
{
use UpdateModelPublished;
/**
* Create the controller instance.
*/
public function __construct()
{
$this->authorizeResource(User::class);
}
/**
* Display a listing of the resource.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Contracts\View\View
*/
public function index(Request $request): \Illuminate\Contracts\View\View
{
/** @var \Illuminate\Database\Eloquent\Builder $query */
$query = User::query();
/** @var string $search Search field */
$search = $request->search;
if ($search) {
$this->searchQuery(
$query,
$search,
null,
'first_name',
'last_name',
'email',
'role',
);
}
$searchFields = \implode(', ', [
trans('validation.attributes.first_name'),
trans('validation.attributes.last_name'),
trans('validation.attributes.email'),
trans('validation.attributes.role'),
]);
/** Sort columns with a query */
$this->sortQuery($query);
/** Custom pagination */
$userModels = $query->paginate($this->getPagination());
return view('bo.pages.users.index', compact('userModels', 'search', 'searchFields'));
}
/**
* Show the form for creating a new resource.
*
* @param \App\Models\User $user
* @return \Illuminate\Contracts\View\View
*/
public function create(User $user): \Illuminate\Contracts\View\View
{
$brandModels = CollectionHelper::paginate(Brand::getUserAssociatedBrands(), $this->jsonItemPerPagination);
return view('bo.pages.users.create', ['userModel' => $user, 'brandModels' => $brandModels]);
}
/**
* Store a newly created resource in storage.
*
* @param \App\Http\Requests\Bo\Users\StoreUserRequest $request
* @return \Illuminate\Http\RedirectResponse
*/
public function store(StoreUserRequest $request): \Illuminate\Http\RedirectResponse
{
return DB::transaction(function () use ($request) {
$user = new User();
$user->fill($request->validated());
if ($user->saveOrFail()) {
$user->brands()->sync(collect($request->validated()['brand_id'])->pluck('id')->toArray());
return redirect()->route('bo.users.edit', $user)
->with('success', __('crud.messages.has_been_created', [
'model' => Str::ucfirst(__('models.classes.user'))
]));
}
return redirect()->back()
->with('error', __('crud.messages.cannot_be_created', [
'model' => Str::ucfirst(__('models.classes.user'))
]));
});
}
/**
* Show the form for editing the specified resource.
*
* @param App\Models\User $user
* @return \Illuminate\Contracts\View\View
*/
public function edit(User $user): \Illuminate\Contracts\View\View
{
$brandModels = CollectionHelper::paginate(Brand::getUserAssociatedBrands(), $this->jsonItemPerPagination);
return view('bo.pages.users.edit', ['userModel' => $user, 'brandModels' => $brandModels]);
}
/**
* Update the specified resource in storage.
*
* @param \App\Http\Requests\Bo\Users\UpdateUserRequest $request
* @param \App\Models\User $user
* @return \Illuminate\Http\RedirectResponse
*/
public function update(UpdateUserRequest $request, User $user): \Illuminate\Http\RedirectResponse
{
return DB::transaction(function () use ($request, $user) {
$user->fill($request->validated());
if ($user->saveOrFail()) {
$user->brands()->sync(collect($request->validated()['brand_id'])->pluck('id')->toArray());
return redirect()->route('bo.users.edit', $user)
->with('success', __('crud.messages.has_been_updated', [
'model' => Str::ucfirst(__('models.classes.user'))
]));
}
return redirect()->route('bo.users.edit', $user)
->with('error', __('crud.messages.cannot_be_updated', [
'model' => Str::ucfirst(__('models.classes.user'))
]));
});
}
/**
* Remove the specified resource from storage.
*
* @param \App\Models\User $user
* @return \Illuminate\Http\RedirectResponse
*/
public function destroy(User $user): \Illuminate\Http\RedirectResponse
{
if ($user->deleteOrFail()) {
return redirect()->route('bo.users.index')
->with('success', __('crud.messages.has_been_deleted', [
'model' => Str::ucfirst(__('models.classes.user'))
]));
}
return redirect()->back()
->with('error', __('crud.messages.cannot_be_deleted', [
'model' => Str::ucfirst(__('models.classes.user'))
]));
}
/**
* Duplicate the specified resource.
*
* @param \App\Models\User $user
* @return \Illuminate\Contracts\View\View
*/
public function duplicate(User $user): \Illuminate\Contracts\View\View
{
return $this->create($user->replicate());
}
}

View file

@ -2,23 +2,28 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Enums\Pagination\ItemsPerPaginationEnum;
use App\Enums\Theme\BootstrapThemeEnum;
use App\Http\Requests\Bo\PaginateModelsRequest;
use App\Http\Requests\Bo\ThemeRequest;
use App\Lib\Helpers\ToolboxHelper;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Foundation\Bus\DispatchesJobs;
use Illuminate\Foundation\Validation\ValidatesRequests; use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Routing\Controller as BaseController; use Illuminate\Routing\Controller as BaseController;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Schema; use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\Session; use Illuminate\Support\Facades\Session;
use Illuminate\Support\Str;
/**
* @codeCoverageIgnore
*/
class Controller extends BaseController class Controller extends BaseController
{ {
use AuthorizesRequests; use AuthorizesRequests;
use DispatchesJobs;
use ValidatesRequests; use ValidatesRequests;
/** @var integer $itemPerPagination */
public $jsonItemPerPagination = 5;
/** /**
* Build Search query on specified fields * Build Search query on specified fields
* splitting search into words or using whole sentence * splitting search into words or using whole sentence
@ -36,7 +41,7 @@ class Controller extends BaseController
string $search, string $search,
callable $searchQuery = null, callable $searchQuery = null,
string ...$fields string ...$fields
): Builder { ): \Illuminate\Database\Eloquent\Builder {
if (count($fields)) { if (count($fields)) {
// Search using words. // Search using words.
$words = explode(' ', $search); $words = explode(' ', $search);
@ -65,36 +70,85 @@ class Controller extends BaseController
* @param array $fields * @param array $fields
* @return \Illuminate\Database\Eloquent\Builder * @return \Illuminate\Database\Eloquent\Builder
*/ */
private function searchOnFields(Builder $query, string $search, array $fields): Builder private function searchOnFields(
{ Builder $query,
string $search,
array $fields
): \Illuminate\Database\Eloquent\Builder {
$table = $query->getModel()->getTable(); $table = $query->getModel()->getTable();
foreach ($fields as $f) { foreach ($fields as $field) {
$f = str_replace('?', '\?', $f); $field = str_replace('?', '\?', $field);
if (is_array($f)) { if (is_array($field)) {
$query->orWhereRaw( $query->orWhereRaw(
sprintf("UPPER(CONCAT(%s)) LIKE UPPER(?)", collect($f)->map(function ($q) use ($table) { sprintf("UPPER(CONCAT(%s)) LIKE UPPER(?)", collect($field)->map(function ($q) use ($table) {
return "`$table`.`$q`"; return "`$table`.`$q`";
})->implode(', \' \', ')), })->implode(', \' \', ')),
['%' . filter_var($search, FILTER_SANITIZE_STRING) . '%'] ['%' . htmlspecialchars($search) . '%']
);
} else {
$query->orWhereRaw(
"UPPER(`$table`.`$f`) LIKE UPPER(?)",
['%' . filter_var($search, FILTER_SANITIZE_STRING) . '%']
); );
return $query;
} }
} // Get models woth enum's label.
if (is_array($this->getSearchValue($query, $search, $field))) {
foreach ($this->getSearchValue($query, $search, $field) as $array) {
$query->orWhereRaw(
"UPPER(`$table`.`$field`) LIKE UPPER(?)",
['%' . htmlspecialchars($array->value) . '%']
);
}
return $query;
}
// Get models with relation's name/label.
if (Str::endsWith($field, '_id') && $query->getModel()->has(Str::remove('_id', $field))) {
$query->orWhereHas(Str::remove('_id', $field), function ($queryRelation) use ($field, $search) {
if (Schema::hasColumn(Str::remove('_id', $field) . 's', 'name')) {
$queryRelation->where('name', 'like', '%' . $search . '%');
} elseif (Schema::hasColumn(Str::remove('_id', $field) . 's', 'label')) {
$queryRelation->where('label', 'like', '%' . $search . '%');
}
});
return $query;
}
$query->orWhereRaw(
"UPPER(`$table`.`$field`) LIKE UPPER(?)",
['%' . htmlspecialchars($this->getSearchValue($query, $search, $field)) . '%']
);
} //end foreach
return $query; return $query;
} }
/** /**
* Sort columns with a query * Get the search value (on string or Enum).
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @param string $search
* @param string $field
* @return string|array
*/
protected function getSearchValue(Builder $query, string $search, string $field): string|array
{
if (
count($query->getModel()->getCasts()) and // Check if the model has casts.
isset($query->getModel()->getCasts()[$field]) and // Check if there is cast about specific field.
enum_exists($query->getModel()->getCasts()[$field]) // Check if the cast is an enum.
) {
$cases = collect($query->getModel()->getCasts()[$field]::toArray());
$enum = $cases->filter(function ($item) use ($search) {
return preg_match('/' . Str::lower($search) . '/', Str::lower($item->label));
});
return $enum->toArray();
} else {
return $search;
}
}
/**
* Sort columns with a query.
* *
* @param \Illuminate\Database\Eloquent\Builder $query * @param \Illuminate\Database\Eloquent\Builder $query
* @param array $ignore * @param array $ignore
* @return \Illuminate\Database\Eloquent\Builder * @return \Illuminate\Database\Eloquent\Builder
*/ */
protected function sortQuery(Builder $query, array $ignore = []): Builder protected function sortQuery(Builder $query, array $ignore = []): \Illuminate\Database\Eloquent\Builder
{ {
$rName = request()->route()->getName(); $rName = request()->route()->getName();
$table = $query->getModel()->getTable(); $table = $query->getModel()->getTable();
@ -106,7 +160,7 @@ class Controller extends BaseController
Session::put("$rName.sort_way", request()->sort_way); Session::put("$rName.sort_way", request()->sort_way);
Session::put("$rName.sorted", true); Session::put("$rName.sorted", true);
} }
if (request()->rst) { if (request()->rstSrts) {
Session::remove("$rName.sorted"); Session::remove("$rName.sorted");
Session::remove("$rName.sort_col"); Session::remove("$rName.sort_col");
Session::remove("$rName.sort_way"); Session::remove("$rName.sort_way");
@ -122,7 +176,72 @@ class Controller extends BaseController
$query = $query->orderBy('order', 'ASC'); $query = $query->orderBy('order', 'ASC');
Session::put("$rName.sort_col", 'order'); Session::put("$rName.sort_col", 'order');
Session::put("$rName.sort_way", 'asc'); Session::put("$rName.sort_way", 'asc');
} elseif (Schema::hasColumn($table, 'updated_at')) {
$query = $query->orderBy('updated_at', 'DESC');
Session::put("$rName.sort_col", 'updated_at');
Session::put("$rName.sort_way", 'desc');
} elseif (Schema::hasColumn($table, 'created_at')) {
$query = $query->orderBy('created_at', 'DESC');
Session::put("$rName.sort_col", 'created_at');
Session::put("$rName.sort_way", 'desc');
} }
return $query; return $query;
} }
/**
* Get the number of items per page.
*
* @param \App\Enums\Pagination\ItemsPerPaginationEnum|null $pagination
* @return integer
*/
protected function getPagination(ItemsPerPaginationEnum $pagination = null): int
{
$currentRoutePath = Str::slug(request()->route()->getName());
$cacheKey = "pagination.{$currentRoutePath}";
$pagination = $pagination ?? ItemsPerPaginationEnum::twelve;
$pagination = ToolboxHelper::getValidatedEnum(
Cache::get($cacheKey, $pagination->value()),
'pagination',
'\App\Enums\Pagination\ItemsPerPaginationEnum',
);
if (Cache::get($cacheKey) !== $pagination) {
Cache::put($cacheKey, $pagination);
}
return $pagination;
}
/**
* Get a paginate resource.
*
* @param \App\Http\Requests\Bo\PaginateModelsRequest $request
* @return \Illuminate\Http\JsonResponse
*/
public function jsonPaginateModels(PaginateModelsRequest $request): \Illuminate\Http\JsonResponse
{
/** @var \Illuminate\Database\Eloquent\Model $models */
$models = $request->targetModel::query()
->where($request->input('fieldName') ?? 'name', 'like', "%{$request->input('search')}%")
->orderBy($request->input('fieldName') ?? 'name', 'ASC')
->paginate($request->input('paginate') ?? 10);
return \response()->json($models);
}
/**
* Set the bootstrap theme or light (default).
*
* @param \App\Http\Requests\Bo\ThemeRequest $request
* @return \Illuminate\Http\RedirectResponse
*/
protected function setTheme(ThemeRequest $request): \Illuminate\Http\RedirectResponse
{
$theme = ToolboxHelper::getValidatedEnum(
$request->validated('theme') ?: Cache::get('theme', BootstrapThemeEnum::light->value()),
'theme',
'\App\Http\Controllers\BootstrapThemeEnum',
);
if (Cache::get("theme") !== $theme) {
Cache::put("theme", $theme);
}
return redirect()->back()->with('success', __('crud.messages.theme_updated'));
}
} }

View file

@ -1,307 +0,0 @@
<?php
namespace App\Http\Controllers;
use App\Enums\CustomerExportType;
use App\Http\Requests\CustomerExport;
use App\Models\Brand;
use App\Models\Customer;
use App\Models\CustomerInfo;
use App\Models\CustomerLogin;
use App\Models\SingleOptin;
use Illuminate\Database\Query\JoinClause;
use Illuminate\Http\Request;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\Session;
use Illuminate\Support\Str;
use League\Csv\Writer;
use Symfony\Component\HttpFoundation\StreamedResponse;
/**
* @codeCoverageIgnore
*/
class CustomerController extends Controller
{
/**
* View all customers
*
* @param Request $request
* @return \Illuminate\Http\Response
*/
public function index(Request $request)
{
$modelTable = (new Customer())->getTable();
$modelClass = Customer::class;
$brands = Brand::select('id', 'name')->get();
$columns = ['customer_infos.lastname', 'customer_infos.firstname'];
// * Jointure pour pouvoir trier les colonnes.
/** @var \Illuminate\Database\Eloquent\Builder */
$query = CustomerInfo::join('customer_logins', function (JoinClause $join) {
$join->on('customer_logins.brand_id', '=', 'customer_infos.brand_id')
->on('customer_logins.customer_id', '=', 'customer_infos.customer_id');
})
->join('customers', function (JoinClause $join) {
$join->on('customers.id', '=', 'customer_infos.customer_id');
})
->join('brands', function (JoinClause $join) {
$join->on('brands.id', '=', 'customer_infos.brand_id');
});
if ($request->rst) {
Session::remove("crud.$modelTable.brand");
Session::remove("crud.$modelTable.search");
Session::remove("crud.$modelTable.sort_way");
Session::remove("crud.$modelTable.sort_col");
}
if (request()->brand or Session::has("crud.$modelTable.brand")) {
$brand = $request->brand ?? Session::get("crud.$modelTable.brand");
Session::put("crud.$modelTable.brand", $brand);
$query = $query->where('customer_infos.brand_id', $brand);
}
// * Search on crud Label column.
if ($request->search or Session::has("crud.$modelTable.search")) {
$search = $request->search ?? Session::get("crud.$modelTable.search");
Session::put("crud.$modelTable.search", $search);
$query->where(function ($q) use ($columns, $search) {
foreach ($columns as $column) {
$q = $q->orWhere($column, 'LIKE', "%$search%");
}
$q = $q->orWhere('customers.email', 'LIKE', "%$search%");
$q = $q->orWhere('customer_logins.last_connection', 'LIKE', "%$search%");
});
}
// * Sort columns.
if ($request->sort_col) {
Session::put("crud.$modelTable.sort_col", $request->sort_col);
}
if ($request->sort_way) {
Session::put("crud.$modelTable.sort_way", $request->sort_way);
}
if (
Session::has("crud.$modelTable.sort_col") and
Session::has("crud.$modelTable.sort_way") and
(Schema::hasColumn($modelTable, Session::get("crud.$modelTable.sort_col")) or
in_array(Session::get("crud.$modelTable.sort_col"), [
'brands.name',
'customers.email',
'customer_infos.firstname',
'customer_infos.lastname',
'customer_infos.opt_in',
'customer_logins.last_connection',
'customer_logins.status'
]))
) {
$query = $query->orderBy(
Session::get("crud.$modelTable.sort_col"),
Session::get("crud.$modelTable.sort_way")
);
}
$searchFields = trans('Email, Prénom, Nom');
/** @var \Illuminate\Pagination\AbstractPaginator */
$customerModels = $query->paginate(50);
return view('customers.index', compact(
'modelClass',
'modelTable',
'customerModels',
'brands',
'searchFields'
));
}
/**
* Show a specific custmer
*
* @param Customer $customer
* @param Brand $brand
* @return \Illuminate\Http\Response
*/
public function show(Customer $customer, Brand $brand)
{
$customerInfos = CustomerInfo::where('customer_id', $customer->id)
->where('brand_id', $brand->id)->firstOrFail();
$customerLogin = CustomerLogin::where('customer_id', $customer->id)
->where('brand_id', $brand->id)->first();
return view('customers.show', compact(
'customerInfos',
'customerLogin'
));
}
/**
* Delete a specific customer info
*
* @param Customer $customer
* @param Brand $brand
* @return \Illuminate\Http\RedirectResponse
*/
public function delete(Customer $customer, Brand $brand)
{
try {
// TODO: Gestion avec mesBR.
/** @var \App\Models\CustomerInfo */
$customerInfo = $customer->info($brand, Customer::FIRST_OR_FAIL);
/** @var \App\Models\CustomerLogin */
$customerLogin = $customer->login($brand, Customer::FIRST_OR_FAIL);
DB::transaction(function () use (
$brand,
$customer,
$customerLogin,
$customerInfo
) {
if (
$customerInfo->delete() === false ||
$customerLogin->delete() === false
) {
throw new \ErrorException('Api failed to delete customer.');
};
$customer->optInHistory()->whereHas('brand', function ($q) use ($brand) {
$q->where('id', $brand->id);
})->delete();
$logins = $customer->logins()->count();
$infos = $customer->infos()->count();
$history = $customer->optInHistory()->count();
// If customer is not associated with any brand then delete.
if (!$logins and !$infos and !$history) {
if ($customer->delete() === false) {
throw new \ErrorException('Api failed to update customer.');
}
}
});
} catch (\ErrorException $e) {
return redirect()->route('customers.index')
->with('error', trans('Le compte n\'a pas pu être supprimé.'));
} //end try
return redirect()->route('customers.index')->with('warning', trans('Compte supprimé.'));
}
/**
* Show export the customers form
*
* @return \Illuminate\Http\Response
*/
public function export()
{
$brands = Brand::all();
return view('customers.export', compact(
'brands'
));
}
/**
* Exports customers following the requested export type
*
* @param CustomerExport $request
* @return \Symfony\Component\HttpFoundation\StreamedResponse
* @throws \Exception If the request export type in not handled.
*/
public function exportSubmit(CustomerExport $request)
{
$pagination = 2000;
$header = '"lastname";"firstname";"email";"hashcode"';
$w = Writer::createFromString($header . PHP_EOL);
$w->setDelimiter(';');
$w->setEnclosure('"');
$w->setOutputBOM(Writer::BOM_UTF8);
$exportType = CustomerExportType::make(\intval($request->export_type));
// * We send the CSV using Symfony StreamedResponse.
$response = new StreamedResponse();
$response->headers->set('Content-Encoding', 'none');
$response->headers->set('Content-Type', 'text/csv; charset=UTF-8');
$response->headers->set('Description', 'File Transfer');
$response->headers->set(
'Content-Disposition',
sprintf(
'attachment; filename="Export pour newsletter %s %s.csv"',
Carbon::now()->format('d-m-Y_H-i'),
Str::slug($exportType)
)
);
switch ($exportType) {
case CustomerExportType::both():
$continue = true;
// ! Continue
case CustomerExportType::customer():
$page = 1;
/** @var \Illuminate\Database\Eloquent\Builder $req */
$req = CustomerInfo::where('brand_id', $request->brand)
->where('opt_in', true);
$total = $req->count();
$pages = \ceil($total / $pagination);
do {
/** @var \Illuminate\Pagination\LengthAwarePaginator $items */
$items = $req->with('customer', function ($q) {
$q->select(['id', 'email']);
})->paginate(
$pagination,
['lastname', 'firstname', 'hashcode', 'customer_id'],
'page',
$page
);
$items->map(function (CustomerInfo $customerInfo) use ($w) {
$w->insertOne([
'lastname' => $customerInfo->lastname,
'firstname' => $customerInfo->firstname,
'email' => $customerInfo->customer->email,
'hashcode' => $customerInfo->hashcode
]);
});
} while ($page++ <= $pages);
if (!isset($continue)) {
break;
}
// ! Continue
case CustomerExportType::singleOptin():
SingleOptin::where('brand_id', $request->brand)->get()
->map(function (SingleOptin $singleOptin) use ($w) {
$w->insertOne([
'lastname' => '',
'firstname' => '',
'email' => $singleOptin->email,
'hashcode' => ''
]);
});
break;
default:
throw new \Exception(sprintf('Unhandled CustomerExportType %s', $exportType->label));
} //end switch
/*
We create a callable to output the CSV in chunk
with Symfony StreamResponse you can flush the body content if necessary
see Symfony documentation for more information
*/
// * The flush value should depend on your CSV size.
$flush_threshold = 1000;
$response->setCallback(function () use ($w, $flush_threshold) {
foreach ($w->chunk(1024) as $offset => $chunk) {
echo $chunk;
if ($offset % $flush_threshold === 0) {
flush();
}
}
});
return $response;
}
}

View file

@ -1,54 +0,0 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
/**
* @codeCoverageIgnore
*/
class LoginController extends Controller
{
/**
* Shows the login view
*
* @return \Illuminate\Http\Response
*/
public function index()
{
return view('login');
}
/**
* Undocumented function
*
* @param Request $request
* @return \Illuminate\Http\Response
*/
public function authenticate(Request $request)
{
if (
Auth::attempt(
["email" => $request->email, "password" => $request->password],
$request->remember !== null ? true : false
)
) {
return redirect()->route('customers.index');
} else {
return back()->withInput($request->except("password"))
->withErrors("Les identifiants entrés sont incorrects, veuillez réessayer.");
}
}
/**
* Undocumented function
*
* @return \Illuminate\Http\Response
*/
public function logout()
{
Auth::logout();
return redirect()->route('login');
}
}

View file

@ -1,67 +0,0 @@
<?php
namespace App\Http\Controllers;
use App\Models\SingleOptin;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
/**
* @codeCoverageIgnore
*/
class SingleOptinController extends Controller
{
/**
* Display a listing of the resource.
*
* @param Request $request
* @return \Illuminate\Http\Response
*/
public function index(Request $request)
{
/** @var \Illuminate\Database\Eloquent\Builder $query */
$query = SingleOptin::query();
if ($request->brand) {
$query = $query->where('brand_id', '=', request()->brand);
}
if ($request->search) {
$this->searchQuery(
$query,
$request->search,
null,
'email',
);
}
$this->sortQuery($query);
$searchFields = trans('E-mail');
/** @var \Illuminate\Pagination\AbstractPaginator */
$singleoptinModels = $query->paginate(12);
return view('singleoptins.index', compact('singleoptinModels', 'searchFields'));
}
/**
* Remove the specified resource from storage.
*
* @param \App\Models\SingleOptin $singleoptin
* @return \Illuminate\Http\RedirectResponse
*/
public function destroy(SingleOptin $singleoptin): \Illuminate\Http\RedirectResponse
{
if ($singleoptin->deleteOrFail()) {
return redirect()->route('single_optins.index')
->with('success', trans(
':model a été supprimé.',
['model' => Str::singular(trans('models.classes.singleoptins'))]
));
}
return redirect()->back()
->with('error', trans(
':model n\'a pas pu être supprimé.',
['model' => Str::singular(trans('models.classes.singleoptins'))]
));
}
}

View file

@ -14,12 +14,12 @@ class Kernel extends HttpKernel
* @var array * @var array
*/ */
protected $middleware = [ protected $middleware = [
\App\Http\Middleware\TrustHosts::class,
\App\Http\Middleware\TrustProxies::class, \App\Http\Middleware\TrustProxies::class,
\Fruitcake\Cors\HandleCors::class, \Illuminate\Http\Middleware\HandleCors::class,
\App\Http\Middleware\PreventRequestsDuringMaintenance::class, \App\Http\Middleware\PreventRequestsDuringMaintenance::class,
\Illuminate\Foundation\Http\Middleware\ValidatePostSize::class, \Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
\App\Http\Middleware\TrimStrings::class, \App\Http\Middleware\TrimStrings::class,
\App\Http\Middleware\XssSanitizer::class
]; ];
/** /**
@ -36,15 +36,20 @@ class Kernel extends HttpKernel
\Illuminate\View\Middleware\ShareErrorsFromSession::class, \Illuminate\View\Middleware\ShareErrorsFromSession::class,
\App\Http\Middleware\VerifyCsrfToken::class, \App\Http\Middleware\VerifyCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class, \Illuminate\Routing\Middleware\SubstituteBindings::class,
\App\Http\Middleware\SecureHeaders::class
], ],
'api' => [ 'api' => [
'throttle:api', \Illuminate\Routing\Middleware\ThrottleRequests::class . ':api',
\App\Http\Middleware\JsonOnly::class, \App\Http\Middleware\JsonOnly::class,
\App\Http\Middleware\ApiResponse::class, \App\Http\Middleware\ApiResponse::class,
\App\Http\Middleware\ApiCheckTimeStamps::class, \App\Http\Middleware\ApiCheckTimeStamps::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class \Illuminate\Routing\Middleware\SubstituteBindings::class
], ],
'saml' => [
\App\Http\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
],
]; ];
/** /**
@ -55,15 +60,17 @@ class Kernel extends HttpKernel
* @var array * @var array
*/ */
protected $routeMiddleware = [ protected $routeMiddleware = [
'auth' => \App\Http\Middleware\Authenticate::class, 'auth' => \App\Http\Middleware\Authenticate::class,
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class, 'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class, 'auth.session' => \Illuminate\Session\Middleware\AuthenticateSession::class,
'can' => \Illuminate\Auth\Middleware\Authorize::class, 'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
'check-brand' => \App\Http\Middleware\CheckBrand::class, 'can' => \Illuminate\Auth\Middleware\Authorize::class,
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class, 'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class, 'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class,
'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class, 'signed' => \App\Http\Middleware\ValidateSignature::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class, 'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class, 'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
'htmlsanitize' => \App\Http\Middleware\HtmlSanitizer::class,
'logoutusers' => \App\Http\Middleware\LogoutUnpublishedUsers::class,
]; ];
} }

View file

@ -3,7 +3,7 @@
namespace App\Http\Middleware; namespace App\Http\Middleware;
use App\Enums\ApiError; use App\Enums\ApiError;
use App\Exceptions\Api\V2\ApiErrorException; use App\Exceptions\Api\V4\ApiErrorException;
use App\Lib\Helper; use App\Lib\Helper;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Carbon; use Illuminate\Support\Carbon;
@ -18,22 +18,24 @@ class ApiCheckTimeStamps
/** /**
* Handle an incoming request. * Handle an incoming request.
* *
* @param \Illuminate\Http\Request $request * @param \Illuminate\Http\Request $request
* @param \Closure $next * @param \Closure $next
* @return mixed * @return mixed
* @throws ApiErrorException With ApiError::wrongTimeStamp if timestamp has an incorrect format or is timed out. * @throws \App\Exceptions\Api\V4\ApiErrorException With ApiError::wrongTimeStamp
* if timestamp has an incorrect
* format or is timed out.
*/ */
public function handle(Request $request, \Closure $next) public function handle(Request $request, \Closure $next): mixed
{ {
if (Helper::isRequestForApiV2($request)) { if (Helper::isRequestForApiV2($request) || Helper::isRequestForApiV4($request)) {
if (!\is_string($request->timestamp) or !Carbon::hasFormat($request->timestamp, 'Y-m-d H:i:s')) { if (!\is_string($request->timestamp) or !Carbon::hasFormat($request->timestamp, 'Y-m-d H:i:s')) {
throw new ApiErrorException(ApiError::wrongTimeStamp()); throw new ApiErrorException(ApiError::wrongTimeStamp);
} }
if ( if (
Carbon::parse($request->timestamp, 'utc') Carbon::parse($request->timestamp, 'utc')
->isBefore(Carbon::now('UTC')->subSeconds(config('api.timestampTimeout', 10))) ->isBefore(Carbon::now('UTC')->subSeconds(config('api.timestampTimeout', 10)))
) { ) {
throw new ApiErrorException(ApiError::wrongTimeStamp()); throw new ApiErrorException(ApiError::wrongTimeStamp);
} }
} }
return $next($request); return $next($request);

View file

@ -4,12 +4,11 @@ namespace App\Http\Middleware;
use App\Enums\ApiStatus; use App\Enums\ApiStatus;
use App\Http\Resources\Api\V2\ApiResponse as V2ApiResponse; use App\Http\Resources\Api\V2\ApiResponse as V2ApiResponse;
use App\Http\Resources\Api\V4\ApiResponse as V4ApiResponse;
use App\Lib\Helper; use App\Lib\Helper;
use App\Models\Service; use App\Models\Service;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
class ApiResponse class ApiResponse
{ {
@ -21,7 +20,7 @@ class ApiResponse
* @return mixed * @return mixed
* @throws \ErrorException If the received response is not a JsonResponse. * @throws \ErrorException If the received response is not a JsonResponse.
*/ */
public function handle(Request $request, \Closure $next) public function handle(Request $request, \Closure $next): mixed
{ {
/** @var \Illuminate\Http\JsonResponse $response */ /** @var \Illuminate\Http\JsonResponse $response */
$response = $next($request); $response = $next($request);
@ -35,19 +34,6 @@ class ApiResponse
// Set no cache header. // Set no cache header.
$response->header('Cache-Control', 'No-Store'); $response->header('Cache-Control', 'No-Store');
if (Helper::isRequestForApiV1($request)) {
// @codeCoverageIgnoreStart
try {
$bearerToken = Helper::getBearerTokenFromRequest($request) ?? $request->api_token ?? '';
$service = Service::getFromBearerToken($bearerToken);
$service->incrementStat();
} catch (ModelNotFoundException $e) {
Log::error(sprintf('Could not increment stats %s', $e->getMessage()));
}
return $response;
// @codeCoverageIgnoreEnd
}
/** @var \App\Models\Service|null */ /** @var \App\Models\Service|null */
if (\optional($service = app(Service::class))->id) { if (\optional($service = app(Service::class))->id) {
$service->incrementStat(); $service->incrementStat();
@ -56,18 +42,23 @@ class ApiResponse
/** @var object $data */ /** @var object $data */
$data = $response->getData(); $data = $response->getData();
// If reponse is already a V2ApiResponse then just return it. This happens if error handler comes to action. /** Check if it's V2 or V4 response. */
if ( if (
gettype($data) === "object" and
isset($data->status) and isset($data->status) and
isset($data->data) and isset($data->data) and
isset($data->version) and isset($data->version) and
in_array($data->status, [ApiStatus::OK()->value, ApiStatus::KO()->value]) in_array($data->status, [ApiStatus::OK->value(), ApiStatus::KO->value()])
) { ) {
/** Return direct ApiResponse */
return $response; return $response;
} }
/** @var \App\Enums\ApiStatus $status */ $status = intval($response->getStatusCode() / 100) == 2 ? ApiStatus::OK : ApiStatus::KO;
$status = intval($response->getStatusCode() / 100) == 2 ? ApiStatus::OK() : ApiStatus::KO();
return $response->setData(new V2ApiResponse((object)$response->getData(), $status)); if (Helper::isRequestForApiV2($request)) {
return $response->setData(new V2ApiResponse($status, (object) $data));
}
return $response->setData(new V4ApiResponse($status, (object) $data));
} }
} }

View file

@ -2,57 +2,38 @@
namespace App\Http\Middleware; namespace App\Http\Middleware;
use App\Lib\Helper;
use App\Models\Service;
use Closure;
use Illuminate\Auth\Middleware\Authenticate as Middleware; use Illuminate\Auth\Middleware\Authenticate as Middleware;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Session;
/**
* Updated for ApiV2
*/
class Authenticate extends Middleware class Authenticate extends Middleware
{ {
/**
* Handle an incoming request.
*
* @param mixed $request
* @param \Closure $next
* @param mixed ...$guards
* @return mixed
*
* @throws \Illuminate\Auth\AuthenticationException If authentification fails.
*/
public function handle($request, Closure $next, ...$guards)
{
if (Helper::isRequestForApiV1($request)) {
// @codeCoverageIgnoreStart
// Use patched v1 Auth mecanism (without bearer reimplementation).
$bearerToken = Helper::getBearerTokenFromRequest($request) ?? $request->api_token ?? '';
$service = Service::getFromBearerToken($bearerToken);
$request->request->add(['brand_id' => $service->brand->id]);
$response = $next($request);
// @codeCoverageIgnoreEnd
} else {
// Use Laravel auth check mecanism.
$response = \call_user_func_array(
[$this, 'parent::' . __FUNCTION__],
array_merge([$request, $next], $guards)
);
}
return $response;
}
/** /**
* Get the path the user should be redirected to when they are not authenticated. * Get the path the user should be redirected to when they are not authenticated.
* *
* @param mixed $request * @param \Illuminate\Http\Request $request
* @return string|null * @return string|null
* @codeCoverageIgnore
*/ */
protected function redirectTo($request) protected function redirectTo(Request $request): ?string
{ {
if (! $request->expectsJson()) { $logins = [
return route('login'); 'backend' => route('bo.login')
];
if (!$request->expectsJson()) {
// * Notify must login
Session::flash('warning', trans('Veuillez vous connecter pour continuer'));
foreach ($request->route()->gatherMiddleware() as $middleware) {
if (\is_string($middleware) and strpos($middleware, 'auth:') !== false) {
$route = $logins[str_replace('auth:', '', $middleware)] ?? null;
if ($route) {
return $route;
}
}
}
return route('bo.login');
} }
return null;
} }
} }

View file

@ -1,40 +0,0 @@
<?php
namespace App\Http\Middleware;
use App\Lib\Helper;
use App\Models\Service;
use Closure;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Http\Request;
/**
* @codeCoverageIgnore
*/
class CheckBrand
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle(Request $request, Closure $next)
{
$apiToken = Helper::getBearerTokenFromRequest($request) ?? $request->api_token ?? '';
if ($apiToken === null) {
abort(401, 'Le token d\'authentification est incorrect ' . \json_encode($request->all()));
}
try {
$brand = Service::getFromBearerToken($apiToken)->brand;
$request->request->add(['brand_id' => $brand->id]);
} catch (ModelNotFoundException $e) {
abort(401, 'Le token d\'authentification est incorrect ' . $e->getMessage());
}
return $next($request);
}
}

View file

@ -0,0 +1,63 @@
<?php
namespace App\Http\Middleware;
use App\Lib\Helpers\ToolboxHelper;
use Illuminate\Http\Request;
class HtmlSanitizer
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @param string|array ...$params
* @return \Symfony\Component\HttpFoundation\Response
*/
public function handle(
Request $request,
\Closure $next,
string|array ...$params
): \Symfony\Component\HttpFoundation\Response {
$inputs = $request->all();
/** @var callable|null */
$cleanInput = null;
$cleanInput = function (
null|string|array &$input,
string $name,
?string $rootName = null
) use (
$params,
&$cleanInput
): void {
// * Nested element array
if (is_array($input)) {
array_walk_recursive($input, $cleanInput, $name);
}
if (!is_string($input)) {
return;
}
if (in_array($name, $params) or in_array($rootName ?? $name, $params)) {
// ? Is a json input ?
if ($isJson = \json_decode($input, true) and is_array($isJson)) {
// ! Decode each json field recursive.
\array_walk_recursive($isJson, function (string|array &$item) {
if (\is_string($item)) {
$item = clean($item);
$item = ToolboxHelper::stripCkEditorSchemesOnMediaInputs($item);
}
});
} else {
$input = clean($input);
$input = ToolboxHelper::stripCkEditorSchemesOnMediaInputs($input);
}
} else {
$input = strip_tags($input);
}
};
\array_walk($inputs, $cleanInput);
$request->merge($inputs);
return $next($request);
}
}

View file

@ -2,7 +2,6 @@
namespace App\Http\Middleware; namespace App\Http\Middleware;
use App\Lib\Helper;
use Illuminate\Http\Request; use Illuminate\Http\Request;
class JsonOnly class JsonOnly
@ -14,11 +13,9 @@ class JsonOnly
* @param \Closure $next * @param \Closure $next
* @return mixed * @return mixed
*/ */
public function handle(Request $request, \Closure $next) public function handle(Request $request, \Closure $next): mixed
{ {
// TODO remove condition v1 check when dropped.
if ( if (
!Helper::isRequestForApiV1($request) and
(!$request->isJson() or (!$request->isJson() or
!$request->expectsJson() or !$request->expectsJson() or
is_null(json_decode($request->getContent()))) is_null(json_decode($request->getContent())))

View file

@ -0,0 +1,28 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class LogoutUnpublishedUsers
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle(Request $request, Closure $next)
{
/** @var \App\Models\User|null $userModel */
$userModel = Auth::user();
if (!is_null($userModel) && !$userModel->published) {
Auth::logout();
return redirect()->route('bo.login')->with('warning', trans('auth.unpublished_user'));
}
return $next($request);
}
}

View file

@ -9,7 +9,7 @@ class PreventRequestsDuringMaintenance extends Middleware
/** /**
* The URIs that should be reachable while maintenance mode is enabled. * The URIs that should be reachable while maintenance mode is enabled.
* *
* @var array * @var array<int, string>
*/ */
protected $except = []; protected $except = [];
} }

View file

@ -2,31 +2,39 @@
namespace App\Http\Middleware; namespace App\Http\Middleware;
use App\Providers\RouteServiceProvider;
use Closure;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth; use Symfony\Component\HttpFoundation\Response;
/**
* @codeCoverageIgnore
*/
class RedirectIfAuthenticated class RedirectIfAuthenticated
{ {
/** /**
* Handle an incoming request. * Handle an incoming request.
* *
* @param \Illuminate\Http\Request $request * @param \Illuminate\Http\Request $request
* @param \Closure $next * @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
* @param string|null ...$guards * @param string|null ...$guards
* @return mixed * @return \Symfony\Component\HttpFoundation\Response
* @throws \RuntimeException If guard is unhandled.
* @phpcs:disable Squiz.Commenting.FunctionComment.IncorrectTypeHint
*/ */
public function handle(Request $request, Closure $next, ...$guards) public function handle(Request $request, \Closure $next, ...$guards): Response
{ {
// phpcs:enable phpcs:disable
$guards = empty($guards) ? [null] : $guards; $guards = empty($guards) ? [null] : $guards;
foreach ($guards as $guard) { foreach ($guards as $guard) {
if (Auth::guard($guard)->check()) { if (auth($guard)->check()) {
return redirect(RouteServiceProvider::HOME); switch ($guard) {
case 'frontend':
return \redirect()->route('fo.homepage');
case 'backend':
if (!$request->routeIs('bo.*')) {
continue;
}
return \redirect()->route('bo.dashboard');
default:
throw new \RuntimeException("Unhandled guard `{$guard}` .");
}
} }
} }

Some files were not shown because too many files have changed in this diff Show more