Compare commits

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

503 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
84018f9d5b
Ticket 738 : Rewrite DeleteUnconfirmedCustomers 2022-03-29 18:09:29 +02:00
Jérémy Munsch
f4c60bf6f7
Ticket 698 :
- Npm audit Fix + update
- Fix filter search and reset on customers.index
2022-03-28 16:22:33 +02:00
Vincent BRIDIER
f106ec7a4d
ticket 584 : Fix answer save 2022-03-22 11:47:58 +01:00
Vincent BRIDIER
708baaa59d
Merge branch 'fixSymphonyRequestEmptyStrings' into develop 2022-03-22 11:29:13 +01:00
Vincent BRIDIER
e08123212b
Fix Arcadia phone length field (RemoveSpaces) 2022-03-22 11:26:11 +01:00
Jérémy Munsch
3f88a572e7
Fix Laravel Framework issue with empty inputs string casted to null (Hashmac signature calculation) 2022-03-17 15:21:30 +01: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
33fac79758
changed comments to be user friendly
Bump to 3.0.2
2022-02-23 11:34:21 +01:00
Jérémy Munsch
87b4007339
Merge branch 'develop' into trunk 2022-02-10 11:28:00 +01:00
Jérémy Munsch
1c3ad6079e
Ajout config supervisor workers 2022-02-10 11:24:22 +01:00
Jérémy Munsch
45cc2276a5
update client 2022-02-10 09:44:36 +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
7fade7390b
Merge branch 'v3' into develop 2022-01-06 12:11:19 +01:00
81aabdab72
Version 3.0.0
This removes api authentification with token in favor of signature method.
Refactorize code and clean up. It add more unit test plus code coverage
in test/converage.

- Added config.api.timestampTimeout
- Added Helper::signRequest This calculates the request signature
- Added more unit tests and extend code coverage.
- Added rate limiter for web routes
- Added RemoveSpaces Sanitizer on api requests
- Added stress test suite
- Changed AuthServiceProvider api-token to api-hmac
- Changed CustomerSeeder to have at least of customer activated (for unit tests)
- Changed used transation for saveing multiple models
- Changed, in ServiceController generateApiToken to generateServicePassword
- Changed, timestamp has to be send in request with a DateTime format, it also has to a UTC  value
- Composer added brianium/paratest
- Composer update
- Fixed case where removing single optin could be the wrong one (not for the good service)
- Fixed customer delete
- Fixed customer would not be resolved by brand.
- Fixed Really check that request is json checking body content, preventing sending malformed requests
- Remove ApiError::needAuth
- Removed api_token
- Removed ApiController::authenticate
- Removed config.api.tokenTimeout
- Removed Header check or usage because of HAProxy doing weird stuff
- Removed lock files from gitignore
- Removed memcached dependency
- Removed unused questions resources api routes
- Removed workspace file

Migrations
- Added unique index on services.bearer_token
- Changed customer_id on customer_answer is nullable
- Changed services.bearer_pwd length to 312 (the size of the token)
- Dropped services.api_token and service.api_token_date
2022-01-06 12:11:05 +01:00
c94827127b
fix arcadia testValidateGeneratedXmlGame 2021-12-05 19:29:10 +01:00
d748758676
update readme tests 2021-12-05 19:28:53 +01:00
e1b2d86315
cleanup git module flaw 2021-12-05 18:28:53 +01:00
4c9b760d09
update readme to run tests
change composer.json test memcache to use 127.0.0.1 instead of localhost
2021-12-05 17:31:09 +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
28b04252ea
add LdcWebStatusChecker 2021-12-03 14:36:12 +01:00
Jérémy Munsch
c22068b8ea
update client 2021-12-03 14:32:04 +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
f0a771031c
Arcadia push order id in name 2021-11-15 11:34:57 +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
4c5f51bf97
fix customer show missing crud var 2021-11-12 17:03:20 +01:00
Jérémy Munsch
8a289f98ed
remove double paginator set in appservice provider 2021-11-12 17:03:01 +01:00
Jérémy Munsch
d6b0c5a039
add laravel-mix-clean 2021-11-10 16:57:03 +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
afa3e06699
Arcadia xml add hint for xml failed 2021-11-10 09:13:45 +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
f7edb2ff5e
force bump all kwaadpeppers dependencies 2021-11-08 16:45:30 +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
12eaeed020
export fix get where opt_in true 2021-11-08 16:01:45 +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
d38d8c861d
remove setServiceStat high retry 2021-11-08 15:02:15 +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
22d3cf0082
Fix repair_stats_jobs migration not deleting jobs 2021-11-05 16:23:01 +01:00
Jérémy Munsch
4294de1a6d
Remove redis queue and composer spatie/laravel-mix-preload in favor of a queue named "stats" with special supervisor conf 2021-11-05 16:22:36 +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
9b5bbd9691
Merge branch 'stats' into develop 2021-11-05 15:12:05 +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
744 changed files with 190028 additions and 15404 deletions

5
.browserslistrc Normal file
View file

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

2
.dockerignore Normal file
View file

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

View file

@ -3,9 +3,9 @@ root = true
[*] [*]
charset = utf-8 charset = utf-8
end_of_line = lf end_of_line = lf
insert_final_newline = true
indent_style = space
indent_size = 4 indent_size = 4
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true trim_trailing_whitespace = true
[*.md] [*.md]
@ -13,3 +13,9 @@ trim_trailing_whitespace = false
[*.{yml,yaml}] [*.{yml,yaml}]
indent_size = 2 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_USERNAME=root
DB_PASSWORD= DB_PASSWORD=
DB_CONNECTION_OLD=mysql
DB_HOST_OLD=127.0.0.1
DB_PORT_OLD=3306
DB_DATABASE_OLD=ldccrm_12_2020
DB_USERNAME_OLD=root
DB_PASSWORD_OLD=
MIGRATIONPAGINATION=20000
BROADCAST_DRIVER=log BROADCAST_DRIVER=log
CACHE_DRIVER=file CACHE_DRIVER=file
QUEUE_CONNECTION=sync QUEUE_CONNECTION=database
SESSION_DRIVER=file SESSION_DRIVER=file
SESSION_LIFETIME=120 SESSION_LIFETIME=120
@ -34,23 +25,27 @@ REDIS_PASSWORD=null
REDIS_PORT=6379 REDIS_PORT=6379
MAIL_MAILER=smtp MAIL_MAILER=smtp
MAIL_HOST=smtp.mailtrap.io MAIL_HOST=dev-mailpit
MAIL_PORT=2525 MAIL_PORT=1025
MAIL_USERNAME=null MAIL_USERNAME=null
MAIL_PASSWORD=null MAIL_PASSWORD=null
MAIL_ENCRYPTION=null MAIL_ENCRYPTION=null
MAIL_FROM_ADDRESS=null MAIL_FROM_ADDRESS=null
MAIL_FROM_NAME="${APP_NAME}" MAIL_FROM_NAME="${APP_NAME}"
MAIL_DIGITAL_SERVICE=""
AWS_ACCESS_KEY_ID= # Sentry
AWS_SECRET_ACCESS_KEY= SENTRY_LARAVEL_DSN=
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=
PUSHER_APP_ID= # UWIL ADMIN SECURE
PUSHER_APP_KEY= # - LIST SEPARATED WITH ',' (Required)
PUSHER_APP_SECRET= UWIL_ADMIN_SECURE_CIDR_LIST=""
PUSHER_APP_CLUSTER=mt1 # - LIST SEPARATED WITH ',' (Optional)
UWIL_ADMIN_SECURE_CIDR_PROJECT_ALLOWED_LIST=""
MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}" # SAML
MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}" SAML2_DEFAULT_TENANT_UUID=
SAML2_SP_CERT_x509=
SAML2_SP_CERT_PRIVATEKEY=
SAML2_CONTACT_TECHNICAL_EMAIL=
SAML2_CONTACT_SUPPORT_EMAIL=

View file

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

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 * text=auto eol=lf
*.css linguist-vendored
*.scss linguist-vendored *.blade.php diff=html
*.js linguist-vendored *.css diff=css
*.html diff=html
*.md diff=markdown
*.php diff=php
/.github export-ignore
CHANGELOG.md export-ignore CHANGELOG.md export-ignore
.styleci.yml export-ignore

37
.gitignore vendored
View file

