Compare commits

..

475 commits

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
Alexis Gousseau
7b5cd4c4e8 fix cascade delete customer_answer 2023-06-23 13:48:33 +02:00
Alexis Gousseau
c6c2dec647 fix force https always 2023-06-23 13:30:45 +02:00
Alexis Gousseau
97e64f3180 fix search belongs to many dropdown 2023-06-23 13:30:17 +02:00
Alexis Gousseau
d206e3bf55 fix request brands customer count 2023-06-23 13:29:03 +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
Alexis Gousseau
fc26fe3f5a add policies on model actions 2023-06-20 17:37:15 +02:00
Alexis Gousseau
034b8d0b9d fix tests with policies 2023-06-20 11:55:52 +02:00
Alexis Gousseau
9613acf437 fix tests stats 2023-06-20 10:55:01 +02:00
Alexis Gousseau
321d15fc97 fix tests stats 2023-06-20 10:53:38 +02:00
Alexis Gousseau
619260a9ed fix construct service model 2023-06-19 13:01:13 +02:00
Alexis Gousseau
e3d155394d fix word-counter input/textarea style 2023-06-19 11:01:30 +02:00
Alexis Gousseau
cc716cff12 remove crud policies + clean project 2023-06-16 12:15:03 +02:00
Alexis Gousseau
6479a67096 update stats section 2023-06-16 11:14:20 +02:00
Alexis Gousseau
886f5a5186 mini fix v3 2023-06-16 10:59:28 +02:00
Alexis Gousseau
bcc23b1130 update export pages 2023-06-15 15:31:56 +02:00
Alexis Gousseau
a2a6f6a19b update CRUD singleoptin 2023-06-15 14:40:51 +02:00
Alexis Gousseau
02a6f37a9c mini fix v2 2023-06-15 13:21:32 +02:00
Alexis Gousseau
49f3d8b8b7 remove crud-policies scriptConstants 2023-06-15 13:17:51 +02:00
Alexis Gousseau
9294e8c7dc add searchfields on search-bar-brand 2023-06-15 13:16:51 +02:00
Alexis Gousseau
6934918cd4 update question request translations 2023-06-15 13:15:26 +02:00
Alexis Gousseau
bf87c0162e update CRUD service 2023-06-15 13:07:52 +02:00
Alexis Gousseau
2a55273598 update CRUD answer 2023-06-15 12:30:56 +02:00
Alexis Gousseau
05b4f83009 update CRUD question 2023-06-15 12:28:05 +02:00
Alexis Gousseau
1f7455de5a update CRUD brand v2 2023-06-15 12:24:40 +02:00
Alexis Gousseau
d7fb859caf add custom scss 2023-06-15 12:18:33 +02:00
Alexis Gousseau
9c7c41adc3 update ts modules 2023-06-15 12:16:44 +02:00
Alexis Gousseau
7449cd6e59 add translations 2023-06-15 12:15:19 +02:00
Alexis Gousseau
3a82230e9c update CRUD customer 2023-06-13 11:11:53 +02:00
Alexis Gousseau
ff292c6f26 update CRUD brand 2023-06-13 11:05:22 +02:00
Alexis Gousseau
ad5becf181 upgrade project sass/ts/stylelint/eslint/vue 2023-06-12 17:46:18 +02:00
Alexis Gousseau
0aab2213ae update project with laravel base project 2023-06-12 15:31:55 +02:00
Alexis Gousseau
39729a02ef mini fix 2023-06-12 15:26:11 +02:00
Vincent BRIDIER
f7f4b4271a Merge branch 'ticket1149' into 'develop'
Fix question crud + Fix max questions request 3 months

See merge request service-web-lab/crm!2
2022-12-06 12:03:19 +00:00
Jérémy Munsch
e18b685738
Fix max question request 3 months 2022-12-01 18:13:25 +01:00
Jérémy Munsch
84e7e95a24
bump composer crud policies 2022-12-01 17:35:58 +01:00
Jérémy Munsch
39e71e1965
Fix question crud
- remover answer on create form
- add brand_id onto fillable
- readonly on brand (dont change brand after question create)
- add cascade delete on question

You should create questions the add answers
2022-12-01 17:35:21 +01:00
Jérémy Munsch
febae0f282
Merge branch 'hotfix' into develop 2022-08-31 10:28:25 +02:00
Jérémy Munsch
c5182b65b4
Merge branch 'hotfix' 2022-08-31 10:28:07 +02:00
Jérémy Munsch
a9e4b7cde3
ArcadiaController V1 fix for game xml not including code_on_pack 2022-08-31 10:26:00 +02:00
Jérémy Munsch
b6ac60f6e5
Merge branch 'trunk' 2022-08-29 11:05:48 +02:00
Jérémy Munsch
de2525db68
Merge branch 'develop' into trunk 2022-08-29 11:05:13 +02:00
Vincent BRIDIER
ed1aa3fda5 Merge branch 'fixJobDeleteCustomers' into 'develop'
Fix Job delete unconfirmed users

See merge request service-web-lab/crm!1
2022-08-29 09:03:54 +00:00
Jérémy Munsch
608e24f2f2
Fix Job delete unconfirmed users 2022-08-29 10:55:54 +02:00
Jérémy Munsch
678f5b9d04
Merge branch 'hotfix' into develop 2022-08-24 15:12:30 +02:00
Jérémy Munsch
1cc7bf5098
Merge branch 'hotfix' 2022-08-24 15:12:21 +02:00
Jérémy Munsch
9fe08103f4
Fix ArcadiaMail Requests on api Allow phone to be empty string 2022-08-24 15:12:09 +02:00
Jérémy Munsch
030d26ca9b
Merge branch 'hotfix' into develop 2022-08-24 14:46:46 +02:00
Jérémy Munsch
62786745d2
Merge branch 'hotfix' 2022-08-24 14:46:39 +02:00
Jérémy Munsch
a312773099
Http/Kernel.php: Move ConvertEmptyStringsToNull from global middleware to web middleware 2022-08-24 14:46:06 +02:00
Jérémy Munsch
6894ded382
Merge branch 'trunk' 2022-08-24 12:28:28 +02:00
Jérémy Munsch
ef33e53054
Merge branch 'develop' into trunk 2022-08-24 12:17:46 +02:00
Jérémy Munsch
8c2516362f
Release 3.0.5 + Bumped composer deps
Update test coverage

update submodules client
2022-08-24 12:10:04 +02:00
Jérémy Munsch
d245f92b4d
Merge remote-tracking branch 'origin/ticket-997_sh01' into develop 2022-08-24 11:27:48 +02:00
Jérémy Munsch
e9b077e9ea
Merge remote-tracking branch 'origin/ticket918' into develop 2022-08-24 11:27:21 +02:00
Jérémy Munsch
d62f33fccd
Merge remote-tracking branch 'origin/ticket905' into develop 2022-08-24 11:26:52 +02:00
Sebastien Hattry
1f5ecd45ce Invert lastname and firstname in header customer list 2022-08-02 15:44:21 +02:00
Sebastien Hattry
8ae2379721 Audit fix 2022-08-02 14:08:55 +02:00
Sebastien Hattry
18774985ac Merge branch 'develop' of https://voight001.lesidc.com/crm into develop 2022-08-02 14:03:54 +02:00
Jérémy Munsch
f43d928e5e
Merge branch 'develop' into trunk 2022-06-17 16:39:15 +02:00
Jérémy Munsch
23468d0cb7
bump client and version to 3.0.3 2022-06-17 16:38:50 +02:00
Jérémy Munsch
6ceee32368
SendArchadiaGameMailRequest: Limit code on pack to 50 chars 2022-06-17 14:08:08 +02:00
Jérémy Munsch
72cfa5234c
Update test coverage 2022-06-09 11:30:49 +02:00
Jérémy Munsch
d182924371
update v2 client git version 2022-06-09 11:26:42 +02:00
Jérémy Munsch
2c3e6972f1
bump composer deps 2022-06-09 11:26:30 +02:00
Jérémy Munsch
2662eab4f1
bump composer version 2022-06-09 11:25:45 +02:00
Jérémy Munsch
f2371d97ea
Fixed #905
- handled Country and Phone on customer store and update
- refactorize Country and Phone
2022-06-09 11:09:11 +02:00
Jérémy Munsch
c3574291d6
Fix composer json project name 2022-06-09 10:35:32 +02:00
Sebastien Hattry
7eb5a08c3c npm audit fix 2022-06-02 17:26:27 +02:00
Jérémy Munsch
dda767f7b4
Merge branch 'trunk' 2022-03-02 10:21:48 +01:00
Jérémy Munsch
a6174c4bce 3.0.2 Release to Maitre coq
-----BEGIN PGP SIGNATURE-----
 
 iQGzBAABCgAdFiEEbXCAeh4tnJbE5ao1VRlr7kRfYEkFAmIWDcUACgkQVRlr7kRf
 YElyRQv+OPmTeQWhobAaNy40hiVC/1Ltx9Nf29tlPq7XRVPDqfRSB96YKtwLGDDh
 30kWdWaZifGCZIbk2ZPDuKB1lGpd462mwe+J547Di4phk8u6qu3DHhZ9JjIDsEnA
 uQUkajMOaHXbge0ZEXMiIhkIID9PNZLN78xgXD2woNLdgSn+pW+xAJhj8e2A4Aic
 H32LLHYVd1OZL8thfIArjkc/TmN4ZGShNxN/WB3Fu/Wcb1rOR6xap5V/x0Qwftm5
 y1LNa97f/Ne7Rk+IJ3T2aLGAuMkJQAf6GHW9ho8Rv+KcUtBEcr3FEaXb2smv11oa
 KlP7hkhU67KSUahunF9ebaPJlUDwSJCSwaAU2nk/jTmz/jEcB81mhU/ZYMNeXRJx
 5a+J1XttET0scRHmHp7zHYceMTisCj9jemXU7oEFjN9hrRUPSik/2GDI7I3U1Zjx
 ffOLBF6jnaI779T5/8uwzbQcVovSz8+TwEro8+S3UxxZkJ/sQ1LMjTyQ9kE429H4
 C0TG9MgQ
 =Y0eR
 -----END PGP SIGNATURE-----
gpgsig -----BEGIN PGP SIGNATURE-----
 
 iQGzBAABCgAdFiEEbXCAeh4tnJbE5ao1VRlr7kRfYEkFAmIWDeQACgkQVRlr7kRf
 YEnJSQv+NhEBiqpheBDfDvYscMjWoNLe07r+AUczNn58fWQpdGgsXEvMdP8x9pqj
 R4pPd3Yhf5ukfjaq7mJhrtqcw1dOSoniNKP1NiXnhlJZA3LuiEaXbZl3hQN3pzbi
 WY4+63GnBvrwXlAa3HDwTVBi/nXnZVyEVY1voLv3h++PXEFtdZDSCxChF06y/Trk
 5U12jv/XbL33RwXeIzjIEAQI5z/aK9ukj2QExF+AFIdukwfdAWjBElWqGIcO1QGH
 GBKlX1Uv8OrgdXW0MobPBiRsKq/myLeCtCyRGE6djKR9cOVOkuYsasciJbqG2nVP
 SlpqjV9wUm1RpSqya814aLXcz3sOkBlT6OPqmlfJ8zX0hrVkx1JJOfrAwlhRYu99
 alGIOVE6yFH46rJUpsCE2kokF5M1Scc6JiqQskFNGn7wApj68BPgMdMzGkA8hv+p
 8mFUA4jfGouEByFHPjmZJ+7pNd1NIpZYHTfWnDbXZZdk342+1eXGBnSymrbUlJc8
 4gVXuyRI
 =fq59
 -----END PGP SIGNATURE-----

Merge tag '3.0.2' into trunk

3.0.2  Release to Maitre coq
2022-02-23 11:35:15 +01:00
Jérémy Munsch
87b4007339
Merge branch 'develop' into trunk 2022-02-10 11:28:00 +01:00
Jérémy Munsch
a6731e98ae 3.0.0 release
-----BEGIN PGP SIGNATURE-----
 
 iQGzBAABCgAdFiEEbXCAeh4tnJbE5ao1VRlr7kRfYEkFAmHWzmEACgkQVRlr7kRf
 YEnOawv/dh5IkA3miThsXr+ALWqVipe2qack2OhZ2I5w8GkuzOhXtaMo6eGIFwjV
 vOtLISsQnCPlcZMw/HY3gVc3EWF2MKku7NgMMnsWi85Gi5jI5LlXygwhx0eZkC4O
 6Fc1SACyaSl8zxgiGpgwLqTTQcloWt2qJ1UdA4v6bD5y8ubcZ18vsgfxsnpfdmvN
 P0h6yGej/KeQtwb0d9UdgdBzU7dcI0s75Oy2NurgPMN2NYHBOUcfF/en8rrBb6Sy
 IrPWquDWyBTntPzLWhOpqllm/oRvBYjNTacxH4fqEWdHWPy21MwHsClkkbteNWyN
 cSGEnJuknh9GV5TPjG8YjKTlYnIrvBiPh6odMmOQDSS0NR19Tah30P0sXfj5JTX2
 Phi+8ktyyMdS2xBKU+8e6hgYuDWWJN7z/yFLdBdow4DBV+aZn6M++q7xN+AdWkML
 0ICaYjuZYVN1k7a+dt5/YvRfqS1y3zvfi8UmKBdvGz/NQIfH5nJod7/BEUW/IbAp
 Erm4+3ft
 =oP5n
 -----END PGP SIGNATURE-----