@ -1,22 +1,31 @@
/node_modules
/public/hot
/public/storage
/storage/*.key
/vendor
.vscode/
.idea/
package-lock.json
composer.lock
.env .env
.env.testing .env.testing
.env.backup .env.backup
.env.production
.phpunit.result.cache .phpunit.result.cache
.stylelintcache
/.fleet
/.idea
/.phpunit.cache
/.vscode
/node_modules
/bootstrap
/public/build
/public/cache
/public/hot
/public/sitemap.xml
/public/storage
/storage/*.key
/vendor
auth.json
docker-compose.override.yml
Homestead.json Homestead.json
Homestead.yaml Homestead.yaml
npm-debug.log npm-debug.log
yarn-error.log yarn-error.log
storage/crm_migration.csv .npmrc
MIG/errors_sql.txt .phpdoc
public/js .zshrc
public/css .npm/
public/assets .composer/
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"] [submodule "client"]
path = clients/V2 path = client
url = https://ldc-web:phaeZ6ej@voight001.lesidc.com/crm-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"
}

View file

@ -1,7 +0,0 @@
{
"folders": [
{
"path": "."
}
]
}

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,61 +1,10 @@
<p align="center"><a href="https://laravel.com" target="_blank"><img src="https://raw.githubusercontent.com/laravel/art/master/logo-lockup/5%20SVG/2%20CMYK/1%20Full%20Color/laravel-logolockup-cmyk-red.svg" width="400"></a></p> # CRM
<p align="center"> ## Documentation
<a href="https://travis-ci.org/laravel/framework"><img src="https://travis-ci.org/laravel/framework.svg" alt="Build Status"></a> 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
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/dt/laravel/framework" alt="Total Downloads"></a>
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/v/laravel/framework" alt="Latest Stable Version"></a>
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/l/laravel/framework" alt="License"></a>
</p>
## About Laravel ## Tests unitaire
Laravel is a web application framework with expressive, elegant syntax. We believe development must be an enjoyable and creative experience to be truly fulfilling. Laravel takes the pain out of development by easing common tasks used in many web projects, such as: ### Comment lancer les tests unitaires ?
- [Simple, fast routing engine](https://laravel.com/docs/routing). [Suivre le wiki dédié au tests unitaires](https://lab.s-web.fr/ldc-web/projets/crm/-/wikis/Tests-unitaires)
- [Powerful dependency injection container](https://laravel.com/docs/container).
- Multiple back-ends for [session](https://laravel.com/docs/session) and [cache](https://laravel.com/docs/cache) storage.
- Expressive, intuitive [database ORM](https://laravel.com/docs/eloquent).
- Database agnostic [schema migrations](https://laravel.com/docs/migrations).
- [Robust background job processing](https://laravel.com/docs/queues).
- [Real-time event broadcasting](https://laravel.com/docs/broadcasting).
Laravel is accessible, powerful, and provides tools required for large, robust applications.
## Learning Laravel
Laravel has the most extensive and thorough [documentation](https://laravel.com/docs) and video tutorial library of all modern web application frameworks, making it a breeze to get started with the framework.
If you don't feel like reading, [Laracasts](https://laracasts.com) can help. Laracasts contains over 1500 video tutorials on a range of topics including Laravel, modern PHP, unit testing, and JavaScript. Boost your skills by digging into our comprehensive video library.
## Laravel Sponsors
We would like to extend our thanks to the following sponsors for funding Laravel development. If you are interested in becoming a sponsor, please visit the Laravel [Patreon page](https://patreon.com/taylorotwell).
### Premium Partners
- **[Vehikl](https://vehikl.com/)**
- **[Tighten Co.](https://tighten.co)**
- **[Kirschbaum Development Group](https://kirschbaumdevelopment.com)**
- **[64 Robots](https://64robots.com)**
- **[Cubet Techno Labs](https://cubettech.com)**
- **[Cyber-Duck](https://cyber-duck.co.uk)**
- **[Many](https://www.many.co.uk)**
- **[Webdock, Fast VPS Hosting](https://www.webdock.io/en)**
- **[DevSquad](https://devsquad.com)**
- **[OP.GG](https://op.gg)**
## Contributing
Thank you for considering contributing to the Laravel framework! The contribution guide can be found in the [Laravel documentation](https://laravel.com/docs/contributions).
## Code of Conduct
In order to ensure that the Laravel community is welcoming to all, please review and abide by the [Code of Conduct](https://laravel.com/docs/contributions#code-of-conduct).
## Security Vulnerabilities
If you discover a security vulnerability within Laravel, please send an e-mail to Taylor Otwell via [taylor@laravel.com](mailto:taylor@laravel.com). All security vulnerabilities will be promptly addressed.
## License
The Laravel framework is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT).

View file

@ -4,12 +4,17 @@ namespace App\Console\Commands;
use App\Models\Customer; use App\Models\Customer;
use App\Models\CustomerLogin; use App\Models\CustomerLogin;
use App\Models\CustomerOptInHistory;
use Carbon\Carbon; use Carbon\Carbon;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
/**
* @codeCoverageIgnore
*/
class DeleteUnconfirmedCustomers extends Command class DeleteUnconfirmedCustomers extends Command
{ {
/** /**
* The name and signature of the console command. * The name and signature of the console command.
* *
@ -28,25 +33,40 @@ class DeleteUnconfirmedCustomers extends Command
/** /**
* Execute the console command. * Execute the console command.
* *
* @return int * @return integer
*/ */
public function handle() public function handle(): int
{ {
$customersLogins = CustomerLogin::where([['status', 0], ['created_at', '<', Carbon::now()->subDay(1)]])->get(); /** @var \Illuminate\Database\Eloquent\Collection $customersLogins */
foreach ($customersLogins as $customerLogin) { $customersLogins = CustomerLogin::query()
// Si non activé et créer il y a plus d'un jour : Suppression ->where('status', false)
$customerLogin->customer->infos()->where('brand_id', $customerLogin->brand_id)->first()->delete(); ->where('created_at', '<', Carbon::now()->subDay(1))
// On récupère son id pour le cas ou on doit le supprimer complètement après ->with(['brand', 'customer'])
$customerId = $customerLogin->customer->id; ->get();
// Suppression de ses infos de login pour la marque $customersLogins->map(function (CustomerLogin $customerLogin) {
$customerLogin->delete(); Log::info($customerLogin->customer_id);
// Si le customer n'a plus d'info il n'a donc plus besoin de compte en crm DB::transaction(function () use ($customerLogin) {
if (count($customerLogin->customer->infos) === 0) { /** @var \App\Models\Customer $customer */
// Suppression du customer une fois tout le reste supprimé $customer = $customerLogin->customer;
$customer = Customer::find($customerId);
$customer->optInHistory()->delete(); /** @var \App\Models\Brand */
$customer->delete(); $brand = $customerLogin->brand;
}
} // * Suppression de ses infos de login pour la marque.
$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()) {
// * Suppression du customer une fois tout le reste supprimé.
$customer->optInHistory()->get()
->map(function (CustomerOptInHistory $customerOptInHistory) {
$customerOptInHistory->deleteOrFail();
});
$customer->deleteOrFail();
}
});
});
return 0;
} }
} }

View file