gpgsig -----BEGIN PGP SIGNATURE-----
 
 iQGzBAABCgAdFiEEbXCAeh4tnJbE5ao1VRlr7kRfYEkFAmHWznkACgkQVRlr7kRf
 YEkI/wwAhEqDJ2ohlg7ral3KrGKRiYfrQ3FxWdG4WF7c2yBPEIEUfhl7RtomKnQ/
 rrMXsbKFEDs2BnfDXoZth4KYLiWjgXGCtcTHZTRBQ/LJTaeAnfXzuPUtiaoHj003
 8sKmW8tMTPSbHEtZ6f0zZVqQGFuZvzBSbDhLd25m/H8pZgYrCGsltsOBFEgCRf1v
 wbStHyWWgw2HCGfZ8TCl5GGRDVUIKC0KV0N4X3cqW4TmrGdm5e8N8WEzT/hpcYnF
 p4/DUexa3R4QIc6N0MaEohYeslmtKycRzNloAu+omlCzmOz2mvd4stRfoOPwAAxt
 B0KH6vVO8hULhGFSf2+/zffHcnIuqVRgjdE8d5AszgVQaJJF4tTPzQSVslwmobZJ
 JDN+KEWfYE9vY1OGiuYt1wBkF3IURDCZ1mRQ3Fq4uYSQchj+G27V/QKrPfAHF3Wu
 sfl035HlWgIv3Zd/QoAO1hil8QX2tMy6O0fzdBysK7KAYNri/SEgSCK39gRfjl2D
 EoRuomau
 =NcUP
 -----END PGP SIGNATURE-----

Merge tag '3.0.0' into trunk

3.0.0 release
2022-01-06 12:11:52 +01:00
Jérémy Munsch
e48c0c6661
Merge branch 'trunk' 2021-12-03 14:36:35 +01:00
Jérémy Munsch
e6ffb668da
Merge branch 'develop' into trunk 2021-12-03 14:36:24 +01:00
Jérémy Munsch
4c2acc1493
Merge branch 'trunk' 2021-11-15 11:35:25 +01:00
Jérémy Munsch
6495977d6d
Merge branch 'develop' into trunk 2021-11-15 11:35:16 +01:00
Jérémy Munsch
1650de20a7
Merge branch 'trunk' 2021-11-12 17:03:44 +01:00
Jérémy Munsch
99af7a9460
Merge branch 'develop' into trunk 2021-11-12 17:03:37 +01:00
Jérémy Munsch
394cd081c4
Merge branch 'trunk' 2021-11-10 09:24:20 +01:00
Jérémy Munsch
9be8a44be2
Merge branch 'develop' into trunk 2021-11-10 09:14:07 +01:00
Jérémy Munsch
7856444635
Merge branch 'trunk' 2021-11-08 16:45:54 +01:00
Jérémy Munsch
36b295add7
Merge branch 'develop' into trunk 2021-11-08 16:45:46 +01:00
Jérémy Munsch
6deddae3aa
Merge branch 'trunk' 2021-11-08 16:03:20 +01:00
Jérémy Munsch
60df8ca280
Merge branch 'develop' into trunk 2021-11-08 16:02:16 +01:00
Jérémy Munsch
ed0e314a6c
Merge branch 'trunk' 2021-11-08 15:02:42 +01:00
Jérémy Munsch
91a36d6c1a
Merge branch 'develop' into trunk 2021-11-08 15:02:30 +01:00
Jérémy Munsch
a5694ba6ba
Merge branch 'trunk' 2021-11-08 08:56:21 +01:00
Jérémy Munsch
4a90b4bc22
Merge branch 'develop' into trunk 2021-11-08 08:55:46 +01:00
Jérémy Munsch
fec5e856b6
Merge branch 'trunk' 2021-11-05 15:13:10 +01:00
Jérémy Munsch
da63ec11c7
Merge branch 'develop' into trunk 2021-11-05 15:13:00 +01:00
Jérémy Munsch
eb0039653d
Merge branch 'trunk' 2021-10-26 16:57:06 +02:00
Jérémy Munsch
7f2575b165
Merge branch 'develop' into trunk 2021-10-26 16:57:00 +02:00
Jérémy Munsch
6eab2196fd
Merge branch 'trunk' 2021-10-26 16:02:08 +02:00
Jérémy Munsch
bb7a348bb7
Merge branch 'develop' into trunk 2021-10-26 16:01:57 +02:00
Jérémy Munsch
02cf4caa99
Merge branch 'trunk' 2021-10-26 12:52:41 +02:00
Jérémy Munsch
fbb0c638e2 2.2.3
-----BEGIN PGP SIGNATURE-----
 
 iQGzBAABCgAdFiEEbXCAeh4tnJbE5ao1VRlr7kRfYEkFAmF33d8ACgkQVRlr7kRf
 YEmS6QwAoVabtQydK11Xkm0rzy8qrOv2Lf9g0CSn53i1RpTG+o6O+5NK3XHImJxM
 A0roDZl9CmcikzsbmJ8usCyKYB+ti7gpLXoRfS4/y2q/KmTVfTTV7Zd1ticq8C5T
 9PrwyZUK6l6lNHFLA6VV2Aix6MEo3bwBhoSqMtYrvgHxFRYujiJIsauvAPVBi86v
 Rpizf6yzrBFHug9G+MBgm/iBRckAmlSKr9LWWNT/VGht6aeRZ15kY0wgxMRU39ZV
 qBxzL5Bsg8vUUcOvtIhEp/2L7LkpYaVs+z5U6s57htrYRXYOmSRT+Zfv4yMNDZA1
 a+2MHHtG7UR7jDSEnoK6EK/eQ6nEbmGiCLEFZM9QKENpRwVvXRUi1Z1oFtoX9UUv
 lEPuXbuAD2n5c8QCE5ynpSCZY5Mqus8DWBsTkKmJJakoMtDkIyC7kf4kpxf9i9vH
 4OydTFtlC+w2nreG9zL0pxLXdb9AgWV2wBGKJFChEgI7+UPcnbicgl02Gcoc87Ls
 nRVN5JkL
 =Ltz8
 -----END PGP SIGNATURE-----
gpgsig -----BEGIN PGP SIGNATURE-----
 
 iQGzBAABCgAdFiEEbXCAeh4tnJbE5ao1VRlr7kRfYEkFAmF33e8ACgkQVRlr7kRf
 YEmZawwApaSnfnXo42CiOLFbMI+78pIpYYdGqy/OvMRaDAYWUvrVoEydTofiqBUj
 KWwNsU33qJUqAJNJGk1eg9xRvNwRoWeuhmJfuM+2W46QkC2sjYAkOKvxaPsh8Jy0
 GqYoujM6lWlitSt52vPOqTeVdG/V/AWn+uyVNYGhX9TrOBr1ZxcNQlrg7S3AA+On
 eS1DWl7okPunhQg9rRIqT4T/q1fFKNh4vVxtyP41AitwVFrPwc/nwYnbM8uMzkVr
 JNepkow+LIgaG61uKXXzInaMphhjc1EECZ1ZGGuc+dYQDCD+LgkwAlw0by4+p9fU
 xGEDlfEtdEWe60xslnp7GCcQJEBNmqTrugXDXP5iK47MFpfsWUkgCZCsYIs3hKLS
 NRoFQC4R+TqWYYfBj5mUdVfLesR+IIT2Uhz41Cn2E4sHUvY8CGfTgAIZDCSdix/T
 Tr2sl/MxfD+voEFpKcNwd9fX5xg3FbQ+Hx8OVtjt/vXEKBXMt5bn8s5K2qxUH/n7
 IyuXi+OE
 =hp06
 -----END PGP SIGNATURE-----

Merge tag '2.2.3' into trunk

2.2.3
2021-10-26 12:52:31 +02:00
Jérémy Munsch
362ab9a612
Merge branch 'trunk' 2021-10-21 17:12:14 +02:00
Jérémy Munsch
d015f80bd2
Merge branch 'develop' into trunk 2021-10-21 17:11:56 +02:00
Jérémy Munsch
fb6531cc37
Merge branch 'trunk' 2021-10-21 14:24:40 +02:00
Jérémy Munsch
d21f488869
Merge branch 'develop' into trunk 2021-10-21 14:24:32 +02:00
Jérémy Munsch
8b8ada00e3
Merge branch 'trunk' 2021-10-20 15:31:47 +02:00
Jérémy Munsch
1d75bcd18a
Merge branch 'develop' into trunk 2021-10-20 15:31:33 +02:00
Jérémy Munsch
e05f56642a
Merge branch 'trunk' 2021-10-18 09:05:33 +02:00
Jérémy Munsch
f95e3d47c0
Merge branch 'develop' into trunk 2021-10-18 09:05:20 +02:00
Jérémy Munsch
034e10891a
Merge branch 'trunk' 2021-10-15 08:06:12 +02:00
Jérémy Munsch
5661c7514e
Merge branch 'develop' into trunk 2021-10-15 08:06:04 +02:00
Jérémy Munsch
7da3f48ece
Merge branch 'trunk' 2021-09-21 16:21:52 +02:00
Jérémy Munsch
6b6f441064
Merge branch 'develop' into trunk 2021-09-21 14:54:55 +02:00
Jérémy Munsch
a70539f8ff 2.1.8
-----BEGIN PGP SIGNATURE-----
 
 iQGzBAABCgAdFiEEbXCAeh4tnJbE5ao1VRlr7kRfYEkFAmFJr0kACgkQVRlr7kRf
 YEn2ZQv/Vi8bTSKUORF+8UxG6ZlduwW3TA+QNU3Bj/mW134wB0Kr5BBZ0KSqkUiu
 JIMwfF6JtLzma33MXMR4t1IfOavD9R6keg45owPlUn1FEz+xCVKVLc0Hh109vp3f
 yX2FiFdBBqio7pLqdbfrIx93VMkqaarsiW2coGwNVERH3FxoB4QMhtnXnVGoXKJI
 QzzIROb9hZUE+DReLP19gYIllJ3HF3HIwjCLPUp9KKDfx2UGUQRxnCSVhIKceSPr
 lQBPkBxtxZGZQTRGvj/lZe4sOkQEfcguqXVTOyCr1LGjPmLSwluVr3ceUQDUA7kg
 x1yrdh+YAUKsIFI/zczmMPJfc0dxPHJi0dqcWPdNDADvKKXaXRkld5pFLCufk886
 vIcXlvcGRasn2/pA83CEqO4kRQ8Hm9x9FjhBicy8JxwYJmoJI1E9FD5xGMWQrrEb
 T4YC4/rEmCIbxiJnXE6GGEwiGysq15QPDyLdFqIys4VPM027KhI9x7Ye278ITml8
 8+OPi2/A
 =BvV5
 -----END PGP SIGNATURE-----
gpgsig -----BEGIN PGP SIGNATURE-----
 
 iQGzBAABCgAdFiEEbXCAeh4tnJbE5ao1VRlr7kRfYEkFAmFJr28ACgkQVRlr7kRf
 YEmYAQv/Y5W3p4Mt4zzSjMmPu9YJTNUdkPUqCpvlx87hN5xN/Ed6I8XCl+PXDMv8
 nNlGX4pvNatFFTr9LN2cKfqLfqxmdgEbxzEHpQoUCG1L9DNHKPhIi5td6aiQO6HK
 7b6uDWjAnE9RTjh+v151xJrQk4F3ifRGslXQjCEGZ/TUCwjN+fM3jbHlkMpCfFXq
 /WiteEHlttRyUfyscyXsqiCg6GOlpXiIBnPO9lL5kKXkG8z9TSTixke/c/TqqBTj
 fbcP+luk6W8ZPvm3448rvGEtoFiEamTQdusET0AnORce7nS4Mc6shmM9kCDyGkzg
 EtpeC+daUq/oDBouOpVOg9LvGU0VvcTQH7PdjB9f3HF0xHUR1cihvNrFgeQGD1g+
 nErxHQFrHhZz57S8wh94HamqtHkRnUDYbnK9kHKOklbKaO488yJpe4TO1vx7IDMD
 pJkO7fdbvxGAWAhQuCRhESW5l91HN+3mGOulyg8rV3ER9X/z8dQLVhP/ShW3K4bR
 v0/VD9xH
 =EEkH
 -----END PGP SIGNATURE-----

Merge tag '2.1.8' into trunk

2.1.8
2021-09-21 12:09:51 +02:00
Jérémy Munsch
2785c7cbcb Merge branch 'trunk' 2021-09-15 12:05:14 +02:00
Jérémy Munsch
a1ddc45707 Merge branch 'develop' into trunk 2021-09-15 12:05:02 +02:00
Jérémy Munsch
ef74e5a191 Merge branch 'trunk' 2021-09-15 10:57:10 +02:00
Jérémy Munsch
d09fe34f45 Merge branch 'develop' into trunk 2021-09-15 10:56:59 +02:00
Jérémy Munsch
8030b619e7 Merge branch 'trunk' 2021-09-15 10:31:00 +02:00
Jérémy Munsch
b3976e462e Merge branch 'develop' into trunk 2021-09-15 10:30:26 +02:00
Jérémy Munsch
0092d0b007 Merge branch 'trunk' 2021-09-14 12:13:02 +02:00
Jérémy Munsch
8603f2e0f6 Merge branch 'develop' into trunk 2021-09-14 12:12:44 +02:00
Jérémy Munsch
bf98cfc545 Merge branch 'trunk' 2021-09-14 11:48:44 +02:00
Jérémy Munsch
346b880491 Merge branch 'develop' into trunk 2021-09-14 11:20:25 +02:00
Jérémy Munsch
3ff0175763 Merge branch 'develop' into trunk 2021-09-10 16:31:30 +02:00
Jérémy Munsch
6b1e234f7b Merge branch 'develop' into trunk 2021-09-09 16:29:47 +02:00
Jérémy Munsch
5240f25ab2 Merge branch 'develop' into trunk 2021-09-09 16:08:56 +02:00
Jérémy Munsch
e3cb5e13cd Merge branch 'develop' 2021-08-26 15:00:43 +02:00
Jérémy Munsch
71b2cf14c3 Merge branch 'develop' into trunk 2021-08-26 15:00:29 +02:00
Jérémy Munsch
184bbb094b Merge branch 'trunk' 2021-08-25 17:32:08 +02:00
Jérémy Munsch
8b9ca7c3c3 Merge branch 'develop' into trunk 2021-08-25 17:31:58 +02:00
Jérémy Munsch
d599757e28 Merge branch 'trunk' 2021-08-25 11:24:28 +02:00
Jérémy Munsch
aed9c9ef9b Merge branch 'develop' into trunk 2021-08-25 11:24:15 +02:00
Jérémy Munsch
3d3edf50fa Merge branch 'develop' into trunk 2021-08-25 09:24:24 +02:00
Jérémy Munsch
e31898bb7b Merge branch 'develop' into trunk 2021-08-24 16:05:23 +02:00
Jérémy Munsch
cea868b2e4 Merge branch 'develop' into trunk 2021-08-24 12:46:35 +02:00
Jérémy Munsch
17115d1c24 Merge tag '2.1.1' into trunk 2021-08-24 08:56:50 +02:00
Vincent BRIDIER
c468dd5c52 Merge branch 'trunk' 2021-08-02 17:23:27 +02:00
Vincent BRIDIER
e1d72fdc59 Merge branch 'develop' into trunk 2021-08-02 17:23:13 +02:00
Vincent BRIDIER
6731124779 Merge branch 'trunk' 2021-06-23 10:21:48 +02:00
Vincent BRIDIER
55ca47938f Merge branch 'develop' into trunk 2021-06-23 10:21:24 +02:00
Vincent BRIDIER
433b0479da Merge branch 'trunk' 2021-06-23 10:17:47 +02:00
Vincent BRIDIER
90f328cf61 Merge branch 'develop' into trunk 2021-06-23 10:17:23 +02:00
Jérémy Munsch
7b56aebeae Merge branch 'trunk' 2021-05-20 09:54:08 +02:00
Jérémy Munsch
c12e9e8752 Merge branch 'develop' into trunk 2021-05-20 09:53:38 +02:00
1104 changed files with 182182 additions and 123561 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
end_of_line = lf
insert_final_newline = true
indent_style = space
indent_size = 4
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
[*.md]
@ -13,3 +13,9 @@ trim_trailing_whitespace = false
[*.{yml,yaml}]
indent_size = 2
[docker-compose.yml]
indent_size = 4
[*.vue]
indent_size = 2

View file

@ -14,18 +14,9 @@ DB_DATABASE=laravel
DB_USERNAME=root
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
CACHE_DRIVER=file
QUEUE_CONNECTION=sync
QUEUE_CONNECTION=database
SESSION_DRIVER=file
SESSION_LIFETIME=120
@ -34,23 +25,27 @@ REDIS_PASSWORD=null
REDIS_PORT=6379
MAIL_MAILER=smtp
MAIL_HOST=smtp.mailtrap.io
MAIL_PORT=2525
MAIL_HOST=dev-mailpit
MAIL_PORT=1025
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null
MAIL_FROM_ADDRESS=null
MAIL_FROM_NAME="${APP_NAME}"
MAIL_DIGITAL_SERVICE=""
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=
# Sentry
SENTRY_LARAVEL_DSN=
PUSHER_APP_ID=
PUSHER_APP_KEY=
PUSHER_APP_SECRET=
PUSHER_APP_CLUSTER=mt1
# UWIL ADMIN SECURE
# - LIST SEPARATED WITH ',' (Required)
UWIL_ADMIN_SECURE_CIDR_LIST=""
# - LIST SEPARATED WITH ',' (Optional)
UWIL_ADMIN_SECURE_CIDR_PROJECT_ALLOWED_LIST=""
MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
# SAML
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_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
CACHE_DRIVER=file
QUEUE_CONNECTION=database

55
.eslintrc.json Normal file
View file