@ -4,20 +4,23 @@ namespace App\Console\Commands;
use App\Enums\ApiError; use App\Enums\ApiError;
use App\Enums\ApiStatus; use App\Enums\ApiStatus;
use App\Enums\ArcadiaMailType;
use App\Enums\ArcadiaOrderType; use App\Enums\ArcadiaOrderType;
use App\Enums\Country; use App\Enums\Country;
use App\Enums\QuestionType; use App\Enums\QuestionType;
use App\Exceptions\Api\V2\ApiErrorException; use App\Exceptions\Api\V4\ApiErrorException;
use App\Exceptions\Api\V2\CrmException; use App\Exceptions\Api\V4\CrmException;
use App\Traits\Enums\BaseEnum;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Illuminate\Support\Facades\File; use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
use Illuminate\Support\Str;
class GenerateCrmClient extends Command /**
* @codeCoverageIgnore
*/
class GenerateClient extends Command
{ {
private const TARGET = 'client/src';
private const TARGET = 'clients/V2/src';
private const CLIENTNAMESPACE = 'LDC\\LDCCRM'; private const CLIENTNAMESPACE = 'LDC\\LDCCRM';
/** /**
@ -32,22 +35,29 @@ class GenerateCrmClient extends Command
* *
* @var string * @var string
*/ */
protected $description = 'Generate the crm v2 client in clients/V2 folder'; protected $description = 'Generate the crm V4 client in client folder';
/** /**
* Execute the console command. * Execute the console command.
* *
* @return int * @return integer
*/ */
public function handle() public function handle(): int
{ {
$this->copyTraits();
$this->copyEnums(); $this->copyEnums();
$this->copyExceptions(); $this->copyExceptions();
$this->copyRoutesList(); $this->copyRoutesList();
$this->copyApiConfigInfo(); $this->copyApiConfigInfo();
return 0;
} }
private function copyApiConfigInfo() /**
* Copy config files
*
* @return void
*/
private function copyApiConfigInfo(): void
{ {
$this->info('Copy Api Config'); $this->info('Copy Api Config');
$rootTarget = app_path('../' . self::TARGET); $rootTarget = app_path('../' . self::TARGET);
@ -60,13 +70,18 @@ class GenerateCrmClient extends Command
); );
} }
private function copyRoutesList() /**
* Copy routes into a json file
*
* @return void
*/
private function copyRoutesList(): void
{ {
$this->info('Copy Routes'); $this->info('Copy Routes');
$routes = []; $routes = [];
/** @var \Illuminate\Routing\Route */ /** @var \Illuminate\Routing\Route */
foreach (Route::getRoutes() as $route) { foreach (Route::getRoutes() as $route) {
if (\strpos($route->getPrefix(), 'v2') !== false) { if (\strpos($route->getPrefix(), 'v4') !== false) {
$routes[$route->getName()] = [ $routes[$route->getName()] = [
'uri' => $route->uri(), 'uri' => $route->uri(),
'methods' => $route->methods() 'methods' => $route->methods()
@ -83,53 +98,84 @@ class GenerateCrmClient extends Command
); );
} }
/**
* Copy Exeption classes
*
* @return void
*/
private function copyExceptions(): void private function copyExceptions(): void
{ {
$this->info('Copy Exceptions'); $this->info('Copy Exceptions');
$rootTarget = app_path('../' . self::TARGET); $rootTarget = app_path('../' . self::TARGET);
$this->copyToFolder([ $classes = [
CrmException::class, CrmException::class,
ApiErrorException::class ApiErrorException::class
], 'Exceptions'); ];
$this->replaceInPhpFile( $this->copyToFolder($classes, 'Exceptions');
"$rootTarget/Exceptions/ApiErrorException.php", foreach ($classes as $class) {
'App\Enums\ApiError', $classBasename = class_basename($class);
'LDC\LDCCRM\Enums\ApiError' $dest = "$rootTarget/Exceptions/$classBasename.php";
); $this->replaceInPhpFile($dest, 'App\\', self::CLIENTNAMESPACE . '\\');
}
} }
/**
* Copy base enums classes
*
* @return void
*/
private function copyTraits(): void
{
$this->info('Copy Traits');
$rootTarget = app_path('../' . self::TARGET);
$classes = [
BaseEnum::class
];
$this->copyToFolder($classes, 'Traits');
foreach ($classes as $class) {
$classBasename = class_basename($class);
$dest = "$rootTarget/Traits/$classBasename.php";
$this->replaceInPhpFile($dest, 'App\\', self::CLIENTNAMESPACE . '\\');
}
}
/**
* Copy enums classes
*
* @return void
*/
private function copyEnums(): void private function copyEnums(): void
{ {
$this->info('Copy Enums'); $this->info('Copy Enums');
$classes = [ $rootTarget = app_path('../' . self::TARGET);
$classes = [
ApiError::class, ApiError::class,
ApiStatus::class, ApiStatus::class,
ArcadiaMailType::class,
ArcadiaOrderType::class, ArcadiaOrderType::class,
Country::class, Country::class,
QuestionType::class QuestionType::class
]; ];
$this->copyToFolder($classes, 'Enums'); $this->copyToFolder($classes, 'Enums');
$rootTarget = app_path('../' . self::TARGET);
foreach ($classes as $class) { foreach ($classes as $class) {
$classBasename = class_basename($class); $classBasename = class_basename($class);
$dest = "$rootTarget/Enums/$classBasename.php"; $dest = "$rootTarget/Enums/$classBasename.php";
$this->replaceInPhpFile($dest, 'BaseEnumRoutable', 'BaseEnum'); $this->replaceInPhpFile($dest, 'App\\', self::CLIENTNAMESPACE . '\\');
} }
} }
/** /**
* Copy Classes to specified folder * Copy Classes to specified folder
* *
* @param array $classes * @param array $classes
* @param string $folder * @param string $folder
* @return void * @return void
*/ */
private function copyToFolder(array $classes, string $folder): void private function copyToFolder(array $classes, string $folder): void
{ {
$rootTarget = app_path('../' . self::TARGET); $rootTarget = app_path('../' . self::TARGET);
$folderNamespace = \str_replace('/', '\\', $folder);
foreach ($classes as $class) { foreach ($classes as $class) {
$classFileName = \str_replace("App\\$folder\\", '', sprintf('%s.php', $class)); $classFileName = \str_replace("App\\$folderNamespace\\", '', sprintf('%s.php', $class));
$classFileName = \str_replace('\\', '/', $classFileName); $classFileName = \str_replace('\\', '/', $classFileName);
$classBasename = class_basename($class); $classBasename = class_basename($class);
$source = \app_path("$folder/$classFileName"); $source = \app_path("$folder/$classFileName");
@ -142,16 +188,28 @@ class GenerateCrmClient extends Command
} }
} }
/**
* Replaces a specific string with another on a file
*
* @param string $source
* @param string $search
* @param string $replace
* @return void
*/
private function replaceInPhpFile(string $source, string $search, string $replace): void private function replaceInPhpFile(string $source, string $search, string $replace): void
{ {
$fContent = File::get($source); $fContent = File::get($source);
// remove trailing \Api\V2 // Remove trailing \Api\V4 .
if ($pos = strpos($fContent, '\Api\V2')) { if ($pos = strpos($fContent, '\Api\V4')) {
$fContent = substr_replace($fContent, '', $pos, strlen('\Api\V2')); $fContent = substr_replace($fContent, '', $pos, strlen('\Api\V4'));
} }
// replace namespace top level // 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); $pos = strpos($fContent, $search);
if ($pos !== false) { if ($pos !== false) {
File::put($source, \str_replace($search, $replace, $fContent)); File::put($source, \str_replace($search, $replace, $fContent));

View file

@ -1,71 +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;
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 int
*/
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;
}
return 0;
}
}

View file

@ -5,9 +5,11 @@ namespace App\Console\Commands;
use App\Jobs\StartStatJobs as JobsStartStatJobs; use App\Jobs\StartStatJobs as JobsStartStatJobs;
use Illuminate\Console\Command; use Illuminate\Console\Command;
/**
* @codeCoverageIgnore
*/
class StartStatJobs extends Command class StartStatJobs extends Command
{ {
/** /**
* The name and signature of the console command. * The name and signature of the console command.
* *
@ -25,9 +27,9 @@ class StartStatJobs extends Command
/** /**
* Execute the console command. * Execute the console command.
* *
* @return int * @return integer
*/ */
public function handle() public function handle(): int
{ {
\dispatch(new JobsStartStatJobs())->afterResponse(); \dispatch(new JobsStartStatJobs())->afterResponse();
return 0; return 0;

View file

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

View file

@ -2,48 +2,204 @@
namespace App\Console\Commands; namespace App\Console\Commands;
use App\Enums\Users\RoleEnum;
use App\Models\User; use App\Models\User;
use Carbon\Carbon;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\ValidationException;
use Illuminate\Support\Str;
class UserCreate extends Command class UserCreate extends Command
{ {
/** /**
* The name and signature of the console command. * The name and signature of the console command.
* *
* @var string * @var string
*/ */
protected $signature = 'user:create'; protected $signature = 'uwil:user:create';
/** /**
* The console command description. * The console command description.
* *
* @var string * @var string
*/ */
protected $description = 'Command description'; protected $description = 'Create a backend admin user';
/**
* The first name of the new user.
*
* @var string|null
*/
protected $firstName;
/**
* The last name of the new user.
*
* @var string|null
*/
protected $lastName;
/**
* The email of the new user.
*
* @var string|null
*/
protected $email;
/**
* The role of the new user.
*
* @var string|null
*/
protected $role;
/** /**
* Execute the console command. * Execute the console command.
* *
* @return int * @return void
*/ */
public function handle() public function handle(): void
{ {
$admin = false; $this->firstName = null;
$this->addFirstName();
$firstname = $this->ask('What is the firstname?'); $this->lastName = null;
$lastname = $this->ask('What is the lastname?'); $this->addLastName();
$email = $this->ask('What is the email?');
$pwd = $this->secret('What is the password?');
$user = new User(); $this->email = null;
$user->firstname = $firstname; $this->addEmail();
$user->lastname = $lastname;
$user->email = $email;
$user->password = Hash::make($pwd);
$user->save();
$this->info('User created.'); $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

@ -2,13 +2,14 @@
namespace App\Console; namespace App\Console;
use App\Console\Commands\DeleteUnconfirmedCustomers;
use App\Jobs\SendTransferredToDigitalCountMail;
use App\Jobs\StartStatJobs; use App\Jobs\StartStatJobs;
use Illuminate\Console\Scheduling\Schedule; use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel; use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
class Kernel extends ConsoleKernel class Kernel extends ConsoleKernel
{ {
/** /**
* The Artisan commands provided by your application. * The Artisan commands provided by your application.
* *
@ -19,13 +20,16 @@ class Kernel extends ConsoleKernel
/** /**
* Define the application's command schedule. * Define the application's command schedule.
* *
* @param \Illuminate\Console\Scheduling\Schedule $schedule * @param \Illuminate\Console\Scheduling\Schedule $schedule
* @return void * @return void
*/ */
protected function schedule(Schedule $schedule) protected function schedule(Schedule $schedule): void
{ {
// stats job on night // @codeCoverageIgnoreStart
$schedule->job(StartStatJobs::class)->dailyAt('03:30'); $schedule->job(StartStatJobs::class)->dailyAt('03:30');
$schedule->job(SendTransferredToDigitalCountMail::class)->weekdays()->at('07:00');
$schedule->command(DeleteUnconfirmedCustomers::class)->dailyAt('05:30');
// @codeCoverageIgnoreEnd
} }
/** /**
@ -33,7 +37,7 @@ class Kernel extends ConsoleKernel
* *
* @return void * @return void
*/ */
protected function commands() protected function commands(): void
{ {
$this->load(__DIR__ . '/Commands'); $this->load(__DIR__ . '/Commands');

View file

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

View file

@ -2,108 +2,43 @@
namespace App\Enums; namespace App\Enums;
use Kwaadpepper\Enum\BaseEnumRoutable; use Uwil\LaravelCommon\Traits\Enums\BaseEnum;
/** enum ApiError: int
* This describes an API error.
*
* @method static self modelNotFound()
* The request model was not found (typically the customer)
*
* @method static self validationFailed()
* This means some data could not be validated,
* you may have to look into message to get the details
*
* @method static self customerExists()
* The customer already exists
*
* @method static self customerNotActivated()
* The customer account is not activated and cannot perform
* this action
*
* @method static self customerAlreadyActive()
* You have requested to activate a customer account,
* but it is already active
*
* @method static self singleOptinCustomerExists()
* You have requested to create a single optin for an email,
* but it already exists
*
* @method static self apiAuthFailed()
* The authentification to the api failed. You may have to
* check into config or .env file for your brand credentials
*
* @method static self authFailed()
* The authentification for this customer failed because of wrong credentials
*
* @method static self needAuth()
* This action requires to be authenticated
*
* @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
{ {
public static function values(): array use BaseEnum;
{
return [
'modelNotFound' => 101,
'validationFailed' => 102,
'customerExists' => 103,
'customerAlreadyActive' => 110,
'customerNotActivated' => 116,
'authFailed' => 117,
'needAuth' => 118,
'apiAuthFailed' => 119,
'singleOptinCustomerExists' => 120,
'methodNotAllowed' => 150,
'wrongTimeStamp' => 151,
'missingParameter' => 400,
'authorizationException' => 403,
'throttleRequestsException' => 429
];
}
public static function labels(): array case modelNotFound = 101;
{ case validationFailed = 102;
return [ case customerExists = 103;
'modelNotFound' => '%s introuvable.', case customerAlreadyActive = 110;
'validationFailed' => 'Impossible de valider les données', case customerNotActivated = 116;
'customerExists' => 'L\'utilisateur est déjà enregistré sur ce site', case authFailed = 117;
'customerNotActivated' => 'Le compte n\'est pas activé', case apiAuthFailed = 119;
'customerAlreadyActive' => 'Le compte est déjà activé', case singleOptinCustomerExists = 120;
'singleOptinCustomerExists' => 'Le single optin ne peut être ajouté car un compte existe.', case methodNotAllowed = 150;
'authFailed' => 'Echec de l\'authentification du compte', case wrongTimeStamp = 151;
'needAuth' => 'Vous devez être authentifié', case missingParameter = 400;
'apiAuthFailed' => 'Echec de l\'authentification client à l\'Api LDC-CRM pour ce service', case authorizationException = 403;
'methodNotAllowed' => 'This method is not allowed for this request', case throttleRequestsException = 429;
'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', * Optionnal labels definition
'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; namespace App\Enums;
use Kwaadpepper\Enum\BaseEnumRoutable; use Uwil\LaravelCommon\Traits\Enums\BaseEnum;
/** enum ApiStatus: int
* @method static self OK()
* @method static self KO()
*
* @package LDCCrmClient
* @subpackage enums
*/
class ApiStatus extends BaseEnumRoutable
{ {
use BaseEnum;
case OK = 1;
case KO = 2;
} }

View file

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

View file

@ -2,22 +2,12 @@
namespace App\Enums; namespace App\Enums;
use Kwaadpepper\Enum\BaseEnumRoutable; use Uwil\LaravelCommon\Traits\Enums\BaseEnum;
/** enum ArcadiaOrderType: int
* @method static self cb()
* @method static self check()
*
* @package LDCCrmClient
* @subpackage enums
*/
class ArcadiaOrderType extends BaseEnumRoutable
{ {
public static function values(): array use BaseEnum;
{
return [ case cb = 0;
'cb' => 0, case check = 1;
'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,32 +2,23 @@
namespace App\Enums; namespace App\Enums;
use Kwaadpepper\Enum\BaseEnumRoutable; use Uwil\LaravelCommon\Traits\Enums\BaseEnum;
/** enum CustomerExportType: int
* @method static self singleOptin()
* @method static self customer()
* @method static self both()
*
* @package LDCCrmClient
* @subpackage enums
*/
class CustomerExportType extends BaseEnumRoutable
{ {
public static function values(): array use BaseEnum;
{
return [ case singleOptin = 1;
'singleOptin' => 1, case customer = 2;
'customer' => 2, case both = 3;
'both' => 3
]; /**
} * Optionnal labels definition
public static function labels(): array */
{ private const LABELS = [
return [ self::singleOptin->name => 'Les single-optins uniquement',
'singleOptin' => trans('Les single optins uniquement'), self::customer->name => 'Les clients uniquement acceptant de recevoir les newsletters',
'customer' => trans('Les clients uniquement acceptant de recevoir les newsletters'), self::both->name => 'La totalité des single-optins ainsi que des clients acceptant
'both' => trans('La totalité des client acceptant de recevoir les newsletters') 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,24 +2,13 @@
namespace App\Enums; namespace App\Enums;
use Kwaadpepper\Enum\BaseEnumRoutable; use Uwil\LaravelCommon\Traits\Enums\BaseEnum;
/** enum QuestionType: int
* @method static self select()
* @method static self radio()
* @method static self checkbox()
*
* @package LDCCrmClient
* @subpackage enums
*/
class QuestionType extends BaseEnumRoutable
{ {
public static function values(): array use BaseEnum;
{
return [ case select = 1;
'select' => 1, case radio = 2;
'radio' => 2, case checkbox = 3;
'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,18 +10,17 @@ use Throwable;
/** /**
* This would be thrown if any of the case * This would be thrown if any of the case
* referenced in \App\Enums\ApiError would * referenced in \App\Enums\ApiError would
* occur within the crm * occur within the crm.
* *
* On the client an ApiErrorException would * On the client an ApiErrorException would
* also be throw with the ApiError corresponding to * also be throw with the ApiError corresponding to
* the situation * the situation.
* *
* @package LDCCrmClient * @package LDCCrmClient
* @subpackage exceptions * @subpackage exceptions
*/ */
class ApiErrorException extends Exception class ApiErrorException extends Exception
{ {
/** /**
* Api Error * Api Error
* This value makes the error more explicit. * This value makes the error more explicit.
@ -35,7 +34,7 @@ class ApiErrorException extends Exception
/** /**
* This is an explicit error message, * This is an explicit error message,
* ou may print this to the front or not depending * ou may print this to the front or not depending
* on $error value * on $error value.
* *
* @var string * @var string
*/ */
@ -46,26 +45,34 @@ class ApiErrorException extends Exception
* This value could be used to handle errors. * This value could be used to handle errors.
* But please take in consideration $error first * But please take in consideration $error first
* *
* @var int * @var integer
*/ */
protected $httpCode; 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) public function __construct(ApiError $error, int $httpCode = 400, string $message = '', Throwable $previous = null)
{ {
$this->error = $error; $this->error = $error;
$this->message = $message; $this->message = $message;
$this->httpCode = $httpCode; $this->httpCode = $httpCode;
parent::__construct( parent::__construct(
strlen($this->message) ? $this->message : $error->label, strlen($this->message) ? $this->message : $error->label(),
$error->value, $error->value(),
$previous $previous
); );
} }
/** /**
* Get the Api Error * Get the Api Error.
* *
* @return ApiError * @return \App\Enums\ApiError
*/ */
public function getApiError(): ApiError public function getApiError(): ApiError
{ {
@ -73,17 +80,25 @@ class ApiErrorException extends Exception
} }
/** /**
* Get the http code * Get the http code.
* *
* @return int * @return integer
*/ */
public function getStatusCode(): int public function getStatusCode(): int
{ {
return $this->httpCode; return $this->httpCode;
} }
public function report() /**
* 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())); Log::debug(sprintf('CRM API: %s', $this->getMessage()));
// @codeCoverageIgnoreEnd
} }
} }

View file

@ -15,7 +15,13 @@ use Illuminate\Support\Facades\Log;
*/ */
class CrmException extends Exception class CrmException extends Exception
{ {
public function report() /**
* 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())); 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,9 +4,10 @@ namespace App\Exceptions;
use App\Enums\ApiError; use App\Enums\ApiError;
use App\Enums\ApiStatus; use App\Enums\ApiStatus;
use App\Exceptions\Api\V2\ApiErrorException; use App\Exceptions\Api\V2\ApiErrorException as V2ApiErrorException;
use App\Exceptions\Api\V2\CrmException; use App\Exceptions\Api\V4\ApiErrorException as V4ApiErrorException;
use App\Http\Resources\Api\V2\ApiResponse as V2ApiResponse; use App\Http\Resources\Api\V2\ApiResponse as V2ApiResponse;
use App\Http\Resources\Api\V4\ApiResponse as V4ApiResponse;
use App\Lib\Helper; use App\Lib\Helper;
use Illuminate\Auth\Access\AuthorizationException; use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Auth\AuthenticationException; use Illuminate\Auth\AuthenticationException;
@ -15,13 +16,12 @@ use Illuminate\Http\Exceptions\ThrottleRequestsException;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException; use Illuminate\Validation\ValidationException;
use Kwaadpepper\ExceptionHandler\Exceptions\ExceptionHandler; use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException; use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException;
use Throwable;
class Handler extends ExceptionHandler class Handler extends ExceptionHandler
{ {
/** /**
* A list of the exception types that are not reported. * A list of the exception types that are not reported.
* *
@ -47,105 +47,129 @@ class Handler extends ExceptionHandler
* *
* @return void * @return void
*/ */
public function register() public function register(): void
{ {
// $this->reportable(function (\Throwable $e) {
if (app()->bound('sentry')) {
app('sentry')->captureException($e);
}
});
} }
/** /**
* Render an exception into an HTTP response. * Render an exception into an HTTP response.
* *
* @param \Illuminate\Http\Request $request * @param mixed $request
* @param \Throwable $e * @param \Throwable $e
* @return \Symfony\Component\HttpFoundation\Response * @return \Symfony\Component\HttpFoundation\Response
*
* @throws \Throwable
*/ */
// phpcs:ignore Generic.Metrics.CyclomaticComplexity.TooHigh public function render(mixed $request, \Throwable $e): Response
public function render($request, Throwable $e)
{ {
if (Helper::isRequestForApiV2($request) and $request->isJson()) { if ((Helper::isRequestForApiV2($request) || Helper::isRequestForApiV4($request)) and $request->isJson()) {
switch (get_class($e)) { return $this->handleApiException($request, $e);
case AuthenticationException::class: } //end if
/** @var AuthenticationException $e */ // @codeCoverageIgnoreStart
return $this->getReponse(
$request,
401,
ApiError::needAuth()->value,
sprintf('%s, %s', ApiError::needAuth()->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())
);
case ValidationException::class:
/** @var ValidationException $e */
return $this->getReponse(
$request,
$e->status,
ApiError::validationFailed()->value,
sprintf(ApiError::validationFailed()->label, $e->getMessage()),
['errors' => $e->errors()]
);
case ModelNotFoundException::class:
/** @var ModelNotFoundException $e */
return $this->getReponse(
$request,
404,
ApiError::modelNotFound()->value,
sprintf(
ApiError::modelNotFound()->label,
\array_slice(explode('\\', $e->getModel()), -1)[0]
)
);
case ApiErrorException::class:
/** @var ApiErrorException $e */
return $this->getReponse(
$request,
$e->getStatusCode(),
$e->getCode(),
$e->getMessage()
);
case ThrottleRequestsException::class:
/** @var ThrottleRequestsException $e */
return $this->getReponse(
$request,
429,
ApiError::throttleRequestsException()->value,
ApiError::throttleRequestsException()->label
);
case AuthorizationException::class:
/** @var AuthorizationException $e */
return $this->getReponse(
$request,
403,
ApiError::authorizationException()->value,
ApiError::authorizationException()->label
);
case CrmException::class:
default:
if (config('app.env') === 'testing') {
// phpcs:ignore Squiz.PHP.DiscouragedFunctions.Found
print_r((string)$e);
}
return $this->getReponse($request, 500, $e->getCode(), $e->getMessage());
}
}
return parent::render($request, $e); return parent::render($request, $e);
// @codeCoverageIgnoreEnd
} }
/** /**
* Generate an ApiV2 json response * Generate a json response for any apiV4 error case.
* *
* @param Request $request * @param \Illuminate\Http\Request $request
* @param integer $httpCode * @param \Throwable $e
* @param integer $apiCode * @return \Illuminate\Http\JsonResponse
* @param string $message */
* @return JsonResponse private function handleApiException(Request $request, \Throwable $e): JsonResponse
{
switch (get_class($e)) {
case AuthenticationException::class:
/** @var AuthenticationException $e */
return $this->getReponse(
$request,
401,
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())
);
case ValidationException::class:
/** @var ValidationException $e */
return $this->getReponse(
$request,
$e->status,
ApiError::validationFailed->value(),
sprintf(ApiError::validationFailed->label(), $e->getMessage()),
['errors' => $e->errors()]
);
case ModelNotFoundException::class:
/** @var ModelNotFoundException $e */
return $this->getReponse(
$request,
404,
ApiError::modelNotFound->value(),
sprintf(
ApiError::modelNotFound->label(),
\array_slice(explode('\\', $e->getModel()), -1)[0]
)
);
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(),
$e->getCode(),
$e->getMessage()
);
case ThrottleRequestsException::class:
/** @var ThrottleRequestsException $e */
return $this->getReponse(
$request,
429,
ApiError::throttleRequestsException->value(),
ApiError::throttleRequestsException->label()
);
case AuthorizationException::class:
/** @var AuthorizationException $e */
return $this->getReponse(
$request,
403,
ApiError::authorizationException->value(),
ApiError::authorizationException->label()
);
} //end switch
// @codeCoverageIgnoreStart
if (config('app.env') === 'testing') {
// phpcs:ignore Squiz.PHP.DiscouragedFunctions.Found
print_r((string) $e);
}
return $this->getReponse($request, 500, \intval($e->getCode()), $e->getMessage());
// @codeCoverageIgnoreEnd
}
/**
* Generate an ApiV4 json response.
*
* @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( private static function getReponse(
Request $request, Request $request,
@ -154,15 +178,15 @@ class Handler extends ExceptionHandler
string $message, string $message,
array $data = [] array $data = []
): JsonResponse { ): JsonResponse {
return (new V2ApiResponse( if (Helper::isRequestForApiV2($request)) {
(object)array_merge( return (new V2ApiResponse(
$data, ApiStatus::KO,
[ (object) array_merge($data, ['message' => $message, 'code' => $apiCode]),
'message' => $message, ))->toResponse($request)->setStatusCode($httpCode);
'code' => $apiCode }
] return (new V4ApiResponse(
), ApiStatus::KO,
ApiStatus::KO() (object) array_merge($data, ['message' => $message, 'code' => $apiCode]),
))->toResponse($request)->setStatusCode($httpCode); ))->toResponse($request)->setStatusCode($httpCode);
} }
} }

View file

@ -1,42 +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;
class AnswerController extends Controller
{
use CrudController {
index as crudIndex;
}
/** @var Log */
public static $modelClass = Answer::class;
public static $layout = 'layouts.app';
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;
}
public function index(CrudIndexRequest $request)
{
/** @var \Illuminate\View\View */
$view = $this->crudIndex($request);
if ($request->ajax()) {
return $view;
}
return view('answer.index', $view->getData());
}
}

View file

@ -1,331 +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;
class ArcadiaController extends Controller
{
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.']);
}
}
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'];
}
// phpcs:ignore Generic.Metrics.CyclomaticComplexity.TooHigh
private function checkRequiredFields($fields, $type)
{
$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'];
}
}
if (!empty($errors)) {
return ['status' => 'KO', 'code' => 203, 'message' => $errors];
}
return ['status' => 'OK'];
}
// phpcs:ignore Generic.Metrics.CyclomaticComplexity.TooHigh
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"));
}
$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,556 +0,0 @@
<?php
namespace App\Http\Controllers\Api\V1;
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;
class CustomerController extends Controller
{
// TODO : Voir pour les contraintes de mot de passe
/**
* Store a newly created resource in storage
* @param Request $request
* @return CustomerResource|StatusResource
*/
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;
$customerInfos->country = !empty($request->country) ?
filter_var($request->country, FILTER_SANITIZE_STRING) : null;
$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 int $id
* @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
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')) {
if (!empty($request->country)) {
$CustomerInfo->country = filter_var($request->country, FILTER_SANITIZE_STRING);
} else {
$CustomerInfo->country = null;
}
}
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,
'city' => $infos->city,
'postcode' => $infos->postcode,
'opt_in' => $infos->opt_in,
'hashcode' => $infos->hashcode,
'status' => $login->status,
]
]
);
}
$login->last_connection = now();
$customer->logins()->save($login);
return new CustomerResource($customer, $brandId);
}
// "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.']);
}
} else {
// "Ce compte n'existe pas dans la CRM"
return new StatusResource(['status' => 'KO', 'code' => 119, 'message' => 'User not found.']);
}
}
/**
* Change the password of the user for the brand
* @param Request $request
* @param $id
*/
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 $id
*/
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 $id
*/
// phpcs:disable Squiz.PHP.NonExecutableCode.Unreachable
public function delete(Request $request, $id)
{
die("Not implemented yet");
// TODO : A voir pour les stats, obuton dans le bo qui permet de faire les stats sur les customer puis supprimer
// 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.']);
}
}
/**
* Change the status of the opt-in for the customer on this brand
* @param Request $request
* @param $id
*/
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.']);
}
}
/**
* Remove opt-in for the customer on this brand
* @param Request $request
* @param $hashcode
*/
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.']);
}
}
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
// phpcs:ignore PSR2.ControlStructures.ControlStructureSpacing.SpacingAfterOpenBrace
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,31 +4,43 @@ namespace App\Http\Controllers\Api\V2;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Http\Requests\Api\V2\StoreCustomerAnswers; use App\Http\Requests\Api\V2\StoreCustomerAnswers;
use App\Models\Answer;
use App\Models\Customer; use App\Models\Customer;
use App\Models\Service; use App\Models\ModelAnswer;
use Illuminate\Http\JsonResponse; use App\Models\Question;
class AnswerController extends Controller class AnswerController extends Controller
{ {
/** /**
* Store the customer answers * Store the customer answers.
* *
* @param \App\Http\Requests\Api\V2\StoreCustomerAnswers $request * @param \App\Http\Requests\Api\V2\StoreCustomerAnswers $request
* @param \App\Models\Service $service * @param \App\Models\Customer $customer
* @param \App\Models\Customer $customer
* @return \Illuminate\Http\JsonResponse * @return \Illuminate\Http\JsonResponse
*/ */
// phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClassBeforeLastUsed
public function storeCustomerAnswers( public function storeCustomerAnswers(
StoreCustomerAnswers $request, StoreCustomerAnswers $request,
Service $service,
Customer $customer Customer $customer
): JsonResponse { ): \Illuminate\Http\JsonResponse {
// * Make sure we are accessing a customer that belongs to our service.
$this->authorize('customerBelongsToService', $customer); $this->authorize('customerBelongsToService', $customer);
$existingAnswerIds = $customer->getAnswers($service)->pluck('id');
$answersID = Answer::whereIn('id', $request->answers)->pluck('id'); $question = Question::find($request->validated('question'));
$customer->answers()->sync($answersID->merge($existingAnswerIds));
return \response()->json(); // 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

@ -2,40 +2,18 @@
namespace App\Http\Controllers\Api\V2; namespace App\Http\Controllers\Api\V2;
use App\Enums\ApiError;
use App\Exceptions\Api\V2\ApiErrorException;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Lib\Helper; use Illuminate\Http\JsonResponse;
use App\Models\Service;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
class ApiController extends Controller class ApiController extends Controller
{ {
public function ping() /**
* Just returns HTTP 200 response.
*
* @return \Illuminate\Http\JsonResponse
*/
public function ping(): JsonResponse
{ {
return response()->json(); return response()->json();
} }
public function authenticate(Request $request)
{
try {
$bearerToken = Helper::getBearerTokenFromRequest($request);
if (!$bearerToken) {
throw new ApiErrorException(ApiError::missingParameter());
}
/** @var \App\Models\Service */
$service = Service::getFromBearerToken($bearerToken);
} catch (ModelNotFoundException $e) {
throw new ApiErrorException(ApiError::apiAuthFailed());
}
if (!Hash::check($request->bearer_pwd, $service->bearer_pwd)) {
throw new ApiErrorException(ApiError::apiAuthFailed());
}
$apiToken = $service->generateApiToken();
$service->saveOrFail();
return response()->json(['api_token' => $apiToken]);
}
} }

View file

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

View file

@ -14,199 +14,247 @@ use App\Jobs\UpdateCustomerLastConnection;
use App\Models\Customer; use App\Models\Customer;
use App\Models\Service; use App\Models\Service;
use App\Models\SingleOptin; use App\Models\SingleOptin;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\ModelNotFoundException; use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
use Illuminate\Http\Resources\Json\JsonResource; use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Support\Facades\DB;
class CustomerController extends Controller class CustomerController extends Controller
{ {
/** /**
* Store a newly created resource in storage. * Creates a new customer for the specified service.
* *
* @param \App\Request\StoreCustomerRequest $request * @param \App\Http\Requests\Api\V2\StoreCustomerRequest $request
* @param \App\Models\Service $service * @param \App\Models\Service $service
* @return \Illuminate\Http\Resources\Json\JsonResource * @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 public function store(StoreCustomerRequest $request, Service $service): JsonResource
{ {
$data = (object)$request->validated(); $data = (object) $request->validated();
/** @var \App\Models\Customer */ /** @var \App\Models\Customer */
$customer = Customer::where('email', $data->email)->firstOrCreate([ $customer = Customer::query()->where('email', $data->email)->firstOrCreate([
'email' => $data->email 'email' => $data->email
]); ]);
$customer->saveOrFail();
$customer->setOptIn($service->brand, $data->opt_in);
// Delete Single optin if exists DB::transaction(function () use (&$customer, $data, $request, $service) {
if (
$singleOptin = SingleOptin::where('email', $data->email)
->where('brand_id', $service->brand->id)->first()
) {
$singleOptin->delete();
}
/** @var \App\Models\CustomerInfo */ $saved = $customer->save();
$customerInfo = $customer->info($service->brand, Customer::FIRST_OR_NEW); $customer->setOptIn($service->brand, $data->opt_in);
if ($customerInfo->exists) {
throw new ApiErrorException(ApiError::customerExists());
}
$customerInfo->fill($request->validated());
$customerInfo->brand()->associate($service->brand);
$customerInfo->customer()->associate($customer);
$customerInfo->saveOrFail();
/** @var \App\Models\CustomerLogin */ // * Delete Single optin if exists.
$customerLogin = $customer->login($service->brand, Customer::FIRST_OR_NEW); /** @var \App\Models\SingleOptin|null $singleOptin */
$customerLogin->fill($request->validated()); $singleOptin = SingleOptin::query()->where('email', $data->email)
$customerLogin->brand()->associate($service->brand); ->whereHas('brand', function (Builder $q) use ($service) {
$customerLogin->customer()->associate($customer); $q->where('id', $service->brand->id);
$customerLogin->saveOrFail(); })->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); return new CustomerResource($service->brand, $customer);
} }
/** /**
* Display the specified resource. * Renders the specific customer infos.
* *
* @param \App\Models\Service $service * @param \App\Models\Service $service
* @param \App\Models\Customer $customer * @param \App\Models\Customer $customer
* @return \Illuminate\Http\Resources\Json\JsonResource * @return \Illuminate\Http\Resources\Json\JsonResource
*/ */
public function show(Customer $customer, Service $service): 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); $this->authorize('customerBelongsToService', $customer);
return new CustomerResource($service->brand, $customer); return new CustomerResource($service->brand, $customer);
} }
/** /**
* Update the specified resource in storage. * Updates information for a specific customer.
* *
* @param \App\Request\UpdateCustomerRequest $request * @param \App\Http\Requests\Api\V2\UpdateCustomerRequest $request
* @param \App\Models\Service $service * @param \App\Models\Service $service
* @param \App\Models\Customer $customer * @param \App\Models\Customer $customer
* @return \Illuminate\Http\Resources\Json\JsonResource * @return \Illuminate\Http\Resources\Json\JsonResource
* @throws \App\Exceptions\Api\V2\CrmException If saving the customer to DB failed.
*/ */
public function update(UpdateCustomerRequest $request, Customer $customer, Service $service): JsonResource 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); $this->authorize('customerBelongsToService', $customer);
// To correct with create
$customer->fill($request->all());
if ($customer->saveOrFail() === false) {
throw new CrmException(sprintf('Could not update customer %d', $customer));
}
if ($request->has('opt_in')) {
$customer->setOptIn($service->brand, $request->opt_in);
}
/** @var \App\Models\CustomerInfo */ DB::transaction(function () use ($request, $customer, $service) {
$customerInfo = $customer->info($service->brand, Customer::FIRST_OR_FAIL); // To correct with create.
$customerInfo->fill($request->validated()); $customer->fill($request->validated());
$customerInfo->saveOrFail(); $saved = $customer->save();
if ($request->has('opt_in')) {
$customer->setOptIn($service->brand, $request->opt_in);
}
/** @var \App\Models\CustomerLogin */ /** @var \App\Models\CustomerInfo */
$customerLogin = $customer->login($service->brand, Customer::FIRST_OR_FAIL); $customerInfo = $customer->info($service->brand, Customer::FIRST_OR_FAIL);
$customerLogin->fill($request->validated()); $customerInfo->fill($request->validated());
$customerLogin->saveOrFail(); $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); return new CustomerResource($service->brand, $customer);
} }
/** /**
* Remove the specified resource from storage. * Deletes customer information for a specific service.
* *
* @param \App\Models\Service $service * @param \App\Models\Service $service
* @param \App\Models\Customer $customer * @param \App\Models\Customer $customer
* @return \Illuminate\Http\JsonResponse * @return \Illuminate\Http\JsonResponse
* @throws \App\Exceptions\Api\V2\CrmException If removing the customer off the DB failed.
*/ */
public function destroy(Customer $customer, Service $service): JsonResponse public function destroy(Service $service, Customer $customer): JsonResponse
{ {
// * Make sure we are accessing a customer that belongs to our service.
$this->authorize('customerBelongsToService', $customer); $this->authorize('customerBelongsToService', $customer);
// TODO : A voir pour les stats, obuton dans le bo qui permet de faire les stats sur les customer puis supprimer
// TODO : Gestion avec mesBR
/** @var \App\Models\CustomerInfo */ /** @var \App\Models\CustomerInfo */
$customerInfo = $customer->info($service->brand, Customer::FIRST_OR_FAIL); $customerInfo = $customer->info($service->brand, Customer::FIRST_OR_FAIL);
/** @var \App\Models\CustomerLogin */ /** @var \App\Models\CustomerLogin */
$customerLogin = $customer->login($service->brand, Customer::FIRST_OR_FAIL); $customerLogin = $customer->login($service->brand, Customer::FIRST_OR_FAIL);
if ( DB::transaction(function () use ($service, $customer, $customerLogin, $customerInfo) {
!$customerInfo->delete() || if (
!$customerLogin->delete() || $customerInfo->delete() === false ||
$customerLogin->delete() === false
) {
// @codeCoverageIgnoreStart
throw new CrmException('Api failed to delete customer.');
// @codeCoverageIgnoreEnd
};
$customer->optInHistory()->whereHas('brand', function ($q) use ($service) { $customer->optInHistory()->whereHas('brand', function ($q) use ($service) {
$q->where('id', $service->brand_id); $q->where('id', $service->brand_id);
})->delete() === false })->delete();
) {
throw new CrmException(sprintf(
'Could not delete customer %d for brand %d',
$customer->id,
$service->brand->id
));
};
$logins = $customer->logins()->count(); // * Force loading real relation counts.
$infos = $customer->infos()->count(); $customer->unsetRelation('logins');
$history = $customer->optInHistory()->count(); $customer->unsetRelation('infos');
$logins = $customer->logins()->count();
$infos = $customer->infos()->count();
// If customer is not associated with any brand then delete // If customer is not associated with any brand then delete.
if (!$logins and !$infos and !$history) { if (!$logins and !$infos) {
if ($customer->delete() === false) { if ($customer->delete() === false) {
throw new CrmException(sprintf( // @codeCoverageIgnoreStart
'Could not delete customer %d', throw new CrmException('Api failed to update customer.');
$customer->id // @codeCoverageIgnoreEnd
)); }
} }
} });
return response()->json(); return response()->json();
} }
/** /**
* Authenticate the user * Authenticate the user.
* @param \App\Models\Service $service *
* @param \App\Request\AuthenticateRequest $request * @param \App\Http\Requests\Api\V2\AuthenticateRequest $request
* @param \App\Models\Service $service
* @return \Illuminate\Http\Resources\Json\JsonResource * @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 public function authenticate(AuthenticateRequest $request, Service $service): JsonResource
{ {
try { try {
/** @var \App\Models\Customer */ /** @var \App\Models\Customer */
$customer = Customer::where('email', $request->email)->firstOrFail(); $customer = Customer::query()->where('email', $request->email)->firstOrFail();
$customer->loadBrandData($service->brand, true);
$customerLogin = $customer->logins->first(); /** @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) { } catch (ModelNotFoundException $e) {
throw new ApiErrorException(ApiError::authFailed()); throw new ApiErrorException(ApiError::authFailed);
} } //end try
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');
return new CustomerResource($service->brand, $customer);
} }
/** /**
* Activate the account of the user * Activate the account of the user.
* @param Customer $customer *
* @param Service $service * @param \App\Models\Customer $customer
* @param \App\Models\Service $service
* @return \Illuminate\Http\JsonResponse * @return \Illuminate\Http\JsonResponse
* @throws \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); $this->authorize('customerBelongsToService', $customer);
/** @var \App\Models\CustomerLogin $login */
$login = $customer->login($service->brand, Customer::FIRST_OR_FAIL); $login = $customer->login($service->brand, Customer::FIRST_OR_FAIL);
if (!$login->status) { if ($login->status) {
$login->status = true; throw new ApiErrorException(ApiError::customerAlreadyActive);
$login->save();
return response()->json();
} else {
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

@ -4,8 +4,11 @@ namespace App\Http\Controllers\Api\V2;
use App\Enums\ApiError; use App\Enums\ApiError;
use App\Exceptions\Api\V2\ApiErrorException; use App\Exceptions\Api\V2\ApiErrorException;
use App\Exceptions\Api\V2\CrmException;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Http\Requests\Api\V2\BaseApiRequest;
use App\Http\Requests\Api\V2\SingleOptinRequest; use App\Http\Requests\Api\V2\SingleOptinRequest;
use App\Http\Requests\Api\V2\SingleOptinRemoveWithHashcodeRequest;
use App\Models\CustomerInfo; use App\Models\CustomerInfo;
use App\Models\Service; use App\Models\Service;
use App\Models\SingleOptin; use App\Models\SingleOptin;
@ -13,28 +16,88 @@ use App\Models\SingleOptin;
class OptinController extends Controller class OptinController extends Controller
{ {
/** /**
* Store a newly created resource in storage. * Creates a new Single optin or destroys it if exists.
* *
* @param \App\Request\SingleOptinRequest $request * @param \App\Http\Requests\Api\V2\SingleOptinRequest $request
* @param \App\Models\Service $service * @param \App\Models\Service $service
* @return \Illuminate\Http\JsonResponse * @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()) { $validatedData = $request->validated();
throw new ApiErrorException(ApiError::singleOptinCustomerExists()); if (CustomerInfo::getFromEmail($service->brand, $validatedData['email'])->exists()) {
throw new ApiErrorException(ApiError::singleOptinCustomerExists);
} }
/** @var \App\Models\SingleOptin */ /** @var \App\Models\SingleOptin */
$singleOptin = SingleOptin::where('email', $request->email)->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) { if (!$this->changeOptin($service, $request, \boolval($validatedData['opt_in']), $singleOptin)) {
$singleOptin->fill($request->validated()); // @codeCoverageIgnoreStart
$singleOptin->brand()->associate($service->brand); throw new CrmException('Api failed to update single optin.');
$singleOptin->saveOrFail(); // @codeCoverageIgnoreEnd
} else {
$singleOptin->delete();
} }
if (isset($validatedData['answer_id'])) {
$singleOptin->answers()->attach($validatedData['answer_id']);
}
return response()->json(); 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
}
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

@ -14,46 +14,48 @@ use Illuminate\Http\Resources\Json\ResourceCollection;
class QuestionController extends Controller class QuestionController extends Controller
{ {
/** /**
* Display all specified resources. * Returns all service questions.
* *
* @param \App\Models\Service $service * @param \App\Models\Service $service
* @return \Illuminate\Http\Resources\Json\JsonResource * @return \Illuminate\Http\Resources\Json\ResourceCollection
*/ */
// phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClassAfterLastUsed
public function index(Service $service): ResourceCollection public function index(Service $service): ResourceCollection
{ {
return new QuestionsResource($service->brand->questions); return new QuestionsResource($service->brand->questions);
} }
/** /**
* Display all specified resources. * Returns all service questions with user answers for all of them.
* *
* @param \App\Models\Service $service * @param \App\Models\Service $service
* @return \Illuminate\Http\Resources\Json\JsonResource * @param \App\Models\Customer $customer
* @return \Illuminate\Http\Resources\Json\ResourceCollection
*/ */
// phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClassAfterLastUsed
public function indexWithResponse(Service $service, Customer $customer): ResourceCollection public function indexWithResponse(Service $service, Customer $customer): ResourceCollection
{ {
// * Make sure we are accessing a customer that belongs to our service.
$this->authorize('customerBelongsToService', $customer); $this->authorize('customerBelongsToService', $customer);
$customerAnswers = $customer->getAnswers($service); $customerAnswers = $customer->getAnswers($service);
return new QuestionsResource($service->brand->questions, $customerAnswers); return new QuestionsResource($service->brand->questions, $customerAnswers);
} }
/** /**
* Display all specified resource by customer. * Returns all service questions that was not answered by a user.
* *
* @param \App\Models\Service $service * @param \App\Models\Service $service
* @param \App\Models\Customer $customer * @param \App\Models\Customer $customer
* @return \Illuminate\Http\Resources\Json\JsonResource * @return \Illuminate\Http\Resources\Json\JsonResource
*/ */
// phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClassAfterLastUsed
public function unAnsweredBy(Service $service, Customer $customer): JsonResource public function unAnsweredBy(Service $service, Customer $customer): JsonResource
{ {
// * Make sure we are accessing a customer that belongs to our service.
$this->authorize('customerBelongsToService', $customer); $this->authorize('customerBelongsToService', $customer);
$questions = Question::whereHas('brand', function ($q) use ($service) {
$questions = Question::query()->whereHas('brand', function ($q) use ($service) {
$q->where('id', $service->brand->id); $q->where('id', $service->brand->id);
})->whereDoesntHave('answers', function ($q) use ($customer) { })->whereDoesntHave('answers', function ($q) use ($customer) {
$q->whereIn('id', Answer::whereHas('customers', function ($q) use ($customer) { $q->whereIn('id', Answer::query()->whereHas('customers', function ($q) use ($customer) {
$q->where('id', $customer->id); $q->where('id', $customer->id);
})->pluck('id')); })->pluck('id'));
})->with('answers')->get(); })->with('answers')->get();

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,30 +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;
class BrandController extends Controller
{
use CrudController {
index as crudIndex;
}
/** @var Brand */
public static $modelClass = Brand::class;
public static $layout = 'layouts.app';
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,14 +2,246 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Enums\Pagination\ItemsPerPaginationEnum;
use App\Enums\Theme\BootstrapThemeEnum;
use App\Http\Requests\Bo\PaginateModelsRequest;
use App\Http\Requests\Bo\ThemeRequest;
use App\Lib\Helpers\ToolboxHelper;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Foundation\Bus\DispatchesJobs;
use Illuminate\Foundation\Validation\ValidatesRequests; use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Routing\Controller as BaseController; use Illuminate\Routing\Controller as BaseController;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\Session;
use Illuminate\Support\Str;
class Controller extends BaseController class Controller extends BaseController
{ {
use AuthorizesRequests; use AuthorizesRequests;
use DispatchesJobs;
use ValidatesRequests; use ValidatesRequests;
/** @var integer $itemPerPagination */
public $jsonItemPerPagination = 5;
/**
* Build Search query on specified fields
* 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,231 +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 Exception;
use Illuminate\Database\Query\JoinClause;
use Illuminate\Http\Request;
use Illuminate\Support\Carbon;
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
{
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()->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 ($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 (
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'
));
}
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'
));
}
public function delete(Customer $customer, Brand $brand)
{
$nbBrand = $customer->infos()->count();
$customer->infos()->where('brand_id', $brand->id)->delete();
$customer->logins()->where('brand_id', $brand->id)->delete();
$customer->optInHistory()->where('brand_id', $brand->id)->delete();
if ($nbBrand === 1) {
$customer->delete();
}
return redirect()->route('customers.index')->with('success', "Compte supprimé.");
}
public function export()
{
$brands = Brand::all();
return view('customer.export', compact(
'brands'
));
}
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);
$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));
}
//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
$flush_threshold = 1000; //the flush value should depend on your CSV size.
$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,36 +0,0 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class LoginController extends Controller
{
public function index()
{
return view('login');
}
public function authenticate(Request $request)
{
// phpcs:ignore PSR2.ControlStructures.ControlStructureSpacing.SpacingAfterOpenBrace
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.");
}
}
public function logout()
{
Auth::logout();
return redirect()->route('login');
}
}

View file

@ -1,125 +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;
class QuestionController extends Controller
{
use CrudController {
index as crudIndex;
}
/** @var Question */
public static $modelClass = Question::class;
public static $layout = 'layouts.app';
public function index(CrudIndexRequest $request)
{
/** @var \Illuminate\View\View */
$view = $this->crudIndex($request);
if ($request->ajax()) {
return $view;
}
return view('question.index', $view->getData());
}
public function indexQuery(Builder $query): Builder
{
if (request()->brand) {
$query = $query->where('brand_id', '=', request()->brand);
}
return $query;
}
public function export()
{
$brands = Brand::all();
return view('question.export', compact('brands'));
}
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
];
}
}
$pagination = 2000;
$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
$flush_threshold = 1000; //the flush value should depend on your CSV size.
$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,46 +0,0 @@
<?php
namespace App\Http\Controllers;
use App\Http\Requests\GenerateApiToken;
use App\Mail\ServiceCredentialRenewed;
use App\Models\Service;
use Illuminate\Support\Facades\Mail;
use Kwaadpepper\CrudPolicies\Http\Requests\CrudIndexRequest;
use Kwaadpepper\CrudPolicies\Traits\CrudController;
class ServiceController extends Controller
{
use CrudController {
index as crudIndex;
}
/** @var Service */
public static $modelClass = Service::class;
public static $layout = 'layouts.app';
public function index(CrudIndexRequest $request)
{
/** @var \Illuminate\View\View */
$view = $this->crudIndex($request);
if ($request->ajax()) {
return $view;
}
return view('service.index', $view->getData());
}
// phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClassBeforeLastUsed
public function generateApiToken(GenerateApiToken $request, Service $service)
{
/** @var \App\Models\User $user */
$user = auth()->user();
$bearerToken = $service->generateBearerToken();
$bearerPwd = $service->bearer_pwd;
$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 : '$bearerToken' et mot de passe '$bearerPwd'");
}
}

View file

@ -1,38 +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;
class SingleOptinController extends Controller
{
use CrudController {
index as crudIndex;
}
/** @var SingleOptin */
public static $modelClass = SingleOptin::class;
public static $layout = 'layouts.app';
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());
}
public function indexQuery(Builder $query): Builder
{
if (request()->brand) {
$query = $query->where('brand_id', request()->brand);
}
return $query;
}
}

View file

@ -6,7 +6,6 @@ use Illuminate\Foundation\Http\Kernel as HttpKernel;
class Kernel extends HttpKernel class Kernel extends HttpKernel
{ {
/** /**
* The application's global HTTP middleware stack. * The application's global HTTP middleware stack.
* *
@ -15,13 +14,12 @@ class Kernel extends HttpKernel
* @var array * @var array
*/ */
protected $middleware = [ protected $middleware = [
\App\Http\Middleware\TrustHosts::class,
\App\Http\Middleware\TrustProxies::class, \App\Http\Middleware\TrustProxies::class,
\Fruitcake\Cors\HandleCors::class, \Illuminate\Http\Middleware\HandleCors::class,
\App\Http\Middleware\PreventRequestsDuringMaintenance::class, \App\Http\Middleware\PreventRequestsDuringMaintenance::class,
\Illuminate\Foundation\Http\Middleware\ValidatePostSize::class, \Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
\App\Http\Middleware\TrimStrings::class, \App\Http\Middleware\TrimStrings::class,
\App\Http\Middleware\XssSanitizer::class,
\Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class
]; ];
/** /**
@ -31,21 +29,27 @@ class Kernel extends HttpKernel
*/ */
protected $middlewareGroups = [ protected $middlewareGroups = [
'web' => [ 'web' => [
\Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
\App\Http\Middleware\EncryptCookies::class, \App\Http\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class, \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class, \Illuminate\Session\Middleware\StartSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class, \Illuminate\View\Middleware\ShareErrorsFromSession::class,
\App\Http\Middleware\VerifyCsrfToken::class, \App\Http\Middleware\VerifyCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class, \Illuminate\Routing\Middleware\SubstituteBindings::class,
\App\Http\Middleware\SecureHeaders::class
], ],
'api' => [ 'api' => [
'throttle:api', \Illuminate\Routing\Middleware\ThrottleRequests::class . ':api',
\App\Http\Middleware\JsonOnly::class, \App\Http\Middleware\JsonOnly::class,
\App\Http\Middleware\ApiResponse::class, \App\Http\Middleware\ApiResponse::class,
\App\Http\Middleware\ApiCheckTimeStamps::class, \App\Http\Middleware\ApiCheckTimeStamps::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class \Illuminate\Routing\Middleware\SubstituteBindings::class
], ],
'saml' => [
\App\Http\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
],
]; ];
/** /**
@ -56,15 +60,17 @@ class Kernel extends HttpKernel
* @var array * @var array
*/ */
protected $routeMiddleware = [ protected $routeMiddleware = [
'auth' => \App\Http\Middleware\Authenticate::class, 'auth' => \App\Http\Middleware\Authenticate::class,
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class, 'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class, 'auth.session' => \Illuminate\Session\Middleware\AuthenticateSession::class,
'can' => \Illuminate\Auth\Middleware\Authorize::class, 'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
'check-brand' => \App\Http\Middleware\CheckBrand::class, 'can' => \Illuminate\Auth\Middleware\Authorize::class,
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class, 'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class, 'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class,
'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class, 'signed' => \App\Http\Middleware\ValidateSignature::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class, 'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class, 'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
'htmlsanitize' => \App\Http\Middleware\HtmlSanitizer::class,
'logoutusers' => \App\Http\Middleware\LogoutUnpublishedUsers::class,
]; ];
} }

View file

@ -3,10 +3,10 @@
namespace App\Http\Middleware; namespace App\Http\Middleware;
use App\Enums\ApiError; use App\Enums\ApiError;
use App\Exceptions\Api\V2\ApiErrorException; use App\Exceptions\Api\V4\ApiErrorException;
use App\Lib\Helper; use App\Lib\Helper;
use Closure;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Carbon;
/** /**
* Check Http Header Timestamp with request timestamp * Check Http Header Timestamp with request timestamp
@ -18,25 +18,26 @@ class ApiCheckTimeStamps
/** /**
* Handle an incoming request. * Handle an incoming request.
* *
* @param \Illuminate\Http\Request $request * @param \Illuminate\Http\Request $request
* @param \Closure $next * @param \Closure $next
* @return mixed * @return mixed
* @throws \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 (!$request->hasHeader('X-API-TIMESTAMP') or !is_numeric($request->header('X-API-TIMESTAMP'))) { 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 (!$request->timestamp or !is_numeric($request->timestamp)) { if (
throw new ApiErrorException(ApiError::wrongTimeStamp()); Carbon::parse($request->timestamp, 'utc')
} ->isBefore(Carbon::now('UTC')->subSeconds(config('api.timestampTimeout', 10)))
$diff = intval($request->timestamp) - intval($request->header('X-API-TIMESTAMP')); ) {
$maxDiff = 120; throw new ApiErrorException(ApiError::wrongTimeStamp);
if (abs($diff) > $maxDiff) {
throw new ApiErrorException(ApiError::wrongTimeStamp());
} }
} }
return $next($request); return $next($request);
} }
} }

View file

@ -4,83 +4,61 @@ namespace App\Http\Middleware;
use App\Enums\ApiStatus; use App\Enums\ApiStatus;
use App\Http\Resources\Api\V2\ApiResponse as V2ApiResponse; use App\Http\Resources\Api\V2\ApiResponse as V2ApiResponse;
use App\Http\Resources\Api\V4\ApiResponse as V4ApiResponse;
use App\Lib\Helper; use App\Lib\Helper;
use App\Models\Service; use App\Models\Service;
use Closure; use Illuminate\Http\JsonResponse;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
class ApiResponse class ApiResponse
{ {
/** /**
* Handle an incoming request. * Handle an incoming request.
* *
* @param \Illuminate\Http\Request $request * @param \Illuminate\Http\Request $request
* @param \Closure $next * @param \Closure $next
* @return mixed * @return mixed
* @throws \ErrorException If the received response is not a JsonResponse.
*/ */
public function handle(Request $request, Closure $next) public function handle(Request $request, \Closure $next): mixed
{ {
/** @var \Illuminate\Http\JsonResponse $response */ /** @var \Illuminate\Http\JsonResponse $response */
$response = $next($request); $response = $next($request);
// Set no cache header if (\get_class($response) !== JsonResponse::class) {
// @codeCoverageIgnoreStart
throw new \ErrorException('All api responses should be json.');
// @codeCoverageIgnoreEnd
}
// Set no cache header.
$response->header('Cache-Control', 'No-Store'); $response->header('Cache-Control', 'No-Store');
$isApiV1Request = Helper::isRequestForApiV1($request); /** @var \App\Models\Service|null */
$isApiV2Request = Helper::isRequestForApiV2($request); if (\optional($service = app(Service::class))->id) {
$service->incrementStat();
if ($isApiV1Request) {
// Use patched v1 Auth mecanism (without bearer reimplementation)
$bearerToken = Helper::getBearerTokenFromRequest($request) ?? $request->api_token;
try {
$service = Service::getFromBearerToken($bearerToken);
$service->incrementStat();
} catch (ModelNotFoundException $e) {
Log::error(sprintf('Could not increment stats %s', $e->getMessage()));
}
}
if ($isApiV2Request) {
$service = null;
$bearerToken = Helper::getBearerTokenFromRequest($request);
try {
if ($bearerToken) {
$service = Service::getFromBearerToken($bearerToken);
}
} catch (ModelNotFoundException $e) {
try {
if (!$service and $request->api_token) {
$service = Service::getFromApiToken($request->api_token);
}
} catch (ModelNotFoundException $e) {
Log::error(sprintf('Could not increment stats %s', $e->getMessage()));
}
}
if ($service) {
$service->incrementStat();
}
}
if ($isApiV1Request) {
return $response;
} }
/** @var object $data */
$data = $response->getData(); $data = $response->getData();
// Is reponse is already a V2ApiResponse then just return it /** Check if it's V2 or V4 response. */
if ( if (
gettype($data) === "object" and
isset($data->status) and isset($data->status) and
isset($data->data) and isset($data->data) and
isset($data->version) and isset($data->version) and
in_array($data->status, [ApiStatus::OK()->value, ApiStatus::KO()->value]) in_array($data->status, [ApiStatus::OK->value(), ApiStatus::KO->value()])
) { ) {
/** Return direct ApiResponse */
return $response; return $response;
} }
/** @ var \App\Enums\ApiStatus */ $status = intval($response->getStatusCode() / 100) == 2 ? ApiStatus::OK : ApiStatus::KO;
$status = intval($response->getStatusCode() / 100) == 2 ? ApiStatus::OK() : ApiStatus::KO();
return $response->setData(new V2ApiResponse((object)$response->getData(), $status)); if (Helper::isRequestForApiV2($request)) {
return $response->setData(new V2ApiResponse($status, (object) $data));
}
return $response->setData(new V4ApiResponse($status, (object) $data));
} }
} }

View file

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

View file

@ -1,37 +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;
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 dfd' . \json_encode($request->all()));
}
try {
$brand = Service::getFromBearerToken($apiToken)->brand;
} catch (ModelNotFoundException $e) {
abort(401, 'Le token d\'authentification est incorrect ' . $e->getMessage());
}
$request->request->add(['brand_id' => $brand->id]);
return $next($request);
}
}

View file

@ -6,13 +6,10 @@ use Illuminate\Cookie\Middleware\EncryptCookies as Middleware;
class EncryptCookies extends Middleware class EncryptCookies extends Middleware
{ {
/** /**
* The names of the cookies that should not be encrypted. * The names of the cookies that should not be encrypted.
* *
* @var array * @var array
*/ */
protected $except = [ protected $except = [];
//
];
} }

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