@ -0,0 +1,55 @@
{
"env": {
"browser": true,
"es2021": true
},
"extends": [
"eslint:recommended",
"plugin:vue/vue3-strongly-recommended",
"@vue/eslint-config-typescript"
],
"overrides": [
{
"files": ["*.ts", "*.vue"],
"rules": {
"no-undef": "off"
}
},
{
"files": ["*.vue"],
"rules": {
"indent": "off"
}
}
],
"parser": "vue-eslint-parser",
"parserOptions": {
"parser": "@typescript-eslint/parser",
"ecmaVersion": "latest",
"sourceType": "module"
},
"plugins": ["vue", "@typescript-eslint"],
"ignorePatterns": ["webpack.mix.js", "**/vendor/*.js", "**/vendor/**/*.js"],
"rules": {
"indent": ["error", 4, { "SwitchCase": 1 }],
"linebreak-style": ["error", "unix"],
"quotes": ["error", "double"],
"semi": ["error", "always"],
"vue/script-indent": ["error", 2, { "baseIndent": 0, "switchCase": 1 }],
"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
*.css linguist-vendored
*.scss linguist-vendored
*.js linguist-vendored
* text=auto eol=lf
*.blade.php diff=html
*.css diff=css
*.html diff=html
*.md diff=markdown
*.php diff=php
/.github export-ignore
CHANGELOG.md export-ignore
.styleci.yml export-ignore

35
.gitignore vendored
View file

@ -1,20 +1,31 @@
/node_modules
/public/hot
/public/storage
/storage/*.key
/vendor
.vscode/
.idea/
.env
.env.testing
.env.backup
.env.production
.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.yaml
npm-debug.log
yarn-error.log
storage/crm_migration.csv
MIG/errors_sql.txt
public/js
public/css
public/assets
.npmrc
.phpdoc
.zshrc
.npm/
.composer/
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

6
.gitmodules vendored
View file

@ -1,3 +1,3 @@
[submodule "clients/V2"]
path = clients/V2
url = https://voight001.lesidc.com/crm-client
[submodule "client"]
path = client
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

45
.stylelintrc.json Normal file
View file

@ -0,0 +1,45 @@
{
"extends": [
"stylelint-config-standard-scss",
"stylelint-config-sass-guidelines",
"stylelint-config-idiomatic-order"
],
"plugins": [
"stylelint-scss"
],
"files": [
"**/*.css",
"**/*.scss"
],
"ignoreFiles": [
"**/vendor/*.css",
"**/vendor/**/*.css",
"**/vendor/*.scss",
"**/vendor/**/*.scss",
"**/vendor/*.sass",
"**/vendor/**/*.sass",
"**/vendor/*.less",
"**/vendor/**/*.less"
],
"customSyntax": "postcss-scss",
"rules": {
"declaration-property-value-no-unknown": true,
"selector-max-id": null,
"color-no-invalid-hex": true,
"order/properties-alphabetical-order": null,
"max-nesting-depth": 5,
"selector-max-compound-selectors": 5,
"declaration-colon-newline-after": null,
"value-list-comma-newline-after": null,
"scss/double-slash-comment-empty-line-before": [
"always",
{
"ignore": [
"between-comments",
"stylelint-commands",
"inside-block"
]
}
]
}
}

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 :
- 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
## Tests unitaire
Config Supervisor 2 Fichiers :
### Comment lancer les tests unitaires ?
`ldccrm-worker.conf`
**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
[Suivre le wiki dédié au tests unitaires](https://lab.s-web.fr/ldc-web/projets/crm/-/wikis/Tests-unitaires)

View file

@ -2,12 +2,13 @@
namespace App\Console\Commands;
use App\Models\CustomerInfo;
use App\Models\Customer;
use App\Models\CustomerLogin;
use App\Models\CustomerOptInHistory;
use Carbon\Carbon;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
/**
* @codeCoverageIgnore
@ -34,23 +35,26 @@ class DeleteUnconfirmedCustomers extends Command
*
* @return integer
*/
public function handle()
public function handle(): int
{
/** @var \Illuminate\Database\Eloquent\Collection $customersLogins */
$customersLogins = CustomerLogin::whereStatus(false)->where('created_at', '<', Carbon::now()->subDay(1))->get();
$customersLogins = CustomerLogin::query()
->where('status', false)
->where('created_at', '<', Carbon::now()->subDay(1))
->with(['brand', 'customer'])
->get();
$customersLogins->map(function (CustomerLogin $customerLogin) {
Log::info($customerLogin->customer_id);
DB::transaction(function () use ($customerLogin) {
/** @var \App\Models\Customer $customer */
$customer = $customerLogin->customer;
// * Si non activé et créer il y a plus d'un jour : Suppression.
$customerLogin->customer->infos()->where('brand_id', $customerLogin->brand_id)
->get()->map(function (CustomerInfo $customerInfo) {
$customerInfo->deleteOrFail();
});
/** @var \App\Models\Brand */
$brand = $customerLogin->brand;
// * Suppression de ses infos de login pour la marque.
$customerLogin->deleteOrFail();
$customer->info($brand, Customer::FIRST_OR_FAIL)->deleteOrFail();
$customer->login($brand, Customer::FIRST_OR_FAIL)->deleteOrFail();
// * Si le customer n'a plus d'info il n'a donc plus besoin de compte en crm.
if (!$customer->infos()->count()) {

View file

@ -7,18 +7,20 @@ use App\Enums\ApiStatus;
use App\Enums\ArcadiaOrderType;
use App\Enums\Country;
use App\Enums\QuestionType;
use App\Exceptions\Api\V2\ApiErrorException;
use App\Exceptions\Api\V2\CrmException;
use App\Exceptions\Api\V4\ApiErrorException;
use App\Exceptions\Api\V4\CrmException;
use App\Traits\Enums\BaseEnum;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\Str;
/**
* @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';
/**
@ -33,15 +35,16 @@ class GenerateCrmClient extends Command
*
* @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.
*
* @return integer
*/
public function handle()
public function handle(): int
{
$this->copyTraits();
$this->copyEnums();
$this->copyExceptions();
$this->copyRoutesList();
@ -54,7 +57,7 @@ class GenerateCrmClient extends Command
*
* @return void
*/
private function copyApiConfigInfo()
private function copyApiConfigInfo(): void
{
$this->info('Copy Api Config');
$rootTarget = app_path('../' . self::TARGET);
@ -72,13 +75,13 @@ class GenerateCrmClient extends Command
*
* @return void
*/
private function copyRoutesList()
private function copyRoutesList(): void
{
$this->info('Copy Routes');
$routes = [];
/** @var \Illuminate\Routing\Route */
foreach (Route::getRoutes() as $route) {
if (\strpos($route->getPrefix(), 'v2') !== false) {
if (\strpos($route->getPrefix(), 'v4') !== false) {
$routes[$route->getName()] = [
'uri' => $route->uri(),
'methods' => $route->methods()
@ -104,15 +107,36 @@ class GenerateCrmClient extends Command
{
$this->info('Copy Exceptions');
$rootTarget = app_path('../' . self::TARGET);
$this->copyToFolder([
$classes = [
CrmException::class,
ApiErrorException::class
], 'Exceptions');
$this->replaceInPhpFile(
"$rootTarget/Exceptions/ApiErrorException.php",
'App\Enums\ApiError',
'LDC\LDCCRM\Enums\ApiError'
);
];
$this->copyToFolder($classes, 'Exceptions');
foreach ($classes as $class) {
$classBasename = class_basename($class);
$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
{
$this->info('Copy Enums');
$classes = [
$rootTarget = app_path('../' . self::TARGET);
$classes = [
ApiError::class,
ApiStatus::class,
ArcadiaOrderType::class,
@ -131,11 +156,10 @@ class GenerateCrmClient extends Command
QuestionType::class
];
$this->copyToFolder($classes, 'Enums');
$rootTarget = app_path('../' . self::TARGET);
foreach ($classes as $class) {
$classBasename = class_basename($class);
$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
{
$rootTarget = app_path('../' . self::TARGET);
$rootTarget = app_path('../' . self::TARGET);
$folderNamespace = \str_replace('/', '\\', $folder);
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);
$classBasename = class_basename($class);
$source = \app_path("$folder/$classFileName");
@ -175,11 +200,15 @@ class GenerateCrmClient extends Command
{
$fContent = File::get($source);
// Remove trailing \Api\V2 .
if ($pos = strpos($fContent, '\Api\V2')) {
$fContent = substr_replace($fContent, '', $pos, strlen('\Api\V2'));
// Remove trailing \Api\V4 .
if ($pos = strpos($fContent, '\Api\V4')) {
$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.
$pos = strpos($fContent, $search);
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
*/
public function handle()
public function handle(): int
{
\dispatch(new JobsStartStatJobs())->afterResponse();
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;
use App\Enums\Users\RoleEnum;
use App\Models\User;
use Carbon\Carbon;
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
{
/**
@ -16,35 +17,189 @@ class UserCreate extends Command
*
* @var string
*/
protected $signature = 'user:create';
protected $signature = 'uwil:user:create';
/**
* The console command description.
*
* @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.
*
* @return integer
* @return void
*/
public function handle()
public function handle(): void
{
$firstname = $this->ask('What is the firstname?');
$lastname = $this->ask('What is the lastname?');
$email = $this->ask('What is the email?');
$pwd = $this->secret('What is the password?');
$this->firstName = null;
$this->addFirstName();
$user = new User();
$user->firstname = $firstname;
$user->lastname = $lastname;
$user->email = $email;
$user->password = Hash::make($pwd);
$user->save();
$this->lastName = null;
$this->addLastName();
$this->info('User created.');
return 0;
$this->email = null;
$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;
use App\Console\Commands\DeleteUnconfirmedCustomers;
use App\Jobs\SendTransferredToDigitalCountMail;
use App\Jobs\StartStatJobs;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
/**
* @codeCoverageIgnore
*/
class Kernel extends ConsoleKernel
{
/**
@ -25,10 +23,13 @@ class Kernel extends ConsoleKernel
* @param \Illuminate\Console\Scheduling\Schedule $schedule
* @return void
*/
protected function schedule(Schedule $schedule)
protected function schedule(Schedule $schedule): void
{
// @codeCoverageIgnoreStart
$schedule->job(StartStatJobs::class)->dailyAt('03:30');
$schedule->job(SendTransferredToDigitalCountMail::class)->weekdays()->at('07:00');
$schedule->command(DeleteUnconfirmedCustomers::class)->dailyAt('05:30');
// @codeCoverageIgnoreEnd
}
/**
@ -36,7 +37,7 @@ class Kernel extends ConsoleKernel
*
* @return void
*/
protected function commands()
protected function commands(): void
{
$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;
use Kwaadpepper\Enum\BaseEnumRoutable;
use Uwil\LaravelCommon\Traits\Enums\BaseEnum;
/**
* 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
enum ApiError: int
{
/**
* Valeurs des possibilités de cet énumération (value)
*
* @return array
*/
public static function values(): array
{
return [
'modelNotFound' => 101,
'validationFailed' => 102,
'customerExists' => 103,
'customerAlreadyActive' => 110,
'customerNotActivated' => 116,
'authFailed' => 117,
'apiAuthFailed' => 119,
'singleOptinCustomerExists' => 120,
'methodNotAllowed' => 150,
'wrongTimeStamp' => 151,
'missingParameter' => 400,
'authorizationException' => 403,
'throttleRequestsException' => 429
];
}
use BaseEnum;
case modelNotFound = 101;
case validationFailed = 102;
case customerExists = 103;
case customerAlreadyActive = 110;
case customerNotActivated = 116;
case authFailed = 117;
case apiAuthFailed = 119;
case singleOptinCustomerExists = 120;
case methodNotAllowed = 150;
case wrongTimeStamp = 151;
case missingParameter = 400;
case authorizationException = 403;
case throttleRequestsException = 429;
/**
* Libellés des possibilités de cet énumération (label)
*
* @return array
* Optionnal labels definition
*/
public static function labels(): array
{
return [
'modelNotFound' => '%s introuvable.',
'validationFailed' => 'Impossible de valider les données',
'customerExists' => 'L\'utilisateur est déjà enregistré sur ce site',
'customerNotActivated' => 'Le compte n\'est pas activé',
'customerAlreadyActive' => 'Le compte est déjà activé',
'singleOptinCustomerExists' => 'Le single optin ne peut être ajouté car un compte existe.',
'authFailed' => 'Echec de l\'authentification du compte',
'apiAuthFailed' => 'Echec de l\'authentification client à l\'Api LDC-CRM pour ce service',
'methodNotAllowed' => 'This method is not allowed for this request',
'wrongTimeStamp' => 'Your request is malformed, please check timestamp header and parameter',
'missingParameter' => 'The request is missing some parameters',
'authorizationException' => 'You are not autorized to access this',
'throttleRequestsException' => 'Throttle: too many request have been attempted, please retry later'
];
}
private const LABELS = [
self::modelNotFound->name => '%s introuvable.',
self::validationFailed->name => 'Impossible de valider les données',
self::customerExists->name => 'L\'utilisateur est déjà enregistré sur ce site',
self::customerNotActivated->name => 'Le compte n\'est pas activé',
self::customerAlreadyActive->name => 'Le compte est déjà activé',
self::singleOptinCustomerExists->name => 'Le single optin ne peut être ajouté car un compte existe.',
self::authFailed->name => 'Echec de l\'authentification du compte',
self::apiAuthFailed->name => 'Echec de l\'authentification client à l\'Api LDC-CRM pour ce service',
self::methodNotAllowed->name => 'This method is not allowed for this request',
self::wrongTimeStamp->name => 'Your request is malformed, please check timestamp header and
parameter',
self::missingParameter->name => 'The request is missing some parameters',
self::authorizationException->name => 'You are not autorized to access this',
self::throttleRequestsException->name => 'Throttle: too many request have been attempted, please retry later'
];
}

View file

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

View file

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

View file

@ -2,27 +2,12 @@
namespace App\Enums;
use Kwaadpepper\Enum\BaseEnumRoutable;
use Uwil\LaravelCommon\Traits\Enums\BaseEnum;
/**
* @method static self cb()
* @method static self check()
*
* @package LDCCrmClient
* @subpackage enums
*/
class ArcadiaOrderType extends BaseEnumRoutable
enum ArcadiaOrderType: int
{
/**
* Valeurs des possibilités de cet énumération (value)
*
* @return array
*/
public static function values(): array
{
return [
'cb' => 0,
'check' => 1
];
}
use BaseEnum;
case cb = 0;
case 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;
use Kwaadpepper\Enum\BaseEnumRoutable;
use Uwil\LaravelCommon\Traits\Enums\BaseEnum;
/**
* @method static self singleOptin()
* @method static self customer()
* @method static self both()
*
* @codeCoverageIgnore
*/
class CustomerExportType extends BaseEnumRoutable
enum CustomerExportType: int
{
/**
* Valeurs des possibilités de cet énumération (value)
*
* @return array
*/
public static function values(): array
{
return [
'singleOptin' => 1,
'customer' => 2,
'both' => 3
];
}
use BaseEnum;
case singleOptin = 1;
case customer = 2;
case both = 3;
/**
* Libellés des possibilités de cet énumération (label)
*
* @return array
* Optionnal labels definition
*/
public static function labels(): array
{
return [
'singleOptin' => trans('Les single optins uniquement'),
'customer' => trans('Les clients uniquement acceptant de recevoir les newsletters'),
'both' => trans('La totalité des client acceptant de recevoir les newsletters')
];
}
private const LABELS = [
self::singleOptin->name => 'Les single-optins uniquement',
self::customer->name => 'Les clients uniquement acceptant de recevoir les newsletters',
self::both->name => 'La totalité des single-optins ainsi que des clients 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;
use Kwaadpepper\Enum\BaseEnumRoutable;
use Uwil\LaravelCommon\Traits\Enums\BaseEnum;
/**
* @method static self select()
* @method static self radio()
* @method static self checkbox()
*
* @package LDCCrmClient
* @subpackage enums
*/
class QuestionType extends BaseEnumRoutable
enum QuestionType: int
{
/**
* Valeurs des possibilités de cet énumération (value)
*
* @return array
*/
public static function values(): array
{
return [
'select' => 1,
'radio' => 2,
'checkbox' => 3
];
}
use BaseEnum;
case select = 1;
case radio = 2;
case 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
* referenced in \App\Enums\ApiError would
* occur within the crm
* occur within the crm.
*
* On the client an ApiErrorException would
* also be throw with the ApiError corresponding to
* the situation
* the situation.
*
* @package LDCCrmClient
* @subpackage exceptions
@ -34,7 +34,7 @@ class ApiErrorException extends Exception
/**
* This is an explicit error message,
* ou may print this to the front or not depending
* on $error value
* on $error value.
*
* @var string
*/
@ -52,10 +52,10 @@ class ApiErrorException extends Exception
/**
* Create a new exception base on ApiError enum.
*
* @param ApiError $error
* @param integer $httpCode
* @param string $message
* @param Throwable|null $previous
* @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)
{
@ -63,16 +63,16 @@ class ApiErrorException extends Exception
$this->message = $message;
$this->httpCode = $httpCode;
parent::__construct(
strlen($this->message) ? $this->message : $error->label,
$error->value,
strlen($this->message) ? $this->message : $error->label(),
$error->value(),
$previous
);
}
/**
* Get the Api Error
* Get the Api Error.
*
* @return ApiError
* @return \App\Enums\ApiError
*/
public function getApiError(): ApiError
{
@ -80,7 +80,7 @@ class ApiErrorException extends Exception
}
/**
* Get the http code
* Get the http code.
*
* @return integer
*/
@ -95,7 +95,7 @@ class ApiErrorException extends Exception
*
* @return void
*/
public function report()
public function report(): void
{
// @codeCoverageIgnoreStart
Log::debug(sprintf('CRM API: %s', $this->getMessage()));

View file

@ -12,7 +12,6 @@ use Illuminate\Support\Facades\Log;
*
* @package LDCCrmClient
* @subpackage exceptions
* @codeCoverageIgnore
*/
class CrmException extends Exception
{
@ -22,7 +21,7 @@ class CrmException extends Exception
*
* @return void
*/
public function report()
public function report(): void
{
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\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\V4\ApiResponse as V4ApiResponse;
use App\Lib\Helper;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Auth\AuthenticationException;
@ -14,9 +16,9 @@ use Illuminate\Http\Exceptions\ThrottleRequestsException;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
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 Throwable;
class Handler extends ExceptionHandler
{
@ -45,8 +47,13 @@ class Handler extends ExceptionHandler
*
* @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
* @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()) {
return $this->handleApiV2Exception($request, $e);
if ((Helper::isRequestForApiV2($request) || Helper::isRequestForApiV4($request)) and $request->isJson()) {
return $this->handleApiException($request, $e);
} //end if
// @codeCoverageIgnoreStart
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 \Throwable $e
* @return JsonResponse
* @param \Illuminate\Http\Request $request
* @param \Throwable $e
* @return \Illuminate\Http\JsonResponse
*/
private function handleApiV2Exception(Request $request, \Throwable $e): JsonResponse
private function handleApiException(Request $request, \Throwable $e): JsonResponse
{
switch (get_class($e)) {
case AuthenticationException::class:
@ -81,24 +88,24 @@ class Handler extends ExceptionHandler
return $this->getReponse(
$request,
401,
ApiError::apiAuthFailed()->value,
sprintf('%s, %s', ApiError::apiAuthFailed()->label, $e->getMessage())
ApiError::apiAuthFailed->value(),
sprintf('%s, %s', ApiError::apiAuthFailed->label(), $e->getMessage())
);
case MethodNotAllowedHttpException::class:
/** @var MethodNotAllowedHttpException $e */
return $this->getReponse(
$request,
$e->getStatusCode(),
ApiError::methodNotAllowed()->value,
sprintf('%s, %s', ApiError::methodNotAllowed()->label, $e->getMessage())
ApiError::methodNotAllowed->value(),
sprintf('%s, %s', ApiError::methodNotAllowed->label(), $e->getMessage())
);
case ValidationException::class:
/** @var ValidationException $e */
return $this->getReponse(
$request,
$e->status,
ApiError::validationFailed()->value,
sprintf(ApiError::validationFailed()->label, $e->getMessage()),
ApiError::validationFailed->value(),
sprintf(ApiError::validationFailed->label(), $e->getMessage()),
['errors' => $e->errors()]
);
case ModelNotFoundException::class:
@ -106,14 +113,22 @@ class Handler extends ExceptionHandler
return $this->getReponse(
$request,
404,
ApiError::modelNotFound()->value,
ApiError::modelNotFound->value(),
sprintf(
ApiError::modelNotFound()->label,
ApiError::modelNotFound->label(),
\array_slice(explode('\\', $e->getModel()), -1)[0]
)
);
case ApiErrorException::class:
/** @var ApiErrorException $e */
case V4ApiErrorException::class:
/** @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(
$request,
$e->getStatusCode(),
@ -125,36 +140,36 @@ class Handler extends ExceptionHandler
return $this->getReponse(
$request,
429,
ApiError::throttleRequestsException()->value,
ApiError::throttleRequestsException()->label
ApiError::throttleRequestsException->value(),
ApiError::throttleRequestsException->label()
);
case AuthorizationException::class:
/** @var AuthorizationException $e */
return $this->getReponse(
$request,
403,
ApiError::authorizationException()->value,
ApiError::authorizationException()->label
ApiError::authorizationException->value(),
ApiError::authorizationException->label()
);
} //end switch
// @codeCoverageIgnoreStart
if (config('app.env') === 'testing') {
// 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
}
/**
* Generate an ApiV2 json response
* Generate an ApiV4 json response.
*
* @param Request $request
* @param integer $httpCode
* @param integer $apiCode
* @param string $message
* @param array $data
* @return JsonResponse
* @param \Illuminate\Http\Request $request
* @param integer $httpCode
* @param integer $apiCode
* @param string $message
* @param array $data
* @return \Illuminate\Http\JsonResponse
*/
private static function getReponse(
Request $request,
@ -163,9 +178,15 @@ class Handler extends ExceptionHandler
string $message,
array $data = []
): JsonResponse {
return (new V2ApiResponse(
(object)array_merge($data, ['message' => $message, 'code' => $apiCode]),
ApiStatus::KO()
if (Helper::isRequestForApiV2($request)) {
return (new V2ApiResponse(
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);
}
}

View file

@ -1,74 +0,0 @@
<?php
namespace App\Http\Controllers;
use App\Http\Controllers\Controller;
use App\Models\Answer;
use Illuminate\Database\Eloquent\Builder;
use Kwaadpepper\CrudPolicies\Http\Requests\CrudIndexRequest;
use Kwaadpepper\CrudPolicies\Traits\CrudController;
/**
* @codeCoverageIgnore
*/
class AnswerController extends Controller
{
use CrudController {
index as crudIndex;
}
/** @var Log */
public static $modelClass = Answer::class;
/**
* The Bo app layout
*
* @var string
*/
public static $layout = 'layouts.app';
/**
* Event index crud-policies
*
* @param Builder $query
* @return Builder
*/
public function indexQuery(Builder $query): Builder
{
$parent = request()->question;
if ($parent) {
$query->whereHas('question', function ($q) use ($parent) {
return $q->whereId($parent->id);
});
}
return $query;
}
/**
* Overload crud-policies index
*
* @param CrudIndexRequest $request
* @return \Illuminate\Http\Response
*/
public function index(CrudIndexRequest $request)
{
/** @var \Illuminate\View\View */
$view = $this->crudIndex($request);
if ($request->ajax()) {
return $view;
}
return view('answer.index', $view->getData());
}
/**
* Before store model middleware
*
* @param Answer $answer
* @return void
*/
public function storeModel(Answer &$answer)
{
// * Save relation on create
$answer->question()->associate(request()->question);
}
}

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) {
$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) {
$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\Requests\Api\V2\StoreCustomerAnswers;
use App\Models\Answer;
use App\Models\Customer;
use App\Models\Service;
use Illuminate\Http\JsonResponse;
use App\Models\ModelAnswer;
use App\Models\Question;
class AnswerController extends Controller
{
/**
* Store the customer answers
* Store the customer answers.
*
* @param StoreCustomerAnswers $request
* @param Service $service
* @param Customer $customer
* @return JsonResponse
* @param \App\Http\Requests\Api\V2\StoreCustomerAnswers $request
* @param \App\Models\Customer $customer
* @return \Illuminate\Http\JsonResponse
*/
public function storeCustomerAnswers(
StoreCustomerAnswers $request,
Service $service,
Customer $customer
): JsonResponse {
): \Illuminate\Http\JsonResponse {
// * Make sure we are accessing a customer that belongs to our service.
$this->authorize('customerBelongsToService', $customer);
$existingAnswerIds = $customer->getAnswers($service)->pluck('id');
$answersID = Answer::whereIn('id', $request->answers)->pluck('id');
$customer->answers()->sync($answersID->merge($existingAnswerIds));
$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

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

View file

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

View file

@ -14,6 +14,7 @@ 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;
@ -24,18 +25,19 @@ class CustomerController extends Controller
/**
* Creates a new customer for the specified service.
*
* @param StoreCustomerRequest $request
* @param Service $service
* @return JsonResource
* @throws ApiErrorException With ApiError::customerExists if the customer with this email already exists.
* @throws CrmException If saving the customer to DB failed.
* @param \App\Http\Requests\Api\V2\StoreCustomerRequest $request
* @param \App\Models\Service $service
* @return \Illuminate\Http\Resources\Json\JsonResource
* @throws \App\Exceptions\Api\V2\ApiErrorException With ApiError::customerExists if
* 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
{
$data = (object)$request->validated();
$data = (object) $request->validated();
/** @var \App\Models\Customer */
$customer = Customer::where('email', $data->email)->firstOrCreate([
$customer = Customer::query()->where('email', $data->email)->firstOrCreate([
'email' => $data->email
]);
@ -46,8 +48,10 @@ class CustomerController extends Controller
// * Delete Single optin if exists.
/** @var \App\Models\SingleOptin|null $singleOptin */
$singleOptin = SingleOptin::where('email', $data->email)
->where('brand_id', $service->brand->id)->first();
$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;
}
@ -55,7 +59,7 @@ class CustomerController extends Controller
/** @var \App\Models\CustomerInfo */
$customerInfo = $customer->info($service->brand, Customer::FIRST_OR_NEW);
if ($customerInfo->exists) {
throw new ApiErrorException(ApiError::customerExists());
throw new ApiErrorException(ApiError::customerExists);
}
$customerInfo->fill($request->validated());
$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 Customer $customer
* @return JsonResource
* @param \App\Models\Service $service
* @param \App\Models\Customer $customer
* @return \Illuminate\Http\Resources\Json\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 Service $service
* @param Customer $customer
* @return JsonResource
* @throws CrmException If saving the customer to DB failed.
* @param \App\Http\Requests\Api\V2\UpdateCustomerRequest $request
* @param \App\Models\Service $service
* @param \App\Models\Customer $customer
* @return \Illuminate\Http\Resources\Json\JsonResource
* @throws \App\Exceptions\Api\V2\CrmException If saving the customer to DB failed.
*/
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 Customer $customer
* @return JsonResponse
* @throws CrmException If removing the customer off the DB failed.
* @param \App\Models\Service $service
* @param \App\Models\Customer $customer
* @return \Illuminate\Http\JsonResponse
* @throws \App\Exceptions\Api\V2\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);
// TODO: Gestion avec mesBR.
/** @var \App\Models\CustomerInfo */
$customerInfo = $customer->info($service->brand, Customer::FIRST_OR_FAIL);
/** @var \App\Models\CustomerLogin */
@ -189,27 +192,30 @@ class CustomerController extends Controller
}
/**
* Authenticate the user
* @param AuthenticateRequest $request
* @param Service $service
* @return JsonResource
* @throws ApiErrorException With ApiError::authFailed() in case of wrong credentials.
* @throws ApiErrorException With ApiError::customerNotActivated() if customer account is not activated.
* Authenticate the user.
*
* @param \App\Http\Requests\Api\V2\AuthenticateRequest $request
* @param \App\Models\Service $service
* @return \Illuminate\Http\Resources\Json\JsonResource
* @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
{
try {
/** @var \App\Models\Customer */
$customer = Customer::where('email', $request->email)->firstOrFail();
$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());
throw new ApiErrorException(ApiError::authFailed);
}
if (!$customerLogin->status) {
throw new ApiErrorException(ApiError::customerNotActivated());
throw new ApiErrorException(ApiError::customerNotActivated);
}
UpdateCustomerLastConnection::dispatch($customer, $customerLogin, now())
@ -217,19 +223,21 @@ class CustomerController extends Controller
return new CustomerResource($service->brand, $customer);
} catch (ModelNotFoundException $e) {
throw new ApiErrorException(ApiError::authFailed());
}//end try
throw new ApiErrorException(ApiError::authFailed);
} //end try
}
/**
* Activate the account of the user
* @param Customer $customer
* @param Service $service
* Activate the account of the user.
*
* @param \App\Models\Customer $customer
* @param \App\Models\Service $service
* @return \Illuminate\Http\JsonResponse
* @throws ApiErrorException With ApiError::customerAlreadyActive if customer account is already active.
* @throws CrmException If saving the customer to DB failed.
* @throws \App\Exceptions\Api\V2\ApiErrorException With ApiError::customerAlreadyActive
* 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.
$this->authorize('customerBelongsToService', $customer);
@ -238,7 +246,7 @@ class CustomerController extends Controller
$login = $customer->login($service->brand, Customer::FIRST_OR_FAIL);
if ($login->status) {
throw new ApiErrorException(ApiError::customerAlreadyActive());
throw new ApiErrorException(ApiError::customerAlreadyActive);
}
$login->status = true;

View file

@ -6,7 +6,9 @@ use App\Enums\ApiError;
use App\Exceptions\Api\V2\ApiErrorException;
use App\Exceptions\Api\V2\CrmException;
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\SingleOptinRemoveWithHashcodeRequest;
use App\Models\CustomerInfo;
use App\Models\Service;
use App\Models\SingleOptin;
@ -14,35 +16,64 @@ use App\Models\SingleOptin;
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 Service $service
* @return JsonResponse
* @throws ApiErrorException With ApiError::singleOptinCustomerExists if this email already has a full account.
* @throws CrmException If the single optin could not be added or removed.
* @param \App\Http\Requests\Api\V2\SingleOptinRequest $request
* @param \App\Models\Service $service
* @return \Illuminate\Http\JsonResponse
* @throws \App\Exceptions\Api\V2\ApiErrorException With ApiError::singleOptinCustomerExists
* 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()) {
throw new ApiErrorException(ApiError::singleOptinCustomerExists());
$validatedData = $request->validated();
if (CustomerInfo::getFromEmail($service->brand, $validatedData['email'])->exists()) {
throw new ApiErrorException(ApiError::singleOptinCustomerExists);
}
/** @var \App\Models\SingleOptin */
$singleOptin = SingleOptin::where('email', $request->email)
->whereHas('brand', function ($q) use ($service) {
$q->whereId($service->brand->id);
})->firstOrNew();
$singleOptin = SingleOptin::query()
->where('email', $validatedData['email'])
->whereHas('brand', function ($q) use ($service) {
$q->where('id', $service->brand->id);
})
->firstOrNew();
if ($request->opt_in) {
$singleOptin->fill($request->validated());
$singleOptin->brand()->associate($service->brand);
$success = $singleOptin->save();
} else {
$success = $singleOptin->delete() !== false;
if (!$this->changeOptin($service, $request, \boolval($validatedData['opt_in']), $singleOptin)) {
// @codeCoverageIgnoreStart
throw new CrmException('Api failed to update single optin.');
// @codeCoverageIgnoreEnd
}
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
throw new CrmException('Api failed to update single optin.');
// @codeCoverageIgnoreEnd
@ -50,4 +81,23 @@ class OptinController extends Controller
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.
*
* @param Service $service
* @return JsonResource
* @param \App\Models\Service $service
* @return \Illuminate\Http\Resources\Json\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.
*
* @param Service $service
* @param Customer $customer
* @return JsonResource
* @param \App\Models\Service $service
* @param \App\Models\Customer $customer
* @return \Illuminate\Http\Resources\Json\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.
*
* @param Service $service
* @param Customer $customer
* @return JsonResource
* @param \App\Models\Service $service
* @param \App\Models\Customer $customer
* @return \Illuminate\Http\Resources\Json\JsonResource
*/
public function unAnsweredBy(Service $service, Customer $customer): JsonResource
{
// * Make sure we are accessing a customer that belongs to our service.
$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);
})->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);
})->pluck('id'));
})->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\QuestionStatsResource;
use App\Models\BrandStat;
use App\Models\CustomerAnswer;
use App\Models\ModelAnswer;
use App\Models\Question;
use App\Models\Service;
use Illuminate\Http\Resources\Json\ResourceCollection;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
class StatsController extends Controller
{
/**
* Get specific brand statistics
* Get specific brand statistics.
*
* @param GetBrandStatsRequest $request
* @param Service $service
* @return ResourceCollection
* @param \App\Http\Requests\Api\V2\GetBrandStatsRequest $request
* @param \App\Models\Service $service
* @return \Illuminate\Http\Resources\Json\ResourceCollection
*/
public function brandStats(GetBrandStatsRequest $request, Service $service): ResourceCollection
{
$stats = BrandStat::whereDate('created_at', '>=', $request->start)
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
* Get statistics for a specific question.
*
* @param GetQuestionStatsRequest $request
* @param Question $question
* @return QuestionStatsResource
* @param \App\Http\Requests\Api\V2\GetQuestionStatsRequest $request
* @param \App\Models\Question $question
* @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.
$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 Carbon $start
* @param Carbon $end
* @return Collection
* @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): Collection
private function answersByDays(Collection $answers, Carbon $start, Carbon $end): \Illuminate\Support\Collection
{
// * Totalize by date all answers.
$answersByDays = CustomerAnswer::whereDate('created_at', '>=', $start)
$answersByDays = ModelAnswer::query()
->whereDate('created_at', '>=', $start)
->whereDate('created_at', '<=', $end)
->whereIn('answer_id', $answers->pluck('id'))
->get()->map(function ($custAnswer) {
->get()
->map(function ($custAnswer) {
return [
'answer_id' => $custAnswer->answer_id,
'created_at' => $custAnswer->created_at->format('Y-m-d'),
'answer_id' => $custAnswer->answer_id,
'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 $custAnswers->count();
});
});
do {
$currentDate = $start->format('Y-m-d');
$currentDate = $start->isoFormat('YYYY-MM-DD');
/** @var \Illuminate\Support\Collection $answersByDays */
if (!$answersByDays->has($currentDate)) {
$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

@ -0,0 +1,173 @@
<?php
namespace App\Http\Controllers\Bo;
use App\Http\Controllers\Controller;
use App\Http\Requests\Bo\Answers\StoreAnswerRequest;
use App\Http\Requests\Bo\Answers\UpdateAnswerRequest;
use App\Models\Answer;
use App\Models\Question;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
class AnswerController extends Controller
{
/**
* Create the controller instance.
*/
public function __construct()
{
$this->authorizeResource(Answer::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\Question $questionModel */
$questionModel = $request->question;
/** @var \Illuminate\Database\Eloquent\Builder $query */
$query = Answer::query()->where('question_id', $questionModel->id);
if ($request->search) {
$this->searchQuery(
$query,
$request->search,
null,
'answer',
);
}
$this->sortQuery($query);
$searchFields = trans('Réponse');
$answerModels = $query->paginate($this->getPagination());
return view('bo.pages.answers.index', compact('answerModels', 'questionModel', 'searchFields'));
}
/**
* Show the form for creating a new resource.
*
* @param \Illuminate\Http\Request $request
* @param \App\Models\Answer $answer
* @return \Illuminate\Contracts\View\View
*/
public function create(Request $request, Answer $answer): \Illuminate\Contracts\View\View
{
/** @var \App\Models\Question $questionModel */
$questionModel = $request->question;
return view('bo.pages.answers.create', [
'answerModel' => $answer,
'questionModel' => $questionModel,
]);
}
/**
* Store a newly created resource in storage.
*
* @param \App\Http\Requests\Bo\Answers\StoreAnswerRequest $request
* @return \Illuminate\Http\RedirectResponse
*/
public function store(StoreAnswerRequest $request): \Illuminate\Http\RedirectResponse
{
return DB::transaction(function () use ($request) {
$answer = new Answer();
$answer->fill($request->validated());
$questionModel = $request->route()->parameter('question');
$answer->question()->associate($questionModel->getKey());
if ($answer->saveOrFail()) {
return redirect()
->route('bo.questions.answers.edit', ['question' => $questionModel, 'answer' => $answer])
->with('success', trans(
':model a été créé.',
['model' => Str::singular(trans('models.classes.answers'))]
));
}
return redirect()
->back()
->with('error', trans(
':model n\'a pas été créé.',
['model' => Str::singular(trans('models.classes.answers'))]
));
});
}
/**
* Show the form for editing the specified resource.
*
* @param \App\Models\Question $question
* @param \App\Models\Answer $answers
* @return \Illuminate\Contracts\View\View
*/
public function edit(Question $question, Answer $answers): \Illuminate\Contracts\View\View
{
return view('bo.pages.answers.edit', [
'answerModel' => $answers,
'questionModel' => $question,
]);
}
/**
* Update a spacific resource in storage.
*
* @param \App\Http\Requests\Bo\Answers\UpdateAnswerRequest $request
* @param \App\Models\Question $question
* @param \App\Models\Answer $answer
* @return \Illuminate\Http\RedirectResponse
*/
public function update(
UpdateAnswerRequest $request,
Question $question,
Answer $answer
): \Illuminate\Http\RedirectResponse {
return DB::transaction(function () use ($request, $question, $answer) {
$answer->fill($request->validated());
if ($answer->saveOrFail()) {
return redirect()
->route('bo.questions.answers.edit', ['question' => $question, 'answer' => $answer])
->with('success', trans(
':model a été mis à jour.',
['model' => Str::singular(trans('models.classes.answers'))]
));
}
return redirect()
->back()
->with('error', trans(
':model n\'a pas été mis à jour.',
['model' => Str::singular(trans('models.classes.answers'))]
));
});
}
/**
* Remove the specified resource from storage.
*
* @param \App\Models\Question $question
* @param \App\Models\Answer $answer
* @return \Illuminate\Http\RedirectResponse
*/
public function destroy(Question $question, Answer $answer): \Illuminate\Http\RedirectResponse
{
$answer->singleOptins()->detach();
if ($answer->deleteOrFail()) {
return redirect()->route('bo.questions.answers.index', $question)
->with('success', trans(
':model a été supprimé.',
['model' => Str::singular(trans('models.classes.answers'))]
));
}
return redirect()->back()
->with('error', trans(
':model n\'a pas pu être supprimé.',
['model' => Str::singular(trans('models.classes.answers'))]
));
}
}

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

@ -0,0 +1,155 @@
<?php
namespace App\Http\Controllers\Bo;
use App\Http\Controllers\Controller;
use App\Http\Requests\Bo\Brands\StoreBrandRequest;
use App\Http\Requests\Bo\Brands\UpdateBrandRequest;
use App\Http\Requests\Bo\PaginateModelsRequest;
use App\Models\Brand;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Uwil\LaravelCommon\Helpers\CollectionHelper;
class BrandController extends Controller
{
/**
* Create the controller instance.
*/
public function __construct()
{
$this->authorizeResource(Brand::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 = Brand::query();
if ($request->search) {
$this->searchQuery(
$query,
$request->search,
null,
'name',
);
}
$this->sortQuery($query);
$searchFields = trans('Nom');
$query->withCount(['customerInfos', 'customerInfos as customer_infos_optin_count' => function (Builder $q) {
$q->where('opt_in', true);
}]);
$brandModels = $query->paginate($this->getPagination());
return view('bo.pages.brands.index', compact('brandModels', 'searchFields'));
}
/**
* Show the form for creating a new resource.
*
* @param \App\Models\Brand $brands
* @return \Illuminate\Contracts\View\View
*/
public function create(Brand $brands): \Illuminate\Contracts\View\View
{
return view('bo.pages.brands.create', [
'brandModel' => $brands
]);
}
/**
* Store a newly created resource in storage.
*
* @param \App\Http\Requests\Brands\Bo\StoreBrandRequest $request
* @return \Illuminate\Http\RedirectResponse
*/
public function store(StoreBrandRequest $request): \Illuminate\Http\RedirectResponse
{
return DB::transaction(function () use ($request) {
$brand = new Brand();
$brand->fill($request->validated());
if ($brand->saveOrFail()) {
return redirect()
->route('bo.brands.edit', $brand)
->with('success', trans(
':model a été créé.',
['model' => Str::singular(trans('models.classes.brands'))]
));
}
return redirect()
->back()
->with('error', trans(
':model n\'a pas été créé.',
['model' => Str::singular(trans('models.classes.brands'))]
));
});
}
/**
* Show the form for editing the specified resource.
*
* @param \App\Models\Brand $brands
* @return \Illuminate\Contracts\View\View
*/
public function edit(Brand $brands): \Illuminate\Contracts\View\View
{
return view('bo.pages.brands.edit', [
'brandModel' => $brands
]);
}
/**
* Update a spacific resource in storage.
*
* @param \App\Http\Requests\Brands\Bo\UpdateBrandRequest $request
* @param \App\Models\Brand $brand
* @return \Illuminate\Http\RedirectResponse
*/
public function update(UpdateBrandRequest $request, Brand $brand): \Illuminate\Http\RedirectResponse
{
return DB::transaction(function () use ($request, $brand) {
$brand->fill($request->validated());
if ($brand->saveOrFail()) {
return redirect()
->route('bo.brands.edit', $brand)
->with('success', trans(
':model a été mis à jour.',
['model' => Str::singular(trans('models.classes.brands'))]
));
}
return redirect()
->back()
->with('error', trans(
':model n\'a pas été mis à jour.',
['model' => Str::singular(trans('models.classes.brands'))]
));
});
}
/**
* Get a paginate resource.
*
* @param \App\Http\Requests\Bo\PaginateModelsRequest $request
* @return \Illuminate\Http\JsonResponse
*/
public function jsonPaginateModels(PaginateModelsRequest $request): \Illuminate\Http\JsonResponse
{
$models = CollectionHelper::paginateManually(
Brand::getUserAssociatedBrands(),
$this->jsonItemPerPagination,
(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

@ -0,0 +1,288 @@
<?php
namespace App\Http\Controllers\Bo;
use App\Http\Controllers\Controller;
use App\Http\Requests\Bo\QuestionExport;
use App\Http\Requests\Bo\Questions\StoreQuestionRequest;
use App\Http\Requests\Bo\Questions\UpdateQuestionRequest;
use App\Models\Brand;
use App\Models\Customer;
use App\Models\Question;
use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use League\Csv\Writer;
use Symfony\Component\HttpFoundation\StreamedResponse;
class QuestionController extends Controller
{
/**
* Create the controller instance.
*/
public function __construct()
{
$this->authorizeResource(Question::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 = Question::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,
'question',
);
}
$this->sortQuery($query);
$searchFields = trans('Question');
$query->with(['brand', 'answers']);
/** @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.
*
* @param \App\Models\Question $question
* @return \Illuminate\Contracts\View\View
*/
public function create(Question $question): \Illuminate\Contracts\View\View
{
$brandModels = Brand::query()->orderBy('name', 'ASC')->paginate($this->jsonItemPerPagination);
return view('bo.pages.questions.create', [
'questionModel' => $question,
'brandModels' => $brandModels
]);
}
/**
* Store a newly created resource in storage.
*
* @param \App\Http\Requests\Questions\StoreQuestionRequest $request
* @return \Illuminate\Http\RedirectResponse
*/
public function store(StoreQuestionRequest $request): \Illuminate\Http\RedirectResponse
{
return DB::transaction(function () use ($request) {
$question = new Question();
$question->fill($request->validated());
if ($question->saveOrFail()) {
return redirect()
->route('bo.questions.edit', $question)
->with('success', trans(
':model a été créé.',
['model' => Str::singular(trans('models.classes.questions'))]
));
}
return redirect()
->back()
->with('error', trans(
':model n\'a pas été créé.',
['model' => Str::singular(trans('models.classes.questions'))]
));
});
}
/**
* Show the form for editing the specified resource.
*
* @param \App\Models\Question $question
* @return \Illuminate\Contracts\View\View
*/
public function edit(Question $question): \Illuminate\Contracts\View\View
{
$brandModels = Brand::query()->orderBy('name', 'ASC')->paginate($this->jsonItemPerPagination);
$question->load('brand');
return view('bo.pages.questions.edit', [
'questionModel' => $question,
'brandModels' => $brandModels
]);
}
/**
* Update a spacific resource in storage.
*
* @param \App\Http\Requests\Questions\UpdateQuestionRequest $request
* @param \App\Models\Question $question
* @return \Illuminate\Http\RedirectResponse
*/
public function update(UpdateQuestionRequest $request, Question $question): \Illuminate\Http\RedirectResponse
{
return DB::transaction(function () use ($request, $question) {
$question->fill($request->validated());
if ($question->saveOrFail()) {
return redirect()
->route('bo.questions.edit', $question)
->with('success', trans(
':model a été mis à jour.',
['model' => Str::singular(trans('models.classes.questions'))]
));
}
return redirect()
->back()
->with('error', trans(
':model n\'a pas été mis à jour.',
['model' => Str::singular(trans('models.classes.questions'))]
));
});
}
/**
* Show the specified resource.
*
* @param \App\Models\Question $question
* @return \Illuminate\Contracts\View\View
*/
public function show(Question $question): \Illuminate\Contracts\View\View
{
return view('bo.pages.questions.show', [
'questionModel' => $question
]);
}
/**
* Remove the specified resource from storage.
*
* @param \App\Models\Question $question
* @return \Illuminate\Http\RedirectResponse
*/
public function destroy(Question $question): \Illuminate\Http\RedirectResponse
{
if ($question->deleteOrFail()) {
return redirect()->route('bo.questions.index')
->with('success', trans(
':model a été supprimé.',
['model' => Str::singular(trans('models.classes.questions'))]
));
}
return redirect()->back()
->with('error', trans(
':model n\'a pas pu être supprimé.',
['model' => Str::singular(trans('models.classes.questions'))]
));
}
/**
* Shows the question export form
*
* @return \Illuminate\Contracts\View\View
*/
public function export(): \Illuminate\Contracts\View\View
{
$this->authorize('export', Question::class);
$brands = Brand::all();
return view('bo.pages.questions.export', compact('brands'));
}
/**
* Exports questions as csv the requested export type
*
* @param \App\Http\Requests\QuestionExport $request
* @return \Symfony\Component\HttpFoundation\StreamedResponse
*/
public function exportSubmit(QuestionExport $request): \Symfony\Component\HttpFoundation\StreamedResponse
{
$results = [];
/** @var \App\Models\Brand $brand */
$brand = Brand::query()->where('id', $request->brand)->firstOrFail();
$questions = $brand->questions->load('answers');
foreach ($questions as $question) {
$answersCounts = DB::query()
->selectRaw('count(*) as total, answer_id')
->from('model_answer')
->where('model_type', '=', Customer::class)
->whereIn('answer_id', $question->answers->modelKeys())
->groupBy('answer_id')
->get()
->keyBy('answer_id');
$results[$question->id] = [
'question' => $question->question,
'answers' => []
];
foreach ($question->answers as $answer) {
$results[$question->id]['answers'][] =
[
"$answer->answer ",
$answersCounts[$answer->id]->total ?? 0
];
}
} //end foreach
$w = Writer::createFromString('');
$w->setDelimiter(';');
$w->setEnclosure('"');
$w->setOutputBOM(Writer::BOM_UTF8);
// * 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($brand->name)
)
);
foreach ($results as $result) {
$w->insertOne([
'question' => $result['question'],
]);
foreach ($result['answers'] as $answer) {
$w->insertOne($answer);
}
$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
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

@ -0,0 +1,211 @@
<?php
namespace App\Http\Controllers\Bo;
use App\Http\Controllers\Controller;
use App\Http\Requests\Bo\GenerateServiceToken;
use App\Http\Requests\Bo\Services\StoreServiceRequest;
use App\Http\Requests\Bo\Services\UpdateServiceRequest;
use App\Mail\ServiceCredentialRenewed;
use App\Models\Brand;
use App\Models\Service;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Str;
use Uwil\LaravelCommon\Helpers\CollectionHelper;
class ServiceController extends Controller
{
/**
* Create the controller instance.
*/
public function __construct()
{
$this->authorizeResource(Service::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 = Service::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,
'name',
);
}
$this->sortQuery($query);
$searchFields = trans('Nom');
$query->with(['brand']);
$serviceModels = $query->paginate($this->getPagination());
return view('bo.pages.services.index', compact('serviceModels', 'searchFields'));
}
/**
* Show the form for creating a new resource.
*
* @param \App\Models\Service $service
* @return \Illuminate\Contracts\View\View
*/
public function create(Service $service): \Illuminate\Contracts\View\View
{
$brandModels = CollectionHelper::paginate(Brand::getUserAssociatedBrands(), $this->jsonItemPerPagination);
return view('bo.pages.services.create', [
'serviceModel' => $service,
'brandModels' => $brandModels
]);
}
/**
* Store a newly created resource in storage.
*
* @param \App\Http\Requests\Bo\Services\StoreServiceRequest $request
* @return \Illuminate\Http\RedirectResponse
*/
public function store(StoreServiceRequest $request): \Illuminate\Http\RedirectResponse
{
return DB::transaction(function () use ($request) {
$service = new Service();
$service->fill($request->validated());
$service->generateBearerToken();
$this->generateServicePassword($service);
if ($service->saveOrFail()) {
return redirect()
->route('bo.services.edit', $service)
->with('success', trans(
':model a été créé.',
['model' => Str::singular(trans('models.classes.services'))]
));
}
return redirect()
->back()
->with('error', trans(
':model n\'a pas été créé.',
['model' => Str::singular(trans('models.classes.services'))]
));
});
}
/**
* Show the form for editing the specified resource.
*
* @param \App\Models\Service $service
* @return \Illuminate\Contracts\View\View
*/
public function edit(Service $service): \Illuminate\Contracts\View\View
{
$brandModels = CollectionHelper::paginate(Brand::getUserAssociatedBrands(), $this->jsonItemPerPagination);
return view('bo.pages.services.edit', [
'serviceModel' => $service,
'brandModels' => $brandModels
]);
}
/**
* Update a spacific resource in storage.
*
* @param \App\Http\Requests\Services\Bo\UpdateServiceRequest $request
* @param \App\Models\Service $service
* @return \Illuminate\Http\RedirectResponse
*/
public function update(UpdateServiceRequest $request, Service $service): \Illuminate\Http\RedirectResponse
{
return DB::transaction(function () use ($request, $service) {
$service->fill($request->validated());
if ($service->saveOrFail()) {
return redirect()
->route('bo.services.edit', $service)
->with('success', trans(
':model a été mis à jour.',
['model' => Str::singular(trans('models.classes.services'))]
));
}
return redirect()
->back()
->with('error', trans(
':model n\'a pas été mis à jour.',
['model' => Str::singular(trans('models.classes.services'))]
));
});
}
/**
* Show the specified resource.
*
* @param \App\Models\Service $service
* @return \Illuminate\Contracts\View\View
*/
public function show(Service $service): \Illuminate\Contracts\View\View
{
return view('bo.pages.services.show', [
'serviceModel' => $service
]);
}
/**
* Remove the specified resource from storage.
*
* @param \App\Models\Service $service
* @return \Illuminate\Http\RedirectResponse
*/
public function destroy(Service $service): \Illuminate\Http\RedirectResponse
{
if ($service->deleteOrFail()) {
return redirect()->route('bo.services.index')
->with('success', trans(
':model a été supprimé.',
['model' => Str::singular(trans('models.classes.services'))]
));
}
return redirect()->back()
->with('error', trans(
':model n\'a pas pu être supprimé.',
['model' => Str::singular(trans('models.classes.services'))]
));
}
/**
* Regen service credentials.
*
* @param \App\Http\Requests\Bo\GenerateServiceToken $request
* @param \App\Models\Service $service
* @return \Illuminate\Http\RedirectResponse
*/
// phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClassAfterLastUsed, Squiz.Commenting.FunctionComment.Missing
public function generateServicePassword(
Service $service,
GenerateServiceToken $request = null
): \Illuminate\Http\RedirectResponse {
/** @var \App\Models\User $user */
$user = auth('backend')->user();
// * Special Condition for migration from v2 to v3.
$service->bearer_token = !Hash::needsRehash($service->bearer_token) ?
$service->generateBearerToken() : $service->bearer_token;
$bearerPwd = $service->generateBearerPassword();
$service->saveOrFail();
Mail::send(new ServiceCredentialRenewed($service, $user));
return back()
// phpcs:ignore Generic.Files.LineLength.TooLong
->with('error', "ATTENTION : METTEZ À JOUR LE SITE ! \nNouveau token d'API de $service->name : '$service->bearer_token' et mot de passe '$bearerPwd'");
}
}

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
namespace App\Http\Controllers;
namespace App\Http\Controllers\Bo;
use App\Http\Requests\StatsByBrandRequest;
use App\Http\Requests\StatsByQuestionRequest;
use App\Http\Controllers\Controller;
use App\Http\Requests\Bo\StatsByBrandRequest;
use App\Http\Requests\Bo\StatsByQuestionRequest;
use App\Models\Brand;
use App\Models\BrandStat;
use App\Models\CustomerAnswer;
use App\Models\ModelAnswer;
use App\Models\Question;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Schema;
use League\Csv\Writer;
/**
* @codeCoverageIgnore
*/
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
*/
public function viewStatsByBrand(StatsByBrandRequest $request)
{
Gate::authorize('viewStatsByBrand');
$stats = null;
if ($request->post()) {
$stats = BrandStat::whereDate('created_at', '>=', $request->start)
$stats = BrandStat::query()->whereDate('created_at', '>=', $request->start)
->whereDate('created_at', '<=', $request->end)
->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
*/
public function exportByBrandCsv(StatsByBrandRequest $request)
public function exportByBrandCsv(StatsByBrandRequest $request): \Illuminate\Http\Response
{
$brand = Brand::findOrFail($request->brand);
$stats = BrandStat::with('brand')->whereDate('created_at', '>=', $request->start)
$brand = Brand::query()->findOrFail($request->brand);
$stats = BrandStat::query()->with('brand')->whereDate('created_at', '>=', $request->start)
->whereDate('created_at', '<=', $request->end)
->whereBrandId($request->brand)->get();
@ -62,7 +62,7 @@ class StatsController extends Controller
foreach ($stats as $stat) {
$cols = [
$stat->created_at->format('Y-m-d'),
$stat->created_at->isoFormat('YYYY-MM-DD'),
$stat->brand->name
];
foreach ($columns as $colname) {
@ -70,7 +70,7 @@ class StatsController extends Controller
}
$w->insertOne($cols);
}
return response((string)$w, 200, [
return response((string) $w, 200, [
'Content-Encoding' => 'none',
'Content-Type' => 'text/csv; charset=UTF-8',
// 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
* @return \Illuminate\Http\Response
* @param \App\Http\Requests\Bo\StatsByQuestionRequest $request
* @return \Illuminate\Contracts\View\View
*/
public function viewByQuestion(StatsByQuestionRequest $request)
public function viewByQuestion(StatsByQuestionRequest $request): \Illuminate\Contracts\View\View
{
Gate::authorize('viewByQuestion');
$question = null;
$answers = null;
$answersByDays = null;
if ($request->post()) {
$question = Question::findOrFail($request->question);
$question = Question::query()->findOrFail($request->question);
$answers = $question->answers;
$start = Carbon::parse($request->start);
$end = Carbon::parse($request->end);
@ -99,18 +100,18 @@ class StatsController extends Controller
$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
*/
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;
$start = Carbon::parse($request->start);
$end = Carbon::parse($request->end);
@ -134,7 +135,7 @@ class StatsController extends Controller
$dayAnswers->values()->all()
));
}
return response((string)$w, 200, [
return response((string) $w, 200, [
'Content-Encoding' => 'none',
'Content-Type' => 'text/csv; charset=UTF-8',
// 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 Carbon $start The wanted start period.
* @param Carbon $end The wanted end period.
* @return Collection A collection with date as Y_m_d index with array of answers and their total.
* @param \Illuminate\Support\Collection $answers A collection of Answer.
* @param \Illuminate\Support\Carbon $start The wanted start period.
* @param \Illuminate\Support\Carbon $end The wanted end period.
* @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.
$answersByDays = CustomerAnswer::whereDate('created_at', '>=', $start)
$answersByDays = ModelAnswer::query()
->whereDate('created_at', '>=', $start)
->whereDate('created_at', '<=', $end)
->whereIn('answer_id', $answers->pluck('id'))
->whereIn('answer_id', $answers->modelKeys())
->get()->map(function ($custAnswer) {
return [
'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) {
return $day->map(function ($custAnswers) {
@ -169,7 +173,7 @@ class StatsController extends Controller
});
do {
$currentDate = $start->format('Y-m-d');
$currentDate = $start->isoFormat('YYYY-MM-DD');
/** @var \Illuminate\Support\Collection $answersByDays */
if (!$answersByDays->has($currentDate)) {
$answersByDays->put(
@ -185,7 +189,7 @@ class StatsController extends Controller
$answers->mapWithKeys(function ($answer) use ($curentAnswers) {
return [$answer->answer =>
$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

@ -1,44 +0,0 @@
<?php
namespace App\Http\Controllers;
use App\Http\Controllers\Controller;
use App\Models\Brand;
use Kwaadpepper\CrudPolicies\Http\Requests\CrudIndexRequest;
use Kwaadpepper\CrudPolicies\Traits\CrudController;
/**
* @codeCoverageIgnore
*/
class BrandController extends Controller
{
use CrudController {
index as crudIndex;
}
/** @var Brand */
public static $modelClass = Brand::class;
/**
* The Bo app layout
*
* @var string
*/
public static $layout = 'layouts.app';
/**
* Overload crud-policies index
*
* @param CrudIndexRequest $request
* @return \Illuminate\Http\Response
*/
public function index(CrudIndexRequest $request)
{
/** @var \Illuminate\View\View */
$view = $this->crudIndex($request);
if ($request->ajax()) {
return $view;
}
return view('brand.index', $view->getData());
}
}

View file

@ -2,17 +2,246 @@
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\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Foundation\Bus\DispatchesJobs;
use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Routing\Controller as BaseController;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\Session;
use Illuminate\Support\Str;
/**
* @codeCoverageIgnore
*/
class Controller extends BaseController
{
use AuthorizesRequests;
use DispatchesJobs;
use ValidatesRequests;
/** @var integer $itemPerPagination */
public $jsonItemPerPagination = 5;
/**
* Build Search query on specified fields
* splitting search into words or using whole sentence
*
* @param \Illuminate\Database\Eloquent\Builder $query The eloquent query builder.
* @param string $search The query string.
* @param callable $searchQuery The query string.
* @param string ...$fields Either a simple array or a string
* in case of a string the two fields
* would be concatenate to make the search.
* @return \Illuminate\Database\Eloquent\Builder
*/
protected function searchQuery(
Builder $query,
string $search,
callable $searchQuery = null,
string ...$fields
): \Illuminate\Database\Eloquent\Builder {
if (count($fields)) {
// Search using words.
$words = explode(' ', $search);
$query->where(function ($query) use ($searchQuery, $words, $search, $fields) {
foreach ($words as $word) {
if (!strlen($word)) {
continue;
}
$this->searchOnFields($query, $word, $fields);
} //end foreach
// Search whole using whole sentence.
$this->searchOnFields($query, $search, $fields);
if ($searchQuery) {
$searchQuery($query);
}
});
} //end if
return $query;
}
/**
* Build query search on all fields
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @param string $search
* @param array $fields
* @return \Illuminate\Database\Eloquent\Builder
*/
private function searchOnFields(
Builder $query,
string $search,
array $fields
): \Illuminate\Database\Eloquent\Builder {
$table = $query->getModel()->getTable();
foreach ($fields as $field) {
$field = str_replace('?', '\?', $field);
if (is_array($field)) {
$query->orWhereRaw(
sprintf("UPPER(CONCAT(%s)) LIKE UPPER(?)", collect($field)->map(function ($q) use ($table) {
return "`$table`.`$q`";
})->implode(', \' \', ')),
['%' . htmlspecialchars($search) . '%']
);
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;
}
/**
* 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 array $ignore
* @return \Illuminate\Database\Eloquent\Builder
*/
protected function sortQuery(Builder $query, array $ignore = []): \Illuminate\Database\Eloquent\Builder
{
$rName = request()->route()->getName();
$table = $query->getModel()->getTable();
if (request()->sort_col) {
Session::put("$rName.sort_col", request()->sort_col);
Session::put("$rName.sorted", true);
}
if (request()->sort_way) {
Session::put("$rName.sort_way", request()->sort_way);
Session::put("$rName.sorted", true);
}
if (request()->rstSrts) {
Session::remove("$rName.sorted");
Session::remove("$rName.sort_col");
Session::remove("$rName.sort_way");
}
if (request()->sort_col and request()->sort_way and Schema::hasColumn($table, request()->sort_col)) {
if (
!isset($ignore) or
(isset($ignore) and !in_array(request()->sort_col, $ignore))
) {
$query = $query->orderBy(\sprintf("$table.%s", request()->sort_col), request()->sort_way);
}
} elseif (Schema::hasColumn($table, 'order')) {
$query = $query->orderBy('order', 'ASC');
Session::put("$rName.sort_col", 'order');
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;
}
/**
* 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,317 +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\Facades\View;
use Illuminate\Support\Str;
use League\Csv\Writer;
use Symfony\Component\HttpFoundation\StreamedResponse;
/**
* @codeCoverageIgnore
*/
class CustomerController extends Controller
{
/**
* View share crud policies modelClass
*/
public function __construct()
{
$this->middleware(function ($request, $next) {
View::share('modelClass', Customer::class);
return $next($request);
});
}
/**
* 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")
);
}
/** @var \Illuminate\Pagination\AbstractPaginator */
$customers = $query->paginate(config('crud.paginate', 50));
return view('customer.index', compact(
'modelClass',
'modelTable',
'customers',
'brands'
));
}
/**
* 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('customer.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\'as 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('customer.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,159 +0,0 @@
<?php
namespace App\Http\Controllers;
use App\Http\Controllers\Controller;
use App\Http\Requests\QuestionExport;
use App\Models\Brand;
use App\Models\Question;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Kwaadpepper\CrudPolicies\Http\Requests\CrudIndexRequest;
use Kwaadpepper\CrudPolicies\Traits\CrudController;
use League\Csv\Writer;
use Symfony\Component\HttpFoundation\StreamedResponse;
/**
* @codeCoverageIgnore
*/
class QuestionController extends Controller
{
use CrudController {
index as crudIndex;
}
/** @var Question */
public static $modelClass = Question::class;
/**
* The Bo app layout
*
* @var string
*/
public static $layout = 'layouts.app';
/**
* Event index crud-policies
*
* @param Builder $query
* @return Builder
*/
public function indexQuery(Builder $query): Builder
{
if (request()->brand) {
$query = $query->where('brand_id', '=', request()->brand);
}
return $query;
}
/**
* Overload crud-policies index
*
* @param CrudIndexRequest $request
* @return \Illuminate\Http\Response
*/
public function index(CrudIndexRequest $request)
{
/** @var \Illuminate\View\View */
$view = $this->crudIndex($request);
if ($request->ajax()) {
return $view;
}
return view('question.index', $view->getData());
}
/**
* Shows the question export form
*
* @return \Illuminate\Http\Response
*/
public function export()
{
$brands = Brand::all();
return view('question.export', compact('brands'));
}
/**
* Exports questions as csv the requested export type
*
* @param QuestionExport $request
* @return \Symfony\Component\HttpFoundation\StreamedResponse
*/
public function exportSubmit(QuestionExport $request)
{
$results = [];
$brand = Brand::findOrFail($request->brand);
$questions = $brand->questions->load('answers');
foreach ($questions as $question) {
$answersCounts = DB::query()
->selectRaw('count(*) as total, answer_id')
->from('customer_answer')
->whereIn('answer_id', $question->answers->pluck('id'))
->groupBy('answer_id')
->get()->keyBy('answer_id');
$results[$question->id] = [
'question' => $question->question,
'answers' => []
];
foreach ($question->answers as $answer) {
$results[$question->id]['answers'][] =
[
"$answer->answer ",
$answersCounts[$answer->id]->total ?? 0
];
}
}
$w = Writer::createFromString('');
$w->setDelimiter(';');
$w->setEnclosure('"');
$w->setOutputBOM(Writer::BOM_UTF8);
// * 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($brand->name)
)
);
foreach ($results as $result) {
$w->insertOne([
'question' => $result['question'],
]);
foreach ($result['answers'] as $answer) {
$w->insertOne($answer);
}
$w->insertOne([]);
}
/*
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,72 +0,0 @@
<?php
namespace App\Http\Controllers;
use App\Http\Requests\GenerateServiceToken;
use App\Mail\ServiceCredentialRenewed;
use App\Models\Service;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Mail;
use Kwaadpepper\CrudPolicies\Http\Requests\CrudIndexRequest;
use Kwaadpepper\CrudPolicies\Traits\CrudController;
/**
* @codeCoverageIgnore
*/
class ServiceController extends Controller
{
use CrudController {
index as crudIndex;
}
/** @var Service */
public static $modelClass = Service::class;
/**
* The Bo app layout
*
* @var string
*/
public static $layout = 'layouts.app';
/**
* Overload crud-policies index
*
* @param CrudIndexRequest $request
* @return \Illuminate\Contracts\View\View|\Illuminate\Contracts\View\Factory
*/
public function index(CrudIndexRequest $request)
{
/** @var \Illuminate\View\View */
$view = $this->crudIndex($request);
if ($request->ajax()) {
return $view;
}
return view('service.index', $view->getData());
}
/**
* Regen service credentials
*
* @param GenerateServiceToken $request
* @param Service $service
* @return \Illuminate\Http\RedirectResponse
*/
// phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClassBeforeLastUsed, Squiz.Commenting.FunctionComment.Missing
public function generateServicePassword(GenerateServiceToken $request, Service $service)
{
/** @var \App\Models\User $user */
$user = auth()->user();
// * Special Condition for migration from v2 to v3.
$service->bearer_token = !Hash::needsRehash($service->bearer_token) ?
$service->generateBearerToken() : $service->bearer_token;
$bearerPwd = $service->generateBearerPassword();
$service->saveOrFail();
Mail::send(new ServiceCredentialRenewed($service, $user));
return back()
// phpcs:ignore Generic.Files.LineLength.TooLong
->with('error', "ATTENTION : METTEZ À JOUR LE SITE ! \nNouveau token d'API de $service->name : '$service->bearer_token' et mot de passe '$bearerPwd'");
}
}

View file

@ -1,58 +0,0 @@
<?php
namespace App\Http\Controllers;
use App\Models\SingleOptin;
use Illuminate\Database\Eloquent\Builder;
use Kwaadpepper\CrudPolicies\Http\Requests\CrudIndexRequest;
use Kwaadpepper\CrudPolicies\Traits\CrudController;
/**
* @codeCoverageIgnore
*/
class SingleOptinController extends Controller
{
use CrudController {
index as crudIndex;
}
/** @var SingleOptin */
public static $modelClass = SingleOptin::class;
/**
* The Bo app layout
*
* @var string
*/
public static $layout = 'layouts.app';
/**
* Event index crud-policies
*
* @param Builder $query
* @return Builder
*/
public function indexQuery(Builder $query): Builder
{
if (request()->brand) {
$query = $query->where('brand_id', request()->brand);
}
return $query;
}
/**
* Overload crud-policies index
*
* @param CrudIndexRequest $request
* @return \Illuminate\Contracts\View\View|\Illuminate\Contracts\View\Factory
*/
public function index(CrudIndexRequest $request)
{
/** @var \Illuminate\View\View */
$view = $this->crudIndex($request);
if ($request->ajax()) {
return $view;
}
return view('single_optin.index', $view->getData());
}
}

View file

@ -14,13 +14,12 @@ class Kernel extends HttpKernel
* @var array
*/
protected $middleware = [
\App\Http\Middleware\TrustHosts::class,
\App\Http\Middleware\TrustProxies::class,
\Fruitcake\Cors\HandleCors::class,
\Illuminate\Http\Middleware\HandleCors::class,
\App\Http\Middleware\PreventRequestsDuringMaintenance::class,
\Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
\App\Http\Middleware\TrimStrings::class,
\App\Http\Middleware\XssSanitizer::class,
\Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class
];
/**
@ -30,21 +29,27 @@ class Kernel extends HttpKernel
*/
protected $middlewareGroups = [
'web' => [
\Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
\App\Http\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\App\Http\Middleware\VerifyCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
\App\Http\Middleware\SecureHeaders::class
],
'api' => [
'throttle:api',
\Illuminate\Routing\Middleware\ThrottleRequests::class . ':api',
\App\Http\Middleware\JsonOnly::class,
\App\Http\Middleware\ApiResponse::class,
\App\Http\Middleware\ApiCheckTimeStamps::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
*/
protected $routeMiddleware = [
'auth' => \App\Http\Middleware\Authenticate::class,
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
'can' => \Illuminate\Auth\Middleware\Authorize::class,
'check-brand' => \App\Http\Middleware\CheckBrand::class,
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
'auth' => \App\Http\Middleware\Authenticate::class,
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
'auth.session' => \Illuminate\Session\Middleware\AuthenticateSession::class,
'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
'can' => \Illuminate\Auth\Middleware\Authorize::class,
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class,
'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
'signed' => \App\Http\Middleware\ValidateSignature::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::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;
use App\Enums\ApiError;
use App\Exceptions\Api\V2\ApiErrorException;
use App\Exceptions\Api\V4\ApiErrorException;
use App\Lib\Helper;
use Illuminate\Http\Request;
use Illuminate\Support\Carbon;
@ -18,22 +18,24 @@ class ApiCheckTimeStamps
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @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')) {
throw new ApiErrorException(ApiError::wrongTimeStamp());
throw new ApiErrorException(ApiError::wrongTimeStamp);
}
if (
Carbon::parse($request->timestamp, 'utc')
->isBefore(Carbon::now('UTC')->subSeconds(config('api.timestampTimeout', 10)))
) {
throw new ApiErrorException(ApiError::wrongTimeStamp());
throw new ApiErrorException(ApiError::wrongTimeStamp);
}
}
return $next($request);

View file

@ -4,12 +4,11 @@ namespace App\Http\Middleware;
use App\Enums\ApiStatus;
use App\Http\Resources\Api\V2\ApiResponse as V2ApiResponse;
use App\Http\Resources\Api\V4\ApiResponse as V4ApiResponse;
use App\Lib\Helper;
use App\Models\Service;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
class ApiResponse
{
@ -21,7 +20,7 @@ class ApiResponse
* @return mixed
* @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 */
$response = $next($request);
@ -35,19 +34,6 @@ class ApiResponse
// Set no cache header.
$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 */
if (\optional($service = app(Service::class))->id) {
$service->incrementStat();
@ -56,18 +42,23 @@ class ApiResponse
/** @var object $data */
$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 (
gettype($data) === "object" and
isset($data->status) and
isset($data->data) 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;
}
/** @var \App\Enums\ApiStatus $status */
$status = intval($response->getStatusCode() / 100) == 2 ? ApiStatus::OK() : ApiStatus::KO();
return $response->setData(new V2ApiResponse((object)$response->getData(), $status));
$status = intval($response->getStatusCode() / 100) == 2 ? ApiStatus::OK : ApiStatus::KO;
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;
use App\Lib\Helper;
use App\Models\Service;
use Closure;
use Illuminate\Auth\Middleware\Authenticate as Middleware;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Session;
/**
* Updated for ApiV2
*/
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.
*
* @param mixed $request
* @param \Illuminate\Http\Request $request
* @return string|null
* @codeCoverageIgnore
*/
protected function redirectTo($request)
protected function redirectTo(Request $request): ?string
{
if (! $request->expectsJson()) {
return route('login');
$logins = [
'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);
}
}

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