Compare commits

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

203 commits

Author SHA1 Message Date
K Muller
5634d4d232 Merge branch 'trunk' 2026-10-15 09:12:44 +02:00
K Muller
9a1a07307b Merge branch 'develop' into trunk 2026-10-15 09:12:16 +02:00
Karl M'muller
8410447a29 Merge branch '134-restore-soft-delete' into 'develop'
feat: ✨ restore soft-deleted user on create

See merge request ldc-web/projets/support-web!138
2026-10-15 07:11:08 +00:00
Corentin Huet
f0fb7a26f3 feat: ✨ restore soft-deleted user on create 2026-10-08 15:31:11 +02:00
Noah Lancien
f55ca959b8 Merge branch 'trunk' 2026-10-07 15:30:35 +02:00
Noah Lancien
d622f543e9 Merge branch 'develop' into trunk 2026-10-07 15:29:51 +02:00
Noix
1f1c684008 Merge branch '133-export-file' into 'develop'
feat: ✨ change ExportTicketsState to Friday 14:00

See merge request ldc-web/projets/support-web!137
2026-10-07 13:20:45 +00:00
Corentin Huet
e8c5472d45 feat: ✨ change ExportTicketsState to Friday 14:00 2026-10-07 09:30:01 +02:00
Noah Lancien
4ff19d9277 Merge branch 'trunk' 2026-10-01 10:07:26 +02:00
Noah Lancien
7dfd80cf23 Merge branch 'develop' into trunk 2026-10-01 10:07:12 +02:00
Noix
e19d810a2f Merge branch '132-create-gitlab-issue' into 'develop'
feat: ✨ allow creating gitlab issues for mailing type

See merge request ldc-web/projets/support-web!136
2026-10-01 08:06:30 +00:00
Corentin Huet
ab998c975d feat: ✨ allow creating gitlab issues for mailing type 2026-09-01 16:51:04 +02:00
Vincent BRIDIER
9706922312 Merge branch 'trunk' 2026-08-27 20:56:21 +02:00
Vincent BRIDIER
ebb52b5968 Merge branch 'develop' into trunk 2026-08-27 20:56:07 +02:00
Vincent BRIDIER
6c783247a7 fix: Alpine www-data = 82 2026-08-27 20:54:40 +02:00
K Muller
6cf790feab Merge branch 'trunk' 2026-08-26 12:58:54 +02:00
K Muller
57c7613455 Merge branch 'develop' into trunk 2026-08-26 12:58:41 +02:00
Karl M'muller
eddeb4f7f6 Merge branch '128-archived-project' into 'develop'
fix: 🐛 add error message for archived project

See merge request ldc-web/projets/support-web!135
2026-08-26 10:58:10 +00:00
Karl M'muller
d9d1fea624 Merge branch '127-error-sentry' into 'develop'
fix: 🐛 update regex to match both issue and work items

See merge request ldc-web/projets/support-web!134
2026-08-26 10:57:57 +00:00
Corentin Huet
22d79cfb11 fix: 🐛 add error message for archived project 2026-08-25 13:16:11 +02:00
Corentin Huet
22bc49baab fix: 🐛 update regex to match both issue and work items 2026-08-24 17:50:16 +02:00
K Muller
adb2a8b21c Merge branch 'trunk' 2026-07-23 09:26:12 +02:00
K Muller
7246d7894d Merge branch 'develop' into trunk 2026-07-23 09:25:13 +02:00
Karl M'muller
10b75031e7 Merge branch 'issue-126' into 'develop'
Fix: 🐛 duplicate emails sent when a ticket's modif is changed

See merge request ldc-web/projets/support-web!133
2026-07-23 07:23:38 +00:00
Abdel LAMRANI
68a48fdf5a Fix: 🐛 duplicate emails sent when a ticket's modif is changed 2026-07-20 12:45:46 +02:00
Jeremy Munsch
a7debacd65
Merge branch 'trunk' 2026-06-26 15:36:53 +02:00
Jeremy Munsch
dd10d4ba3a
Merge branch 'develop' into trunk 2026-06-26 15:36:43 +02:00
Jeremy Munsch
a7bd6d6420
fixed: NotifyAdmin event listener 2026-06-26 15:36:34 +02:00
Jeremy Munsch
8cfc409d5c
Merge branch 'trunk' 2026-06-26 15:28:54 +02:00
Jeremy Munsch
bc7a1020bc
Merge branch 'develop' into trunk 2026-06-26 15:28:45 +02:00
Jeremy Munsch
e828c8bf66
fix: TicketCreated event 2026-06-26 15:28:31 +02:00
Jeremy Munsch
f28dbdd1df
Merge branch 'trunk' 2026-06-26 15:15:10 +02:00
Jeremy Munsch
965226fae6
Fixed ticket creation 2026-06-26 15:14:32 +02:00
Jeremy Munsch
bd9b7cd32e
fix: new ticket creation failed 2026-06-26 15:13:48 +02:00
Noah Lancien
8c67989526 Merge branch 'trunk' 2026-06-26 09:50:56 +02:00
Noah Lancien
2212b0ae08 Merge branch 'develop' into trunk 2026-06-26 09:50:41 +02:00
Noix
c2b25c3503 Merge branch 'brevo-fix' into 'develop'
fix: brevo in (content commit that were missed)

See merge request ldc-web/projets/support-web!132
2026-06-26 07:46:25 +00:00
Jeremy Munsch
f106c17588
fix: brevo in (content commit that were missed) 2026-06-26 09:33:50 +02:00
Noah Lancien
8e9d117918 Merge branch 'trunk' 2026-06-25 11:07:30 +02:00
Noah Lancien
0dbb483fc4 Merge branch 'develop' into trunk 2026-06-25 08:47:02 +02:00
Noah Lancien
980717f37c ci: swicth to docker multi-stage 2026-06-25 08:46:44 +02:00
Noah Lancien
020fcdaa03 Merge branch 'develop' into trunk 2026-06-25 08:37:04 +02:00
Noix
095f80eac1 Merge branch 'brevo-webhook-fix' into 'develop'
Brevo webhook fix Closes #106

See merge request ldc-web/projets/support-web!130
2026-06-25 06:36:32 +00:00
Noix
6a973703c4 Merge branch 'issue-124' into 'develop'
refactor: change ldc logo Closes #124

See merge request ldc-web/projets/support-web!131
2026-06-25 06:36:22 +00:00
Jeremy Munsch
9a853088b1
refactor: change ldc logo Closes #124 2026-06-24 17:22:58 +02:00
Jeremy Munsch
0ec3be521d
refactor: Fixes #106 create ticket automatically from brevo webhook manager 2026-06-24 16:46:12 +02:00
Jeremy Munsch
6854af708b
fix: fix project migrations 2026-06-24 13:22:36 +02:00
Noah Lancien
8f1c920e26 Merge branch 'trunk' 2026-05-18 10:28:44 +02:00
Noah Lancien
efe5e8ace6 Merge branch 'develop' into trunk 2026-05-18 10:28:29 +02:00
Noah Lancien
91f4773c60 chore: 📦 Added meow npm package 2026-05-18 10:28:21 +02:00
Noah Lancien
6b5b4ec619 Merge branch 'trunk' 2026-05-18 10:18:39 +02:00
Noah Lancien
4edd92e572 Merge branch 'develop' into trunk 2026-05-18 10:18:20 +02:00
Noix
da03fbe273 Merge branch '115-project-not-found' into 'develop'
fix: 🐛 handle 404 when gitlab project is not found

See merge request ldc-web/projets/support-web!128
2026-05-18 08:12:44 +00:00
Noah Lancien
b7f7b7c55d Merge branch 'trunk' 2026-04-29 16:28:13 +02:00
Noah Lancien
49d37a8fa6 Merge branch 'develop' into trunk 2026-04-29 16:18:44 +02:00
Noah Lancien
195a5fcb3e chore: arrow_up: Bump PHP 8.5 2026-04-29 16:18:25 +02:00
Noah Lancien
e0af4a243a Merge branch 'trunk' 2026-04-29 16:10:44 +02:00
Noah Lancien
0c4d376c22 Merge branch 'develop' into trunk 2026-04-29 16:10:11 +02:00
Noix
6fabb998ed Merge branch 'support-to-glab-labels' into 'develop'
fix: 🐛 update to-dispatch label to g-to-dispatch

See merge request ldc-web/projets/support-web!129
2026-04-29 14:09:36 +00:00
Corentin Huet
04bd156849 fix: 🐛 update to-dispatch label to g-to-dispatch 2026-04-28 15:16:20 +02:00
Corentin Huet
a43037d2f9 fix: 🐛 handle 404 when gitlab project is not found 2026-03-30 11:59:42 +02:00
Noah Lancien
ef57fe9fe5 Merge branch 'trunk' 2026-03-25 11:52:33 +01:00
Noah Lancien
62901a015e Merge branch 'develop' into trunk 2026-03-25 11:52:18 +01:00
Noah Lancien
cbabb40f83 Merge branch 'develop' into trunk 2026-02-26 12:10:44 +01:00
Noah Lancien
870986faa4 Merge branch 'develop' into trunk 2026-02-26 11:15:14 +01:00
Noah Lancien
33ea75a5ed Merge branch 'trunk' 2026-01-29 12:38:38 +01:00
Noah Lancien
3e09baf6f3 Merge branch 'develop' into trunk 2026-01-29 12:38:20 +01:00
Noah Lancien
d5f1891764 Merge branch 'trunk' 2026-01-19 09:39:57 +01:00
Noah Lancien
098e40576e Merge branch 'develop' into trunk 2026-01-19 09:39:06 +01:00
Noah Lancien
18b66793a0 Merge branch 'trunk' 2026-01-16 15:56:26 +01:00
Noah Lancien
7ee8a2be82 Merge branch 'develop' into trunk 2026-01-16 15:51:31 +01:00
Noah Lancien
87cc2dec86 Merge branch 'trunk' 2026-01-12 09:32:52 +01:00
Noah Lancien
ea41e19724 Merge branch 'develop' into trunk 2026-01-12 09:32:32 +01:00
Vincent BRIDIER
65772aeb1d Merge branch 'trunk' 2026-01-02 15:31:49 +01:00
Vincent BRIDIER
ea6ac998d8 Merge branch 'develop' into trunk 2026-01-02 15:31:35 +01:00
Noah Lancien
9bc3d9e5e0 Merge branch 'trunk' 2025-11-28 09:33:55 +01:00
Noah Lancien
f99dac8aab Merge branch 'develop' into trunk 2025-11-21 09:07:52 +01:00
Noah Lancien
3f4342f3bb Merge branch 'develop' into trunk 2025-11-20 15:54:16 +01:00
Noah Lancien
481c732f3e Merge branch 'trunk' 2025-10-30 12:32:32 +01:00
Noah Lancien
887837dcaf Merge branch 'develop' into trunk 2025-10-30 12:22:06 +01:00
K Muller
4bdcf60dfa Merge branch 'develop' into trunk 2025-10-23 10:32:31 +02:00
Noah Lancien
435d5626ee Merge branch 'trunk' 2025-10-09 11:50:24 +02:00
Noah Lancien
50ba8eaf8a Merge branch 'develop' into trunk 2025-10-09 11:50:12 +02:00
Noah Lancien
f2a65c85b0 Merge branch 'trunk' 2025-10-09 11:43:15 +02:00
Noah Lancien
1a2fe21212 Merge branch 'develop' into trunk 2025-10-09 11:42:45 +02:00
Noah Lancien
d702c02bdb Merge branch 'trunk' 2025-09-18 08:59:45 +02:00
Noah Lancien
b1a6dfb72c Merge branch 'develop' into trunk 2025-09-18 08:58:20 +02:00
Vincent BRIDIER
844928e945 Merge branch 'trunk' 2025-08-22 12:51:32 +02:00
Vincent BRIDIER
3560e6c7f6 Merge branch 'develop' into trunk 2025-08-22 10:28:08 +02:00
Vincent BRIDIER
aa3f507483
Merge branch 'trunk' 2025-08-18 14:16:25 +02:00
Vincent BRIDIER
8a34a30ab4
Merge branch 'develop' into trunk 2025-08-18 14:16:17 +02:00
Vincent BRIDIER
4a0c556937
Merge branch 'trunk' 2025-08-18 14:03:06 +02:00
Vincent BRIDIER
085028a465
Merge branch 'develop' into trunk 2025-08-18 12:40:48 +02:00
Vincent BRIDIER
f9ed83ff60
Merge branch 'develop' into trunk 2025-08-18 12:13:24 +02:00
Noah Lancien
1ffb5e5bdd Merge branch 'trunk' 2025-07-10 14:54:56 +02:00
Noah Lancien
c6fcd7cfa6 Merge branch 'develop' into trunk 2025-07-10 13:05:54 +02:00
Noah Lancien
3d81cd77ce Merge branch 'trunk' 2025-07-09 15:31:20 +02:00
Noah Lancien
fba178b598 Merge branch 'develop' into trunk 2025-07-09 15:23:22 +02:00
Noah Lancien
1f02f86610 Merge branch 'trunk' 2025-06-24 10:31:22 +02:00
Noah Lancien
efb3914e4f Merge branch 'develop' into trunk 2025-06-24 10:31:03 +02:00
Noah Lancien
b0f4c37153 Merge branch 'trunk' 2025-06-19 12:09:03 +02:00
Noah Lancien
25ab578c93 Merge branch 'develop' into trunk 2025-06-19 12:01:08 +02:00
Noah Lancien
52386bbfd4 Merge branch 'develop' into trunk 2025-06-19 11:48:17 +02:00
Noah Lancien
d6ed7c1f04 Merge branch 'trunk' 2025-06-16 12:00:47 +02:00
Noah Lancien
5477affb97 Merge branch 'develop' into trunk 2025-06-16 11:52:26 +02:00
Vincent BRIDIER
3e199d0f68
Merge branch 'trunk' 2025-05-27 10:30:17 +02:00
Vincent BRIDIER
7564600ace
Merge branch 'develop' into trunk 2025-05-27 10:30:05 +02:00
Vincent BRIDIER
ba6f7f8e7b
Merge branch 'trunk' 2025-05-12 15:11:13 +02:00
Vincent BRIDIER
f1eddb1cc2
Merge branch 'develop' into trunk 2025-05-12 15:10:59 +02:00
Vincent BRIDIER
658fedd3eb
Merge branch 'trunk' 2025-05-12 11:42:21 +02:00
Vincent BRIDIER
31510149af
Merge branch 'develop' into trunk 2025-05-12 11:41:46 +02:00
Noah Lancien
fb8720d41b Merge branch 'trunk' 2025-04-24 15:02:27 +02:00
Noah Lancien
08afe201f8 Merge branch 'develop' into trunk 2025-04-24 15:02:07 +02:00
Noah Lancien
afd8f09d02 Merge branch 'trunk' 2025-04-23 12:11:56 +02:00
Noah Lancien
33cf4f1c93 Merge branch 'develop' into trunk 2025-04-23 12:11:30 +02:00
Vincent BRIDIER
e1944924c1
Merge branch 'trunk' 2025-04-18 10:28:51 +02:00
Vincent BRIDIER
3a0cc47fa8
Merge branch 'develop' into trunk 2025-04-18 10:14:27 +02:00
Vincent BRIDIER
3a2b5a0af9
Merge branch 'develop' into trunk 2025-04-18 09:59:53 +02:00
Noah Lancien
ca7057ce77 Merge branch 'develop' into trunk 2025-04-08 14:11:31 +02:00
Noah Lancien
0dd283b68d Merge branch 'trunk' 2025-04-07 15:01:11 +02:00
Noah Lancien
7c028a4505 Merge branch 'develop' into trunk 2025-04-07 15:00:52 +02:00
Karl Muller
b21dd4d260 Merge branch 'trunk' 2025-03-07 14:27:01 +00:00
Karl Muller
4838b54b4c Merge branch 'develop' into trunk 2025-03-07 14:26:49 +00:00
Karl Muller
911ee5df4b Merge branch 'trunk' 2025-03-07 14:10:23 +00:00
Karl Muller
bbfd4360f2 Merge branch 'develop' into trunk 2025-03-07 14:09:58 +00:00
Vincent BRIDIER
3a51ccb0f3
Merge branch 'trunk' 2025-03-04 09:58:05 +01:00
Vincent BRIDIER
1c3f20782b
Merge branch 'develop' into trunk 2025-03-04 09:57:56 +01:00
Noah Lancien
673494aeda Merge branch 'trunk' 2025-02-20 14:41:26 +01:00
Noah Lancien
1bd04e2e52 Merge branch 'develop' into trunk 2025-02-20 14:41:11 +01:00
Noah Lancien
aa17aab45f Merge branch 'trunk' 2025-02-14 13:42:22 +01:00
Noah Lancien
ec47d2610f Merge branch 'develop' into trunk 2025-02-14 13:42:08 +01:00
Noah Lancien
560ef30cd8 Merge branch 'trunk' 2025-02-14 13:31:00 +01:00
Noah Lancien
b2908aac9f Merge branch 'develop' into trunk 2025-02-14 13:30:20 +01:00
Karl Muller
0da4a6fa13 Merge branch 'trunk' 2025-01-13 13:56:41 +00:00
Karl Muller
b6720bae3d Merge branch 'develop' into trunk 2025-01-13 13:46:09 +00:00
Karl Muller
a58bf2ff3b fix: change manifest for cron issue 2025-01-13 13:45:04 +00:00
Noah Lancien
2b94d780b6 Merge branch 'trunk' 2024-11-26 09:18:29 +01:00
Noah Lancien
74edc5c785 Merge branch 'develop' into trunk 2024-11-26 09:17:42 +01:00
Noah Lancien
781161c5e4 Merge branch 'trunk' 2024-11-25 17:19:55 +01:00
Noah Lancien
2d70ff9f29 Merge branch 'develop' into trunk 2024-11-25 17:18:41 +01:00
Noah Lancien
17a01e1f7a Merge branch 'trunk' 2024-11-25 17:12:45 +01:00
Karl Muller
e3d828a9e5 Merge branch 'develop' into trunk 2024-11-25 15:50:32 +00:00
Noah Lancien
d17678342e Merge branch 'trunk' 2024-11-12 14:32:29 +01:00
Noah Lancien
b646cb41fe Merge branch 'develop' into trunk 2024-11-12 14:07:35 +01:00
Noah Lancien
5bd7673a18 Merge branch 'trunk' 2024-07-31 12:21:21 +02:00
Noah Lancien
2faf043f68 Merge branch 'develop' into trunk 2024-07-31 12:12:17 +02:00
Vincent BRIDIER
66c889b02a
Merge branch 'trunk' 2024-07-26 15:33:24 +02:00
Vincent BRIDIER
74600ef3a7
Merge branch 'develop' into trunk 2024-07-26 15:06:54 +02:00
Vincent BRIDIER
35e18112b9
Merge branch 'develop' into trunk 2024-07-25 17:29:35 +02:00
Vincent BRIDIER
f10b46a012
Merge branch 'trunk' 2024-07-20 23:18:59 +02:00
Vincent BRIDIER
523715a3c1
Merge branch 'develop' into trunk 2024-07-20 23:01:04 +02:00
Vincent BRIDIER
c8b4f97c88
Merge branch 'trunk' 2024-07-10 15:23:32 +02:00
Vincent BRIDIER
65b92edc99
Merge branch 'develop' into trunk 2024-07-10 15:23:17 +02:00
Thibault Raquet
2ee245be4c Merge branch 'trunk' into 'master'
Trunk

See merge request ldc-web/projets/support-web!86
2024-06-27 08:58:58 +00:00
Thibault Raquet
766a990361 Merge branch 'develop' into 'trunk'
Develop

See merge request ldc-web/projets/support-web!85
2024-06-27 08:58:33 +00:00
Vincent BRIDIER
a23509fa09
Merge branch 'trunk' 2024-06-11 10:22:48 +02:00
Vincent BRIDIER
6c8cc25bae
Merge branch 'develop' into trunk 2024-06-11 10:07:26 +02:00
Vincent BRIDIER
3c529a7bc0
Merge branch 'trunk' 2024-02-05 12:17:39 +01:00
Vincent BRIDIER
c8d179e7b3
Merge branch 'develop' into trunk 2024-02-05 12:09:45 +01:00
Vincent BRIDIER
8e4d6d4198
Merge branch 'develop' into trunk 2024-02-02 11:34:37 +01:00
Vincent BRIDIER
a3c2697c86
Merge branch 'trunk' 2024-01-31 18:03:13 +01:00
Vincent BRIDIER
c6d89101b4
Merge branch 'develop' into trunk 2024-01-31 17:55:33 +01:00
Vincent BRIDIER
ad43378b52
Merge branch 'trunk' 2023-10-13 15:23:19 +02:00
Vincent BRIDIER
f40f2e2aa5
Merge branch 'develop' into trunk 2023-10-13 15:16:29 +02:00
Vincent BRIDIER
b55d812aaa Merge branch 'develop' into trunk 2023-10-12 16:55:19 +02:00
Vincent BRIDIER
ab08bad87e Merge branch 'trunk' 2023-10-12 11:47:11 +02:00
Vincent BRIDIER
e8b920a8cd Merge branch 'develop' into trunk 2023-10-12 11:18:37 +02:00
Vincent BRIDIER
cb779c83b8
Merge branch 'trunk' 2023-10-09 15:09:26 +02:00
Vincent BRIDIER
a393b24392
Merge branch 'develop' into trunk 2023-10-04 11:38:28 +02:00
Vincent BRIDIER
cd89f6b443 Merge branch 'trunk' 2023-08-24 16:21:39 +02:00
Vincent BRIDIER
d9b316eae4 Merge branch 'develop' into trunk 2023-08-24 16:01:54 +02:00
Vincent BRIDIER
722369cf61 Merge branch 'develop' into trunk 2023-08-24 12:37:14 +02:00
Vincent BRIDIER
0cc315d2ef Merge branch 'develop' into trunk 2023-08-24 12:35:08 +02:00
Vincent BRIDIER
7559a47044
Merge branch 'trunk' 2023-08-18 12:37:30 +02:00
Vincent BRIDIER
55fef3421e
Merge branch 'develop' into trunk 2023-08-18 12:37:14 +02:00
Vincent BRIDIER
97d8111f4b
Merge branch 'trunk' 2023-08-17 11:21:13 +02:00
Vincent BRIDIER
4354f6a445
Merge branch 'develop' into trunk 2023-08-17 11:18:26 +02:00
Vincent BRIDIER
abdbbae843
Merge branch 'develop' into trunk 2023-08-16 12:24:45 +02:00
Vincent BRIDIER
f150e96d39
Merge branch 'develop' into trunk 2023-08-16 10:30:48 +02:00
Vincent BRIDIER
d2324145a4
Merge branch 'develop' into trunk 2023-08-14 11:24:22 +02:00
Vincent BRIDIER
b6c74efd3d
Merge branch 'trunk' 2023-07-20 15:31:37 +02:00
Vincent BRIDIER
57a437db48
Merge branch 'develop' into trunk 2023-07-20 15:31:24 +02:00
Vincent BRIDIER
f3cb8e7974
Merge branch 'trunk' 2023-06-30 09:14:08 +02:00
Vincent BRIDIER
91c1fe5532
Merge branch 'develop' into trunk 2023-06-30 09:13:52 +02:00
Vincent BRIDIER
29a837ff61
Merge branch 'trunk' 2023-06-29 10:12:31 +02:00
Vincent BRIDIER
bbe4478c09
Merge branch 'develop' into trunk 2023-06-29 10:12:14 +02:00
Vincent BRIDIER
28ee9f593d
Merge branch 'trunk' 2023-06-22 14:58:09 +02:00
Vincent BRIDIER
eb2d4e5267
Merge branch 'develop' into trunk 2023-06-22 14:57:42 +02:00
Vincent BRIDIER
c19d5829e6
Merge branch 'trunk' 2023-04-25 11:35:01 +02:00
Vincent BRIDIER
c1fb743e34
Merge branch 'develop' into trunk 2023-04-25 11:34:24 +02:00
Vincent BRIDIER
1d07c93799
Merge branch 'trunk' 2023-04-18 17:27:35 +02:00
Vincent BRIDIER
32cb11622c
Merge branch 'develop' into trunk 2023-04-18 17:27:21 +02:00
Vincent BRIDIER
911d7a2f6d
Merge branch 'trunk' 2023-04-17 17:21:58 +02:00
Vincent BRIDIER
32be1746b5
Merge branch 'develop' into trunk 2023-04-17 16:22:06 +02:00
Vincent BRIDIER
a5d4c58604
Merge branch 'develop' into trunk 2023-04-12 15:47:13 +02:00
Vincent BRIDIER
0d6f34b05a
Merge branch 'trunk' 2022-12-15 09:10:57 +01:00
Vincent BRIDIER
606ca09608
Merge branch 'develop' into trunk 2022-12-15 09:10:38 +01:00
Jérémy Munsch
6523875860
Merge branch 'trunk' 2022-11-04 17:04:58 +01:00
Jérémy Munsch
83b7e42b90
Merge branch 'develop' into trunk 2022-11-04 17:03:49 +01:00
Jérémy Munsch
6f20a6b8db
Merge branch 'develop' into trunk 2022-10-13 10:15:36 +02:00
Jérémy Munsch
88f3da4c92
Merge branch 'hotfix' 2022-10-12 14:22:46 +02:00
Vincent BRIDIER
bd3133f06c
Merge branch 'trunk' 2022-10-11 12:06:30 +02:00
Vincent BRIDIER
bb2cbc5ad8
Merge branch 'develop' into trunk 2022-10-11 12:06:06 +02:00
75 changed files with 7913 additions and 1795 deletions

View file

@ -7,7 +7,7 @@ include:
file: 'security/security.yml'
- project: 'ldc-web/shared-ci'
ref: 'main'
file: 'build/build.yml'
file: "build/docker-multistage.yml"
- project: 'ldc-web/shared-ci'
ref: 'main'
file: 'test/test.yml'

View file

@ -1,5 +1,43 @@
FROM registry.s-web.fr/ldc-web/projets/ldc-web-images-docker/ldcweb-production-8.3:170625-101047
COPY --chown=www-data:www-data . /app/srcs
FROM composer:2.9.2 AS vendor
ARG GITLAB_USER
ARG GITLAB_PASSWORD
COPY database/ database/
COPY composer.json composer.json
COPY composer.lock composer.lock
RUN composer config gitlab-token.lab.s-web.fr ${GITLAB_USER} ${GITLAB_PASSWORD}
RUN composer install \
--ignore-platform-reqs \
--no-interaction \
--no-plugins \
--no-scripts \
--no-dev \
--prefer-dist
FROM 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 @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
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 www-data
USER ${USER}

4633
_ide_helper.php Normal file

File diff suppressed because it is too large Load diff

View file

@ -2,16 +2,17 @@
namespace App\Console\Commands;
use Carbon\Carbon;
use App\Models\Ticket;
use App\Enums\Tickets\TicketStatusEnum;
use App\Models\GitlabIssues;
use App\Models\GitlabLabels;
use App\Models\GitlabProjects;
use Illuminate\Console\Command;
use App\Models\GitlabMilestones;
use Illuminate\Support\Facades\Cache;
use App\Enums\Tickets\TicketStatusEnum;
use App\Models\GitlabProjects;
use App\Models\Ticket;
use Carbon\Carbon;
use GrahamCampbell\GitLab\Facades\GitLab;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;
class UpdateGitlabDatas extends Command
{
@ -226,10 +227,36 @@ class UpdateGitlabDatas extends Command
$page = 1;
do {
$milestones = GitLab::milestones()->all($projectId, [
'per_page' => 100,
'page' => $page
]);
$milestones = [];
try {
$milestones = GitLab::milestones()->all($projectId, [
'per_page' => 100,
'page' => $page
]);
} catch (\Exception $e) {
if ($e->getCode() === 404) {
if ($project->delete()) {
Log::warning(sprintf(
"Project not found (%s), it has been deleted.",
$projectId
));
} else {
Log::warning(sprintf(
"Project not found (%s).",
$projectId
));
}
} else {
Log::error(sprintf(
'API Error for project %s, Message: %d',
$projectId,
$e->getMessage()
));
report($e);
}
}
$allMilestones = array_merge($allMilestones, $milestones);
$page++;
} while (!empty($milestones));

View file

@ -25,7 +25,7 @@ class Kernel extends ConsoleKernel
*/
protected function schedule(Schedule $schedule)
{
$schedule->job(ExportTicketsState::class)->weekly()->mondays()->at('07:00');
$schedule->job(ExportTicketsState::class)->weekly()->fridays()->at('14:00');
$schedule->job(AutoCloseOldTickets::class)->dailyAt('08:00');
$schedule->job(WarnBeforeAutoCloseTicket::class)->dailyAt('08:05');
$schedule->command('app:update-gitlab-datas --is-cron')->hourly()->between('7:00', '19:00');

View file

@ -0,0 +1,13 @@
<?php
declare(strict_types=1);
namespace App\Dto;
final readonly class BrevoWebHookDto
{
public function __construct(
public readonly string $plainTextContent,
public readonly ?string $brand
) {}
}

View file

@ -0,0 +1,30 @@
<?php
namespace App\Events;
use Domain\Models\ValueObjects\Email;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Foundation\Events\Dispatchable;
readonly class BrevoWebhookCallReceived
{
use Dispatchable;
public function __construct(
public Email $from,
public Email $to,
public string $htmlRawContent
) {}
/**
* Get the channels the event should broadcast on.
*
* @return array<int, \Illuminate\Broadcasting\Channel>
*/
public function broadcastOn(): array
{
return [
new PrivateChannel('default'),
];
}
}

View file

@ -0,0 +1,34 @@
<?php
namespace App\Events;
use App\Models\Site;
use App\Models\Ticket;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class TicketCreated implements ShouldQueue
{
use Dispatchable;
use SerializesModels;
public function __construct(
public readonly Site $siteReceivingTicket,
public readonly Ticket $ticket,
public readonly string $byUserName
) {}
/**
* Get the channels the event should broadcast on.
*
* @return array<int, \Illuminate\Broadcasting\Channel>
*/
public function broadcastOn(): array
{
return [
new PrivateChannel('default'),
];
}
}

View file

@ -1,77 +0,0 @@
<?php
namespace App\Http\Controllers\Bo;
use Carbon\Carbon;
use App\Models\Site;
use App\Models\Ticket;
use App\Mail\NewTicket;
use App\Http\Controllers\Controller;
use Illuminate\Support\Facades\Mail;
use App\Enums\Tickets\TicketTypeEnum;
use App\Enums\Tickets\TicketStatusEnum;
use App\Http\Requests\Bo\BrevoWehbookRequest;
class BrevoController extends Controller
{
public function createTicket(BrevoWehbookRequest $request)
{
$data = [];
$site = Site::where('name', 'Mailing en masse')->firstOrFail();
$formContent = $request->items[0]["ExtractedMarkdownMessage"];
$tblRegexPatterns = [
'user' => '/Matricule et Nom du demandeur\s*\|\s*([^|]+?)\s*\|/i',
'email' => '/Email du demandeur\s*\|\s*([^|]+?)\s*\|/i',
'brand' => '/Marque\s*\/\s*Filiale\s*\|\s*([^|]+?)\s*\|/i',
'subject' => '/Pour quels besoins avez-vous besoin d\'envoyer des emails en masse\s*\?\s*\|\s*([^|]+?)\s*\|/is',
'date_mep' => '/Date de première envoi souhaité\s*\|\s*([^|]+?)\s*\|/i',
'comment' => '/Commentaire\s*\|\s*([^|]+?)(?=\s*\|)/i',
];
foreach ($tblRegexPatterns as $field => $regex) {
if (preg_match($regex, $formContent, $matches)) {
$data[$field] = trim($matches[1]);
} else {
return response()->json(['reason' => "Champ manquant $field"], 422);
}
}
if ($data['date_mep']) {
$dateMep = Carbon::createFromFormat('d/m/Y', $data['date_mep'])->format('Y-m-d');
}
$ticket = new Ticket([
'site_id' => $site->id,
'type' => TicketTypeEnum::mailing->value,
'status' => TicketStatusEnum::new->value,
'date_mep' => $dateMep ?? null,
'text' => "
Bonjour,\n
Nom du demandeur : {$data['user']},
Adresse mail : {$data['email']},
Marque / Filiale concernée : {$data['brand']},
Sujet : {$data['subject']},
Date d'envoi souhaitée : {$data['date_mep']},
Commentaire : {$data['comment']},\n
Cordialement.
",
'title' => "Demande de mail en masse pour {$data['brand']}",
'is_internal' => false
]);
$ticket->user()->associate($site->projectManager);
$ticket->site()->associate($site);
$ticket->assignedUser()->associate($site->projectManager);
$ticket->saveOrFail();
if (config('mail.MAIL_ADMIN')) {
Mail::to(config('mail.MAIL_ADMIN'))->send(new NewTicket($ticket));
} else {
throw new \ErrorException('Missing MAIL_ADMIN in .env');
}
return response()->json(['ok' => true]);
}
}

View file

@ -0,0 +1,58 @@
<?php
namespace App\Http\Controllers\Bo;
use App\Events\BrevoWebhookCallReceived;
use App\Http\Requests\Bo\BrevoWebhookRequest;
use Domain\Models\ValueObjects\Email;
use Psr\Log\LoggerInterface;
use Illuminate\Routing\Controller as BaseController;
use \Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Response;
class BrevoWebhookController extends BaseController
{
public function __construct(
private readonly LoggerInterface $logger,
private readonly Dispatcher $dispatcher
) {}
public function webhookCallReceived(BrevoWebhookRequest $request): JsonResponse
{
$fromEmail = $request->getFromAddress();
$toEmail = $request->getToAddress();
$formContent = $request->getRawHtmlBody();
if (!$this->isFromLdc($fromEmail)) {
$this->logger->warning(
'Received a brevo webhook call from a non ldc email.',
[
'from' => $fromEmail,
'to' => $toEmail,
'content' => base64_encode($formContent)
]
);
return response()->json(
['ok' => false],
Response::HTTP_BAD_REQUEST
);
}
$this->dispatcher->dispatch(
new BrevoWebhookCallReceived(
from: $fromEmail,
to: $toEmail,
htmlRawContent: $formContent,
)
);
return response()->json(['ok' => true]);
}
private function isFromLdc(Email $email): bool
{
return substr(strrchr($email->value, "@"), 1) === 'ldc.fr';
}
}

View file

@ -24,6 +24,7 @@ use App\Http\Requests\Bo\StoreTicket;
use App\Http\Requests\Bo\UpdateTicket;
use App\Lib\Helpers\FileStorageHelper;
use App\Enums\Tickets\TicketStatusEnum;
use App\Events\TicketCreated;
use App\Http\Requests\Bo\CreateMessage;
use App\Http\Requests\Bo\UpdateMessage;
use Illuminate\Support\Facades\Session;
@ -33,14 +34,16 @@ use App\Http\Requests\Bo\ChangeTicketStatus;
use App\Http\Requests\Bo\UpdateChangeOrderRequest;
use App\Http\Requests\Bo\AssignTicketToUserRequest;
use App\Http\Requests\Bo\UpdateTicketPriorityRequest;
use Illuminate\Contracts\Events\Dispatcher;
class TicketController extends Controller
{
/**
* TicketController
*/
public function __construct()
{
public function __construct(
public readonly Dispatcher $dispatcher
) {
parent::__construct();
$this->authorizeResource(Ticket::class, 'ticket');
}
@ -213,7 +216,7 @@ class TicketController extends Controller
}
}
$ticket->user()->associate(auth()->user());
$ticket->user()->associate($user);
$ticket->site()->associate($site);
$ticket->saveOrFail();
@ -223,11 +226,11 @@ class TicketController extends Controller
$user->subscriptions()->toggle($ticket->id);
if (config('mail.MAIL_ADMIN')) {
Mail::to(config('mail.MAIL_ADMIN'))->send(new NewTicket($ticket));
} else {
throw new \ErrorException('Missing MAIL_ADMIN in .env');
}
$this->dispatcher->dispatch(new TicketCreated(
siteReceivingTicket: $site,
ticket: $ticket,
byUserName: $user->name
));
return redirect()->route('bo.ticket.show', $ticket)
->with('success', trans('Ticket enregistré !'));
@ -751,16 +754,23 @@ class TicketController extends Controller
{
$userGitlabId = auth()->user()->gitlab_id;
$gitlabRepoId = $ticket->site->gitlab_project_id;
$description = \sprintf("%s<br>", $ticket->text);
$cLabel = match ($ticket->type) {
TicketTypeEnum::bug => 'c-bug,to-dispatch',
TicketTypeEnum::evolution => 'c-feature',
if (GitLab::projects()->show($gitlabRepoId)['archived']) {
return back()->with('error', \trans("Ce projet GitLab est archivé et ne peut pas être utilisé."));
}
$description = \sprintf("%s<br>", $ticket->text);
$cLabel = match ($ticket->type) {
TicketTypeEnum::bug => 'c-bug,g-to-dispatch',
TicketTypeEnum::evolution,
TicketTypeEnum::mailing => 'c-feature',
TicketTypeEnum::newsletter => 'c-feature,g-to-estimate',
};
$assigneeId = match ($ticket->type) {
TicketTypeEnum::bug => $userGitlabId,
TicketTypeEnum::evolution,
TicketTypeEnum::mailing,
TicketTypeEnum::newsletter => $ticket->site->projectManager?->gitlab_id
};

View file

@ -73,8 +73,9 @@ class UserController extends Controller
public function store(StoreUserRequest $request): \Illuminate\Http\RedirectResponse
{
return DB::transaction(function () use ($request) {
$user = new User();
$user = User::query()->onlyTrashed()->where('email', $request->validated('email'))->first() ?? new User();
$user->fill($request->safe()->except('sites'));
$user->deleted_at = null;
if ($user->saveOrFail()) {
$user->sites()->attach(collect($request->validated('sites'))->pluck('id'));

View file

@ -28,7 +28,7 @@ class RedirectIfAuthenticated
switch ($guard) {
case 'backend':
if (!$request->routeIs('bo.*') && !$request->routeIs('login')) {
continue;
continue 2;
}
return \redirect()->route('bo.dashboard');
default:

View file

@ -0,0 +1,50 @@
<?php
namespace App\Http\Requests\Bo;
use Domain\Models\ValueObjects\Email;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Config;
class BrevoWebhookRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
public function getFromAddress(): Email
{
return new Email($this->string('items.0.From.Address'));
}
public function getToAddress(): Email
{
return new Email((string) $this->input('items.0.To.0.Address'));
}
public function getRawHtmlBody(): string
{
return $this->string('items.0.RawHtmlBody');
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
$expectedFormAddress = Config::string('mail.brevo_webook.expected_from_address');
return [
'items' => ['required', 'array', 'min:1'],
'items.0.From.Address' => ['required', 'email:rfc,spoof'],
'items.0.To.*.Address' => ['required', 'email:rfc,spoof', "in:{$expectedFormAddress}"],
'items.0.RawHtmlBody' => ['required', 'string', 'min:10'],
];
}
}

View file

@ -1,31 +0,0 @@
<?php
namespace App\Http\Requests\Bo;
use Illuminate\Foundation\Http\FormRequest;
class BrevoWehbookRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'items' => 'required|array|min:1',
'items.0.From.Address' => 'required|email',
'items.0.To.*.Address' => 'required|email|in:mailing@reply.uwil.fr',
'items.0.ExtractedMarkdownMessage' => 'required|string|min:10',
];
}
}

View file

@ -40,7 +40,11 @@ class StoreUserRequest extends FormRequest
{
return [
'name' => 'required|string|min:3|max:255',
'email' => 'email|required|unique:users,email',
'email' => [
'email',
'required',
Rule::unique('users', 'email')->whereNull('deleted_at')
],
'role' => [
'required',
Rule::in(collect(RoleEnum::cases())->pluck('value')->all()),

View file

@ -0,0 +1,75 @@
<?php
namespace App\Listeners;
use App\Events\BrevoWebhookCallReceived;
use App\Events\TicketCreated;
use App\Mail\NewTicket;
use App\Repositories\SiteRepository;
use App\Services\AutomaticTicketCreator;
use Domain\Models\ValueObjects\Email;
use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Mail;
use Psr\Log\LoggerInterface;
// NOTE: this should be queued in order to be eventually retried or at least keep info data.
class CreateTicketAutomatically implements ShouldQueue
{
private const string SITE_NAME_WHERE_TO_CREATE_TICKET = 'Mailing en masse';
public function __construct(
private readonly LoggerInterface $logger,
private readonly AutomaticTicketCreator $automaticTicketCreator,
private readonly SiteRepository $siteRepository,
private readonly Dispatcher $dispatcher
) {}
public function handle(BrevoWebhookCallReceived $event): void
{
$fromEmail = $event->from;
$toEmail = $event->to;
$htmlRawContent = $event->htmlRawContent;
$this->logger->info('Received a ticket creation event from brevo');
$this->logger->debug(
'Payload for ticket creation form brevo',
[
'from_email' => $fromEmail->value,
'to_email' => $toEmail->value,
'html_raw_content' => base64_encode($htmlRawContent)
]
);
$siteReceivingTickets = $this->siteRepository->findByName(self::SITE_NAME_WHERE_TO_CREATE_TICKET);
if ($siteReceivingTickets === null) {
throw new \UnexpectedValueException(
'No site found with name \'' .
self::SITE_NAME_WHERE_TO_CREATE_TICKET . '\''
);
}
$projectManager = $siteReceivingTickets->projectManager()->first();
if ($projectManager === null) {
throw new \UnexpectedValueException(
'No project manager found for site \'' .
self::SITE_NAME_WHERE_TO_CREATE_TICKET . '\''
);
}
$ticket = $this->automaticTicketCreator->createTicket(
$siteReceivingTickets,
$projectManager,
$htmlRawContent,
);
$this->dispatcher->dispatch(new TicketCreated(
siteReceivingTicket: $siteReceivingTickets,
ticket: $ticket,
byUserName: $fromEmail->value
));
}
}

View file

@ -3,21 +3,51 @@
namespace App\Listeners;
use App\Events\NotifyAdmin;
use App\Events\TicketCreated;
use App\Mail\NewTicket;
use App\Mail\TicketUpdate;
use Domain\Models\ValueObjects\Email;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Mail;
class SendNotificationMailToAdmin
{
private readonly Email $mailAdmin;
public function __construct()
{
$this->mailAdmin = new Email(
Config::string('mail.MAIL_ADMIN')
);
}
/**
* Handle the event.
*
* @param \App\Events\NotifyAdmin $event
* @return void
*/
public function handle(NotifyAdmin $event)
public function handle(NotifyAdmin | TicketCreated $event)
{
$ticket = $event->ticket;
Mail::to(config('mail.MAIL_ADMIN'))
->send(new TicketUpdate($ticket, $event->corpsMaj));
if ($event instanceof NotifyAdmin) {
$ticket = $event->ticket;
Mail::to($this->mailAdmin->value)
->send(new TicketUpdate($ticket, $event->corpsMaj));
}
if ($event instanceof TicketCreated) {
$ticket = $event->ticket;
$siteReceivingTickets = $event->siteReceivingTicket;
$userName = $event->byUserName;
Mail::to($this->mailAdmin->value)
->send(new NewTicket(
ticketId: $ticket->id,
siteName: $siteReceivingTickets->name,
ticketTitle: $ticket->title,
ticketType: $ticket->type,
userName: $userName,
ticketDescription: $ticket->text,
));
}
}
}

View file

@ -2,29 +2,23 @@
namespace App\Mail;
use App\Models\Ticket;
use App\Enums\Tickets\TicketTypeEnum;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
class NewTicket extends Mailable
class NewTicket extends Mailable implements ShouldQueue
{
use Queueable;
use SerializesModels;
/** @var \App\Models\Ticket */
private $ticket;
/**
* Create a new message instance.
*
* @param \App\Models\Ticket $ticket
* @return void
*/
public function __construct(Ticket $ticket)
{
$this->ticket = $ticket;
}
public function __construct(
private readonly int $ticketId,
private readonly string $siteName,
private readonly string $ticketTitle,
private readonly TicketTypeEnum $ticketType,
private readonly string $userName,
private readonly string $ticketDescription,
) {}
/**
* Build the message.
@ -33,8 +27,14 @@ class NewTicket extends Mailable
*/
public function build()
{
$ticket = $this->ticket;
return $this->subject("[Support] Nouveau ticket n° $ticket->id - $ticket->title")
->view('emails.new-ticket', compact('ticket'));
return $this->subject("[Support] Nouveau ticket n° {$this->ticketId} - {$this->ticketTitle}")
->view('emails.new-ticket', [
'ticketId' => $this->ticketId,
'siteName' => $this->siteName,
'ticketTitle' => $this->ticketTitle,
'ticketType' => $this->ticketType,
'userName' => $this->userName,
'ticketDescription' => $this->ticketDescription,
]);
}
}

View file

@ -7,7 +7,13 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
/**
* @property string $name
* @property int $id Doit correspondre aux ids marques de la CRM
* @property string $name Nom de la marque
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property-read \Illuminate\Database\Eloquent\Collection<int, Account> $accounts
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Site> $sites
* @mixin \Eloquent
*/
class Brand extends Model
{

View file

@ -7,7 +7,14 @@ use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Casts\Attribute;
/**
* @property int $id
* @property string $name
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Invoice> $invoices
* @property-read mixed $value
* @method static \Database\Factories\CompanyFactory factory($count = null, $state = [])
* @mixin \Eloquent
*/
class Company extends Model
{

View file

@ -7,10 +7,16 @@ use App\Models\Brand;
use Illuminate\Database\Eloquent\Model;
/**
* @property string $label
* @property AccountTypeEnum $type
* @property integer $balance
* @property Brand $brand_id
* @property int $id
* @property AccountTypeEnum $type Enum Credit\AccountTypeEnum
* @property string $label Custom account label
* @property float $balance Account balance
* @property int $brand_id
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property-read Brand $brand
* @property-read \App\Models\Credit\CreditLog|null $creditlogs
* @mixin \Eloquent
*/
class Account extends Model
{

View file

@ -6,10 +6,15 @@ use App\Enums\Credit\BalanceOperationEnum;
use Illuminate\Database\Eloquent\Model;
/**
* @property BalanceOperationEnum $type
* @property double $amount
* @property Account $credit_account_id
* @property date $created_at
* @property int $id
* @property BalanceOperationEnum $type Enum Credit\BalanceOperationEnum
* @property float $amount Montant de l'opération
* @property int $credit_account_id
* @property string $created_at
* @property-read \App\Models\Credit\Account $account
* @property-read \App\Models\Credit\EstimationLog|null $pivot
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Credit\Estimation> $estimation
* @mixin \Eloquent
*/
class CreditLog extends Model
{

View file

@ -9,6 +9,22 @@ use App\Models\User;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Validation\ValidationException;
/**
* @property int $id
* @property EstimationStatusEnum $status Enum Credit\EstimationStatusEnum
* @property numeric $amount Estimation amount
* @property int $ticket_id
* @property int $credit_account_id The account to debit.
* @property int $estimator_id The person who has done the estimate.
* @property int|null $validator_id The person who has validate the estimate.
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property-read \App\Models\Credit\Account $account
* @property-read User|null $estimator
* @property-read Ticket|null $ticket
* @property-read User|null $validator
* @mixin \Eloquent
*/
class Estimation extends Model
{
/**

View file

@ -4,6 +4,14 @@ namespace App\Models\Credit;
use Illuminate\Database\Eloquent\Relations\Pivot;
/**
* @property int $id
* @property int $credit_log_id
* @property int $credit_estimation_id
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @mixin \Eloquent
*/
class EstimationLog extends Pivot
{
/**

View file

@ -4,6 +4,26 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Model;
/**
* @property int $id
* @property string $title Gitlab issue title
* @property string $url Gitlab URL
* @property int $estimate_time Gitlab issue estimate time in seconds
* @property int $spent_time Gitlab issue spent time in seconds
* @property int $is_closed Gitlab issue status
* @property int $gitlab_id Gitlab issue id
* @property int $gitlab_iid Gitlab issue iid in project
* @property int|null $milestone_iid Gitlab issue linked milestone iid in project
* @property int $gitlab_project_id Gitlab project id
* @property string $g_created_at Gitlab issue created at
* @property string $g_updated_at Gitlab issue updated at
* @property string|null $g_closed_at Gitlab issue closed at
* @property string|null $g_due_date Gitlab issue due date
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\GitlabLabels> $gitlabLabels
* @mixin \Eloquent
*/
class GitlabIssues extends Model
{
/**

View file

@ -4,6 +4,15 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Model;
/**
* @property int $id
* @property string $name Label name
* @property int $gitlab_id Label Gitlab id
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\GitlabIssues> $issues
* @mixin \Eloquent
*/
class GitlabLabels extends Model
{
/**

View file

@ -4,6 +4,22 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Model;
/**
* @property int $id
* @property string $title Milestone title
* @property string $url Gitlab URL
* @property int $is_closed Gitlab milestone status
* @property int $gitlab_id Gitlab milestone id
* @property int $gitlab_iid Gitlab milestone iid in project
* @property int $gitlab_project_id Gitlab project id
* @property string $g_created_at Gitlab milestone created at
* @property string $g_updated_at Gitlab milestone updated at
* @property string|null $g_start_date Gitlab milestone start date
* @property string|null $g_due_date Gitlab milestone due date
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @mixin \Eloquent
*/
class GitlabMilestones extends Model
{
/**

View file

@ -4,6 +4,16 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Model;
/**
* @property int $id
* @property string $name Gitlab project name
* @property string $url Gitlab URL
* @property int $is_archived Gitlab archive status
* @property int $gitlab_id Gitlab unique ID
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @mixin \Eloquent
*/
class GitlabProjects extends Model
{
/**

View file

@ -7,9 +7,13 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
/**
* @property int $id
* @property string $title
* @property string $message
* @property InfoColorsEnum $type
* @property InfoColorsEnum $type Defined by the InfoColorsEnum enum.
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @mixin \Eloquent
*/
class Information extends Model
{

View file

@ -13,22 +13,34 @@ use Mccarlosen\LaravelMpdf\LaravelMpdf;
use Illuminate\Database\Eloquent\SoftDeletes;
/**
* @property string $ref
* @property string $firstname
* @property string $lastname
* @property string $object
* @property string $tasks
* @property double $total
* @property double $total_days
* @property string $comment
* @property boolean $show_days
* @property date $signed_date
* @property date $sent_client_date
* @property date $sent_accounting_date
* @property Site $site_id
* @property Company $company_id
* @property Company $invoiced_to_id
* @property InvoiceStatusEnum $status
* @property int $id
* @property string $ref The invoice reference built as YYYY_MM_INCREMENT_USER. INCREMENT is the N invoice of the month. USER is the first two letters of the user who last edited the invoice. This field has to generated only on create.
* @property string $firstname The person to which the invoice is addressed.
* @property string $lastname The person to which the invoice is addressed.
* @property string $object The invoice object.
* @property array<array-key, mixed> $tasks A JSON object, each line has to be contructed using these fields description, days, type Invoices\TaskTypeEnum, amount
* @property float $total The total calculated using all tasks
* @property float $total_days The total days calculated using all tasks
* @property string|null $comment Any comment joined to the invoice
* @property int $show_days Show days on the printed invoice
* @property Carbon|null $signed_date The date the invoice was signed
* @property Carbon|null $sent_client_date The date the invoice was send to the client
* @property Carbon|null $sent_accounting_date The date the invoice was send to the client
* @property int $site_id
* @property int $company_id
* @property int $invoiced_to_id
* @property InvoiceStatusEnum $status Status of the invoice.
* @property Carbon|null $created_at
* @property Carbon|null $updated_at
* @property Carbon|null $deleted_at
* @property-read \App\Models\Company $company
* @property-read \App\Models\Company $companyInvoiced
* @property-read \App\Models\Site|null $site
* @method static \Database\Factories\InvoiceFactory factory($count = null, $state = [])
* @method static \Illuminate\Database\Eloquent\Builder<static>|Invoice onlyTrashed()
* @method static \Illuminate\Database\Eloquent\Builder<static>|Invoice withTrashed(bool $withTrashed = true)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Invoice withoutTrashed()
* @mixin \Eloquent
*/
class Invoice extends Model
{

View file

@ -7,6 +7,14 @@ use App\Lib\Helpers\ToolboxHelper;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
/**
* @property int $id
* @property string $name Un ou deux mot désignant le label
* @property string $color Cast HTML color
* @property-read \App\Models\Site|null $site
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Ticket> $tickets
* @mixin \Eloquent
*/
class Label extends Model
{
use HasFactory;

View file

@ -9,12 +9,20 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
/**
*
* @property string $message The message content.
* @property \App\Enums\MessageTypeEnum $type The message is not visible for clients.
* @property string $file The message joined file path.
*
* @package App\Models
* @property int $id
* @property int $ticket_id
* @property int|null $user_id
* @property string $message
* @property MessageTypeEnum $type Message Type defined by an enum.
* @property string|null $file
* @property string|null $file2 Pièce jointes n°2
* @property string|null $file3 Pièce jointes n°3
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property-read \App\Models\Ticket|null $ticket
* @property-read \App\Models\User|null $user
* @mixin \Eloquent
*/
class Message extends Model
{

View file

@ -6,8 +6,12 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
/**
* @property int $id
* @property string $name
* @property double $amount
* @property float $amount The cost per day
* @property bool $published
* @method static \Database\Factories\PackageFactory factory($count = null, $state = [])
* @mixin \Eloquent
*/
class Package extends Model
{

View file

@ -10,11 +10,26 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\SoftDeletes;
/**
* @property string $name
* @property string $url_re7
* @property string $url_prod
* @property integer $gitlab_project_id
* @property Brand $brand_id
* @property int $id
* @property string $name
* @property string|null $url_re7
* @property string|null $url_prod
* @property int|null $gitlab_project_id
* @property int|null $brand_id Relation is nullable, since not all site would be affiliate with a brand.
* @property int|null $project_manager_id Project manager relationship
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property \Illuminate\Support\Carbon|null $deleted_at
* @property-read \App\Models\Brand|null $brand
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Invoice> $invoices
* @property-read \App\Models\User|null $projectManager
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Ticket> $tickets
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\User> $users
* @property-read mixed $value
* @method static \Illuminate\Database\Eloquent\Builder<static>|Site onlyTrashed()
* @method static \Illuminate\Database\Eloquent\Builder<static>|Site withTrashed(bool $withTrashed = true)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Site withoutTrashed()
* @mixin \Eloquent
*/
class Site extends Model
{

View file

@ -4,6 +4,11 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Relations\Pivot;
/**
* @property int $site_id
* @property int $user_id
* @mixin \Eloquent
*/
class SiteUser extends Pivot
{
/**

View file

@ -22,6 +22,44 @@ use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\ModelNotFoundException;
/**
* @property int $id
* @property int $created_by
* @property int|null $assigned_to La personne a qui est assigné le ticket.
* @property int $site_id
* @property TicketTypeEnum $type
* @property string $title Titre du ticket
* @property string $text
* @property string|null $file
* @property string|null $file2
* @property string|null $file3 Pièce jointes n°3
* @property string|null $file4 Pièce jointes n°4
* @property string|null $file5 Pièce jointes n°5
* @property TicketStatusEnum $status
* @property int $order Ticket order.
* @property bool $is_internal
* @property string|null $gitlab_issue URL de l'issue Gitlab
* @property \Illuminate\Support\Carbon|null $date_mep Date MEP souhaitée
* @property int $last_activity Last activity on the ticket.
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property \Illuminate\Support\Carbon|null $deleted_at
* @property-read \App\Models\User|null $assignedUser
* @property-read Estimation|null $estimation
* @property-read string $full_name
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Label> $labels
* @property-read \Illuminate\Database\Eloquent\Collection<int, Ticket> $linkedTickets
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Message> $messages
* @property-read \App\Models\User|null $projectManager
* @property-read \App\Models\Site|null $site
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\User> $subscriptions
* @property-read TicketType|null $ticketType
* @property-read \App\Models\User|null $user
* @method static \Illuminate\Database\Eloquent\Builder<static>|Ticket onlyTrashed()
* @method static \Illuminate\Database\Eloquent\Builder<static>|Ticket withTrashed(bool $withTrashed = true)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Ticket withoutTrashed()
* @mixin \Eloquent
*/
class Ticket extends Model
{
use HasFactory;

View file

@ -9,8 +9,13 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
/**
* @property TicketTypeEnum $type
* @property string $template
* @property int $id
* @property TicketTypeEnum $type TicketType Enum
* @property string $template Le template du type de ticket
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property-read \Illuminate\Database\Eloquent\Collection<int, Ticket> $tickets
* @mixin \Eloquent
*/
class TicketType extends Model
{

View file

@ -15,12 +15,27 @@ use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
/**
* @property int $id
* @property string $name
* @property string $email
* @property string $password
* @property string $remember_token
* @property RoleEnum $role
* @property Collection $sites
* @property string|null $remember_token
* @property int|null $gitlab_id ID du user Gitlab
* @property int|null $favourite_site_id
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property \Illuminate\Support\Carbon|null $deleted_at
* @property-read Collection<int, \App\Models\Message> $messages
* @property-read \Illuminate\Notifications\DatabaseNotificationCollection<int, \Illuminate\Notifications\DatabaseNotification> $notifications
* @property-read Collection<int, \App\Models\Site> $sites
* @property-read Collection<int, \App\Models\Ticket> $subscriptions
* @property-read Collection<int, \App\Models\Ticket> $tickets
* @method static \Database\Factories\UserFactory factory($count = null, $state = [])
* @method static \Illuminate\Database\Eloquent\Builder<static>|User onlyTrashed()
* @method static \Illuminate\Database\Eloquent\Builder<static>|User withTrashed(bool $withTrashed = true)
* @method static \Illuminate\Database\Eloquent\Builder<static>|User withoutTrashed()
* @mixin \Eloquent
*/
class User extends Authenticatable
{

View file

@ -287,7 +287,8 @@ class TicketPolicy
$ticket->type->equals(TicketTypeEnum::bug)
|| $ticket->type->equals(TicketTypeEnum::evolution)
|| $ticket->type->equals(TicketTypeEnum::newsletter)
)
|| $ticket->type->equals(TicketTypeEnum::mailing)
)
&& !\is_null($ticket->site->gitlab_project_id)
&& \is_null($ticket->gitlab_issue);
}

View file

@ -2,12 +2,24 @@
namespace App\Providers;
use App\Repositories\EloquentSiteRepository;
use App\Repositories\SiteRepository;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\URL;
use Illuminate\Support\ServiceProvider;
use Override;
class AppServiceProvider extends ServiceProvider
{
#[Override]
public function register()
{
$this->app->singleton(
SiteRepository::class,
fn() => new EloquentSiteRepository()
);
}
/**
* Bootstrap any application services.
*
@ -24,7 +36,7 @@ class AppServiceProvider extends ServiceProvider
return request()->getClientIp();
} else {
return request()->server('HTTP_X_FORWARDED_FOR') ??
request()->getClientIp();
request()->getClientIp();
}
});

View file

@ -2,14 +2,6 @@
namespace App\Providers;
use App\Events\NotifyAdmin;
use App\Events\NotifySubscriptions;
use App\Events\NotifyTicketCreator;
use App\Events\TicketStatusChanged;
use App\Listeners\AddStatusMessages;
use App\Listeners\SendNotificationMailToAdmin;
use App\Listeners\SendNotificationMailToTicketCreator;
use App\Listeners\SendSubscriptionsNotificationsMail;
use Illuminate\Auth\Events\Registered;
use Illuminate\Auth\Listeners\SendEmailVerificationNotification;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
@ -23,10 +15,6 @@ class EventServiceProvider extends ServiceProvider
*/
protected $listen = [
Registered::class => [SendEmailVerificationNotification::class],
TicketStatusChanged::class => [AddStatusMessages::class],
NotifySubscriptions::class => [SendSubscriptionsNotificationsMail::class],
NotifyTicketCreator::class => [SendNotificationMailToTicketCreator::class],
NotifyAdmin::class => [SendNotificationMailToAdmin::class],
];
/**
@ -36,6 +24,6 @@ class EventServiceProvider extends ServiceProvider
*/
public function shouldDiscoverEvents(): bool
{
return false;
return true;
}
}

View file

@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace App\Repositories;
use App\Models\Site;
use Override;
class EloquentSiteRepository implements SiteRepository
{
#[Override]
public function findByName(string $name): ?Site
{
return Site::query()
->where('name', $name)
->first();
}
}

View file

@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace App\Repositories;
use App\Models\Site;
interface SiteRepository
{
public function findByName(string $name): ?Site;
}

View file

@ -0,0 +1,56 @@
<?php
declare(strict_types=1);
namespace App\Services;
use App\Enums\Tickets\TicketStatusEnum;
use App\Enums\Tickets\TicketTypeEnum;
use App\Models\Site;
use App\Models\Ticket;
use App\Models\User;
class AutomaticTicketCreator
{
public function __construct(
private readonly BrevoWebookParser $parser
) {}
public function createTicket(
Site $forSite,
User $withProjectManager,
string $withRawHtmlContent,
): Ticket {
$parsedContent = $this->parser->parseRawHtml($withRawHtmlContent);
$textContent = "Bonjour,\n";
$textContent .= " Contenu :\n\n";
$textContent .= " {$parsedContent->plainTextContent},\n\n";
$textContent .= " Cordialement.";
$title = "Demande de mail en masse";
if (!empty($parsedContent->brand)) {
$title .= " pour {$parsedContent->brand}";
}
$ticket = new Ticket([
'site_id' => $forSite->id,
'type' => TicketTypeEnum::mailing->value,
'status' => TicketStatusEnum::estimateTodo->value,
'date_mep' => null,
'text' => $textContent,
'title' => $title,
'is_internal' => false
]);
$ticket->site()->associate($forSite);
$ticket->user()->associate($withProjectManager);
$ticket->assignedUser()->associate($withProjectManager);
$ticket->saveOrFail();
return $ticket;
}
}

View file

@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace App\Services;
use App\Dto\BrevoWebHookDto;
use App\Exceptions\BrevoParseFailure;
class BrevoWebookParser
{
/** @throws BrevoParseFailure */
public function parseRawHtml(string $rawHtml): BrevoWebHookDto
{
$data = [];
$plainText = strip_tags($rawHtml);
$tblRegexPatterns = [
'user' => '/^Matricule et Nom du demandeur\s*\r?\n(.*)$/im',
'email' => '/^Email du demandeur\s*\r?\n(.*)$/im',
'brand' => '/^Marque\s*\/\s*Filiale\s*\r?\n(.*)$/im',
];
foreach ($tblRegexPatterns as $field => $regex) {
if (preg_match($regex, $plainText, $matches)) {
$data[$field] = trim($matches[1]);
}
}
return new BrevoWebHookDto(
plainTextContent: $plainText,
brand: $data['brand'] ?? null
);
}
}

View file

@ -17,7 +17,7 @@ trait HasGitlabIssue
*/
public function getGitlabIssue(int $projectId, string $issueUrl): array
{
if (!preg_match('/\/issues\/(\d+)/', $issueUrl, $matches)) { return []; }
if (!preg_match('/\/(?:issues|work_items)\/(\d+)/', $issueUrl, $matches)) { return []; }
$gitlabIssue = GitLab::issues()->show($projectId, (int) $matches[1]);

View file

@ -12,7 +12,7 @@
],
"license": "MIT",
"require": {
"php": "^8.3",
"php": "^8.5",
"carlos-meneses/laravel-mpdf": "^2.1",
"codegreencreative/laravel-samlidp": "^5.2",
"diglactic/laravel-breadcrumbs": "^9.0",
@ -28,6 +28,7 @@
"uwil/k8s-laravel": "^1.2"
},
"require-dev": {
"barryvdh/laravel-ide-helper": "^3.7",
"fakerphp/faker": "^1.9.1",
"laravel-lang/common": "^4.0",
"laravel/pint": "^1.0",
@ -43,7 +44,8 @@
"psr-4": {
"App\\": "app/",
"Database\\Factories\\": "database/factories/",
"Database\\Seeders\\": "database/seeders/"
"Database\\Seeders\\": "database/seeders/",
"Domain\\": "domain/"
}
},
"repositories": [
@ -67,7 +69,11 @@
"@php artisan package:discover --ansi"
],
"post-update-cmd": [
"@php artisan vendor:publish --tag=laravel-assets --ansi --force"
"@php artisan vendor:publish --tag=laravel-assets --ansi --force",
"Illuminate\\Foundation\\ComposerScripts::postUpdate",
"@php artisan lang:update",
"@php artisan ide-helper:generate",
"@php artisan ide-helper:models -RW"
],
"post-root-package-install": [
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\""

2951
composer.lock generated

File diff suppressed because it is too large Load diff

379
config/ide-helper.php Normal file
View file

@ -0,0 +1,379 @@
<?php
declare(strict_types=1);
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\Client\Factory;
use Illuminate\Http\Client\PendingRequest;
use Illuminate\Session\Store;
return [
/*
|--------------------------------------------------------------------------
| Filename
|--------------------------------------------------------------------------
|
| The default filename.
|
*/
'filename' => '_ide_helper.php',
/*
|--------------------------------------------------------------------------
| Models filename
|--------------------------------------------------------------------------
|
| The default filename for the models helper file.
|
*/
'models_filename' => '_ide_helper_models.php',
/*
|--------------------------------------------------------------------------
| PhpStorm meta filename
|--------------------------------------------------------------------------
|
| PhpStorm also supports the directory `.phpstorm.meta.php/` with arbitrary
| files in it, should you need additional files for your project; e.g.
| `.phpstorm.meta.php/laravel_ide_Helper.php'.
|
*/
'meta_filename' => '.phpstorm.meta.php',
/*
|--------------------------------------------------------------------------
| Fluent helpers
|--------------------------------------------------------------------------
|
| Set to true to generate commonly used Fluent methods.
|
*/
'include_fluent' => false,
/*
|--------------------------------------------------------------------------
| Write model query methods
|--------------------------------------------------------------------------
|
| Set to false to disable generated docs for the 'query()', 'newQuery()' and 'newModelQuery()' methods.
|
*/
'write_query_methods' => false,
/*
|--------------------------------------------------------------------------
| Write model magic methods
|--------------------------------------------------------------------------
|
| Set to false to disable write magic methods of model.
|
*/
'write_model_magic_where' => false,
/*
|--------------------------------------------------------------------------
| Write model external Eloquent builder methods
|--------------------------------------------------------------------------
|
| Set to false to disable write external Eloquent builder methods.
|
*/
'write_model_external_builder_methods' => false,
/*
|--------------------------------------------------------------------------
| Write model relation count and exists properties
|--------------------------------------------------------------------------
|
| Set to false to disable writing of relation count and exists properties
| to model DocBlocks.
|
*/
'write_model_relation_count_properties' => false,
'write_model_relation_exists_properties' => false,
/*
|--------------------------------------------------------------------------
| Write Eloquent model mixins
|--------------------------------------------------------------------------
|
| This will add the necessary DocBlock mixins to the model class
| contained in the Laravel framework. This helps the IDE with
| auto-completion.
|
| Please be aware that this setting changes a file within the /vendor directory.
|
*/
'write_eloquent_model_mixins' => false,
/*
|--------------------------------------------------------------------------
| Helper files to include
|--------------------------------------------------------------------------
|
| Include helper files. By default not included, but can be toggled with the
| -- helpers (-H) option. Extra helper files can be included.
|
*/
'include_helpers' => false,
'helper_files' => [
base_path() . '/vendor/laravel/framework/src/Illuminate/Support/helpers.php',
base_path() . '/vendor/laravel/framework/src/Illuminate/Foundation/helpers.php',
],
/*
|--------------------------------------------------------------------------
| Model locations to include
|--------------------------------------------------------------------------
|
| Define in which directories the ide-helper:models command should look
| for models.
|
| glob patterns are supported to easier reach models in sub-directories,
| e.g. `app/Services/* /Models` (without the space).
|
*/
'model_locations' => [
'app',
],
/*
|--------------------------------------------------------------------------
| Models to ignore
|--------------------------------------------------------------------------
|
| Define which models should be ignored.
|
*/
'ignored_models' => [],
/*
|--------------------------------------------------------------------------
| Models hooks
|--------------------------------------------------------------------------
|
| Define which hook classes you want to run for models to add custom information.
|
| Hooks should implement Barryvdh\LaravelIdeHelper\Contracts\ModelHookInterface.
|
*/
'model_hooks' => [
// App\Support\IdeHelper\MyModelHook::class
],
/*
|--------------------------------------------------------------------------
| Extra classes
|--------------------------------------------------------------------------
|
| These implementations are not really extended, but called with magic functions.
|
*/
'extra' => [
'Eloquent' => [Builder::class, \Illuminate\Database\Query\Builder::class],
'Session' => [Store::class],
],
'magic' => [],
/*
|--------------------------------------------------------------------------
| Interface implementations
|--------------------------------------------------------------------------
|
| These interfaces will be replaced with the implementing class. Some interfaces
| are detected by the helpers, others can be listed below.
|
*/
'interfaces' => [
// App\MyInterface::class => App\MyImplementation::class,
],
/*
|--------------------------------------------------------------------------
| Support for camel cased models
|--------------------------------------------------------------------------
|
| There are some Laravel packages (such as Eloquence) that allow for accessing
| Eloquent model properties via camel case, instead of snake case.
|
| Enabling this option will support these packages by saving all model
| properties as camel case, instead of snake case.
|
| For example, normally you would see this:
|
| * @property \Illuminate\Support\Carbon $created_at
| * @property \Illuminate\Support\Carbon $updated_at
|
| With this enabled, the properties will be this:
|
| * @property \Illuminate\Support\Carbon $createdAt
| * @property \Illuminate\Support\Carbon $updatedAt
|
| Note, it is currently an all-or-nothing option.
|
*/
'model_camel_case_properties' => false,
/*
|--------------------------------------------------------------------------
| Property casts
|--------------------------------------------------------------------------
|
| Cast the given "real type" to the given "type".
|
*/
'type_overrides' => [
'integer' => 'int',
'boolean' => 'bool',
],
/*
|--------------------------------------------------------------------------
| Include DocBlocks from classes
|--------------------------------------------------------------------------
|
| Include DocBlocks from classes to allow additional code inspection for
| magic methods and properties.
|
*/
'include_class_docblocks' => false,
/*
|--------------------------------------------------------------------------
| Force FQN usage
|--------------------------------------------------------------------------
|
| Use the fully qualified (class) name in DocBlocks,
| even if the class exists in the same namespace
| or there is an import (use className) of the class.
|
*/
'force_fqn' => false,
/*
|--------------------------------------------------------------------------
| Use generics syntax
|--------------------------------------------------------------------------
|
| Use generics syntax within DocBlocks,
| e.g. `Collection<User>` instead of `Collection|User[]`.
|
*/
'use_generics_annotations' => true,
/*
|--------------------------------------------------------------------------
| Default return types for macros
|--------------------------------------------------------------------------
|
| Define default return types for macros without explicit return types.
| e.g. `\Illuminate\Database\Query\Builder::class => 'static'`,
| `\Illuminate\Support\Str::class => 'string'`
|
*/
'macro_default_return_types' => [
Factory::class => PendingRequest::class,
],
/*
|--------------------------------------------------------------------------
| Additional relation types
|--------------------------------------------------------------------------
|
| Sometimes it's needed to create custom relation types. The key of the array
| is the relationship method name. The value of the array is the fully-qualified
| class name of the relationship, e.g. `'relationName' => RelationShipClass::class`.
|
*/
'additional_relation_types' => [],
/*
|--------------------------------------------------------------------------
| Additional relation return types
|--------------------------------------------------------------------------
|
| When using custom relation types its possible for the class name to not contain
| the proper return type of the relation. The key of the array is the relationship
| method name. The value of the array is the return type of the relation ('many'
| or 'morphTo').
| e.g. `'relationName' => 'many'`.
|
*/
'additional_relation_return_types' => [],
/*
|--------------------------------------------------------------------------
| Enforce nullable Eloquent relationships on not null columns
|--------------------------------------------------------------------------
|
| When set to true (default), this option enforces nullable Eloquent relationships.
| However, in cases where the application logic ensures the presence of related
| records it may be desirable to set this option to false to avoid unwanted null warnings.
|
| Default: true
| A not null column with no foreign key constraint will have a "nullable" relationship.
| * @property int $not_null_column_with_no_foreign_key_constraint
| * @property-read BelongsToVariation|null $notNullColumnWithNoForeignKeyConstraint
|
| Option: false
| A not null column with no foreign key constraint will have a "not nullable" relationship.
| * @property int $not_null_column_with_no_foreign_key_constraint
| * @property-read BelongsToVariation $notNullColumnWithNoForeignKeyConstraint
|
*/
'enforce_nullable_relationships' => true,
/*
|--------------------------------------------------------------------------
| Make soft deletable relations nullable
|--------------------------------------------------------------------------
|
| When set to true (default), relationships to models using SoftDeletes trait
| will be marked as nullable. This is because soft-deleted records are excluded
| from queries by default, meaning even non-nullable foreign keys can return
| null when the related model is soft-deleted.
|
| Default: true
| A relationship to a soft-deletable model will include |null in the type:
| * @property-read Team|null $team
|
| Option: false
| A relationship to a soft-deletable model will NOT include |null (unless
| nullable for other reasons such as nullable foreign key column):
| * @property-read Team $team
|
*/
'soft_deletes_force_nullable' => true,
/*
|--------------------------------------------------------------------------
| Run artisan commands after migrations to generate model helpers
|--------------------------------------------------------------------------
|
| The specified commands should run after migrations are finished running.
|
*/
'post_migrate' => [
// 'ide-helper:models --nowrite',
],
];

View file

@ -18,6 +18,10 @@ return [
// * Mail admin
'MAIL_ADMIN' => env('MAIL_ADMIN', 'digital@ldc.fr'),
'brevo_webook' => [
'expected_from_address' => 'mailing@reply.uwil.fr'
],
/*
|--------------------------------------------------------------------------
| Mailer Configurations

View file

@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace Database\Factories;
use App\Models\Site;
use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;
class SiteFactory extends Factory
{
/**
* The name of the factory's corresponding model.
*
* @var string
*/
protected $model = Site::class;
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition(): array
{
return [
'name' => fake()->company(),
'url_re7' => fake()->url(),
'url_prod' => fake()->url(),
'gitlab_project_id' => fake()->optional()->randomNumber(4),
'brand_id' => null,
'project_manager_id' => null,
];
}
/**
* Indique que le site a un chef de projet assigné.
*/
public function withProjectManager(): static
{
return $this->state(fn(array $attributes) => [
'project_manager_id' => User::factory(),
]);
}
}

View file

@ -1,10 +1,12 @@
<?php
declare(strict_types=1);
namespace Database\Factories;
use App\Models\User;
use App\Enums\Users\RoleEnum;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Str;
class UserFactory extends Factory
{
@ -18,17 +20,23 @@ class UserFactory extends Factory
/**
* Define the model's default state.
*
* @return array
* @return array<string, mixed>
*/
public function definition()
public function definition(): array
{
return [
'name' => $this->faker->name,
'email' => $this->faker->unique()->safeEmail,
'email_verified_at' => now(),
// * password
'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi',
'remember_token' => Str::random(10),
'name' => fake()->name(),
'email' => fake()->unique()->safeEmail(),
'role' => RoleEnum::developper,
'gitlab_id' => fake()->optional()->randomNumber(5),
'favourite_site_id' => null,
];
}
public function admin(): static
{
return $this->state(fn(array $attributes) => [
'role' => RoleEnum::admin,
]);
}
}

View file

@ -16,12 +16,12 @@ class CreateCreditLogsTable extends Migration
Schema::create('credit_logs', function (Blueprint $table) {
$table->id();
$table->unsignedTinyInteger('type')->comment('Enum Credit\\BalanceOperationEnum');
$table->unsignedFloat('amount')->comment('Montant de l\'opération');
$table->tinyInteger('type')->unsigned()->comment('Enum Credit\\BalanceOperationEnum');
$table->float('amount')->unsigned()->comment('Montant de l\'opération');
$table->foreignId('credit_account_id')
->constrained('credit_accounts')
->cascadeOnDelete();
->constrained('credit_accounts')
->cascadeOnDelete();
$table->timestamp('created_at');
});

View file

@ -18,24 +18,24 @@ class CreateCreditEstimationsTable extends Migration
Schema::create('credit_estimations', function (Blueprint $table) {
$table->id();
$table->unsignedTinyInteger('status')->default(EstimationStatusEnum::pending)
->comment('Enum Credit\\EstimationStatusEnum');
$table->tinyInteger('status')->unsigned()->default(EstimationStatusEnum::pending)
->comment('Enum Credit\\EstimationStatusEnum');
$table->unsignedFloat('amount')->comment('Estimation amount');
$table->float('amount')->unsigned()->comment('Estimation amount');
$table->foreignIdFor(Ticket::class)
->constrained();
$table->foreignId('credit_account_id')
->comment('The account to debit.')
->constrained('credit_accounts');
->comment('The account to debit.')
->constrained('credit_accounts');
$table->foreignId('estimator_id')
->comment('The person who has done the estimate.')
->constrained('users');
->comment('The person who has done the estimate.')
->constrained('users');
$table->foreignId('validator_id')
->nullable()
->comment('The person who has validate the estimate.')
->constrained('users');
->nullable()
->comment('The person who has validate the estimate.')
->constrained('users');
$table->timestamps();
});

View file

@ -1,6 +1,7 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class ChangeAmoutFloatingPointOnCreditEstimations extends Migration
@ -12,8 +13,8 @@ class ChangeAmoutFloatingPointOnCreditEstimations extends Migration
*/
public function up()
{
Schema::table('credit_estimations', function ($table) {
$table->unsignedFloat('amount', 8, 3)
Schema::table('credit_estimations', function (Blueprint $table) {
$table->decimal('amount', 8, 3)->unsigned()
->comment('Estimation amount')->change();
});
}
@ -25,8 +26,8 @@ class ChangeAmoutFloatingPointOnCreditEstimations extends Migration
*/
public function down()
{
Schema::table('credit_estimations', function ($table) {
$table->unsignedFloat('amount')
Schema::table('credit_estimations', function (Blueprint $table) {
$table->float('amount')->unsigned()
->comment('Estimation amount')->change();
});
}

View file

@ -17,7 +17,7 @@ class CreatePackagesTable extends Migration
$table->id();
$table->string('name');
$table->unsignedFloat('amount')->comment('The cost per day');
$table->float('amount')->unsigned()->comment('The cost per day');
});
}

View file

@ -37,8 +37,8 @@ class CreateInvoicesTable extends Migration
'A JSON object, each line has to be contructed using these fields description, days, type Invoices\TaskTypeEnum, amount'
);
$table->unsignedFloat('total')->comment('The total calculated using all tasks');
$table->unsignedFloat('total_days')->comment('The total days calculated using all tasks');
$table->float('total')->unsigned()->comment('The total calculated using all tasks');
$table->float('total_days')->unsigned()->comment('The total days calculated using all tasks');
$table->text('comment')->nullable()
->comment('Any comment joined to the invoice');

View file

@ -1,6 +1,6 @@
<?php
use App\Models\Ticket;
use Illuminate\Support\Facades\DB;
use Illuminate\Database\Migrations\Migration;
class ChangeTicketsStatus extends Migration
@ -12,17 +12,22 @@ class ChangeTicketsStatus extends Migration
*/
public function up()
{
Ticket::whereIn('status', [4, 5, 7, 10])
DB::table('tickets')->whereIn('status', [4, 5, 7, 10])
->update(['status' => 3]);
Ticket::where('status', 9)
DB::table('tickets')->where('status', 9)
->update(['status' => 4]);
Ticket::where('status', 17)
DB::table('tickets')->where('status', 17)
->update(['status' => 5]);
Ticket::where('status', 12)
DB::table('tickets')->where('status', 12)
->update(['status' => 6]);
Ticket::where('status', 13)
DB::table('tickets')->where('status', 13)
->update(['status' => 7]);
Ticket::where('status', 15)
DB::table('tickets')->where('status', 15)
->update(['status' => 8]);
}
@ -33,17 +38,22 @@ class ChangeTicketsStatus extends Migration
*/
public function down()
{
Ticket::where('status', 3)
DB::table('tickets')->where('status', 3)
->update(['status' => 10]);
Ticket::where('status', 4)
DB::table('tickets')->where('status', 4)
->update(['status' => 9]);
Ticket::where('status', 5)
DB::table('tickets')->where('status', 5)
->update(['status' => 17]);
Ticket::where('status', 6)
DB::table('tickets')->where('status', 6)
->update(['status' => 12]);
Ticket::where('status', 7)
DB::table('tickets')->where('status', 7)
->update(['status' => 13]);
Ticket::where('status', 8)
DB::table('tickets')->where('status', 8)
->update(['status' => 15]);
}
}

View file

@ -1,9 +1,8 @@
<?php
use App\Models\Ticket;
use Carbon\Carbon;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
@ -17,15 +16,32 @@ return new class extends Migration
{
Schema::table('tickets', function (Blueprint $table) {
$table->timestamp('last_activity')
->default(Carbon::now())
->useCurrent()
->comment('Last activity on the ticket.')
->after('is_internal');
});
Ticket::get()->map(function (object $ticket) {
$lastMessage = $ticket->messages->whereNotNull('user_id')->sortByDesc('updated_at')->first();
$ticket->last_activity =
$lastMessage->updated_at ?? $ticket->updated_at;
$ticket->saveQuietly();
DB::table('tickets')->orderBy('id')->chunk(500, function ($tickets) {
$ticketIds = $tickets->pluck('id')->toArray();
$messages = DB::table('messages')
->whereIn('ticket_id', $ticketIds)
->whereNotNull('user_id')
->orderBy('updated_at', 'desc')
->get()
->groupBy('ticket_id');
foreach ($tickets as $ticket) {
$lastMessageDate = $messages->has($ticket->id)
? $messages->get($ticket->id)->first()->updated_at
: null;
DB::table('tickets')
->where('id', $ticket->id)
->update([
'last_activity' => $lastMessageDate ?? $ticket->updated_at
]);
}
});
}

View file

@ -1,10 +1,9 @@
<?php
use App\Models\Ticket;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
@ -22,11 +21,15 @@ return new class extends Migration
});
DB::transaction(function () {
$tickets = Ticket::all();
foreach ($tickets as $ticket) {
$ticket->title = 'ts-' . $ticket->id;
$ticket->saveQuietly();
}
DB::table('tickets')->orderBy('id')->chunkById(1000, function ($tickets) {
foreach ($tickets as $ticket) {
DB::table('tickets')
->where('id', $ticket->id)
->update([
'title' => 'ts-' . $ticket->id
]);
}
});
});
}

View file

@ -1,8 +1,7 @@
<?php
use App\Models\Ticket;
use App\Models\Ticket\TicketType;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;
return new class extends Migration
{
@ -13,13 +12,19 @@ return new class extends Migration
*/
public function up(): void
{
Ticket::query()->get()->map(function (Ticket $model) {
$model->type = $model->getRawOriginal('type') + 1;
$model->saveQuietly();
});
TicketType::query()->orderBy('type', 'DESC')->get()->map(function (TicketType $model) {
$model->type = $model->getRawOriginal('type') + 1;
$model->saveQuietly();
DB::transaction(function () {
DB::table('tickets')->orderBy('id')->chunkById(1000, function ($tickets) {
$ids = $tickets->pluck('id')->toArray();
DB::table('tickets')->whereIn('id', $ids)->increment('type');
});
$ticketTypes = DB::table('ticket_types')->orderBy('type', 'desc')->get();
foreach ($ticketTypes as $type) {
DB::table('ticket_types')
->where('id', $type->id)
->update(['type' => $type->type + 1]);
}
});
}
@ -30,13 +35,20 @@ return new class extends Migration
*/
public function down(): void
{
Ticket::query()->get()->map(function (Ticket $model) {
$model->type = $model->getRawOriginal('type') - 1;
$model->saveQuietly();
});
TicketType::query()->orderBy('type', 'DESC')->get()->map(function (TicketType $model) {
$model->type = $model->getRawOriginal('type') - 1;
$model->saveQuietly();
DB::transaction(function () {
// 1. Restauration des tickets
DB::table('tickets')->orderBy('id')->chunkById(1000, function ($tickets) {
$ids = $tickets->pluck('id')->toArray();
DB::table('tickets')->whereIn('id', $ids)->decrement('type');
});
$ticketTypes = DB::table('ticket_types')->orderBy('type', 'asc')->get();
foreach ($ticketTypes as $type) {
DB::table('ticket_types')
->where('id', $type->id)
->update(['type' => $type->type - 1]);
}
});
}
};

View file

@ -1,7 +1,7 @@
<?php
use App\Models\Ticket;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;
return new class extends Migration
{
@ -12,10 +12,9 @@ return new class extends Migration
*/
public function up(): void
{
Ticket::query()->where('status', '>', 6)->get()->map(function (Ticket $model) {
$model->status = $model->getRawOriginal('status') + 1;
$model->saveQuietly();
});
DB::table('tickets')
->where('status', '>', 6)
->increment('status');
}
/**
@ -25,9 +24,8 @@ return new class extends Migration
*/
public function down(): void
{
Ticket::query()->where('status', '>', 6)->get()->map(function (Ticket $model) {
$model->status = $model->getRawOriginal('status') - 1;
$model->saveQuietly();
});
DB::table('tickets')
->where('status', '>', 6)
->decrement('status');
}
};

View file

@ -1,9 +1,9 @@
<?php
use Illuminate\Support\Facades\Schema;
use App\Models\Site;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
@ -15,14 +15,31 @@ return new class extends Migration
public function up(): void
{
Schema::table('tickets', function (Blueprint $table) {
$table->integer('order')->after('status')->comment('Ticket order.');
$table->integer('order')->default(0)->after('status')->comment('Ticket order.');
});
Site::query()->get()->map(function ($site) {
$site->tickets->map(function ($ticket, $ticketIndex) {
$ticket->order = $ticketIndex + 1;
$ticket->saveQuietly();
});
DB::transaction(function () {
$currentSiteId = null;
$orderCounter = 1;
DB::table('tickets')
->orderBy('site_id')
->orderBy('id')
->chunk(1000, function ($tickets) use (&$currentSiteId, &$orderCounter) {
foreach ($tickets as $ticket) {
if ($ticket->site_id !== $currentSiteId) {
$currentSiteId = $ticket->site_id;
$orderCounter = 1;
}
DB::table('tickets')
->where('id', $ticket->id)
->update(['order' => $orderCounter]);
$orderCounter++;
}
});
});
}

View file

@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace Domain\Models\ValueObjects;
final readonly class Email
{
public function __construct(
public string $value
) {
$this->validate($value);
}
private function validate(string $email): void
{
if (! filter_var($email, FILTER_VALIDATE_EMAIL)) {
throw new \DomainException('Invalid email address');
}
}
}

View file

@ -75,9 +75,9 @@ spec:
imagePullSecrets:
- name: docker-registry-credentials
securityContext:
runAsUser: 33
runAsGroup: 33
fsGroup: 33
runAsUser: 82
runAsGroup: 82
fsGroup: 82
fsGroupChangePolicy: "OnRootMismatch"
volumes:
- name: storage
@ -161,8 +161,8 @@ spec:
persistentVolumeClaim:
claimName: ${CI_PROJECT_PATH_SLUG}-pvc-storage
securityContext:
runAsUser: 33
runAsGroup: 33
fsGroup: 33
runAsUser: 82
runAsGroup: 82
fsGroup: 82
fsGroupChangePolicy: "OnRootMismatch"
restartPolicy: OnFailure

188
package-lock.json generated
View file

@ -4,6 +4,9 @@
"requires": true,
"packages": {
"": {
"dependencies": {
"meow": "^9.0.0"
},
"devDependencies": {
"@eastdesire/jscolor": "^2.5.1",
"@fortawesome/fontawesome-svg-core": "latest",
@ -76,7 +79,6 @@
"version": "7.22.13",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz",
"integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==",
"dev": true,
"dependencies": {
"@babel/highlight": "^7.22.13",
"chalk": "^2.4.2"
@ -416,7 +418,6 @@
"version": "7.22.20",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz",
"integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==",
"dev": true,
"engines": {
"node": ">=6.9.0"
}
@ -462,7 +463,6 @@
"version": "7.22.20",
"resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz",
"integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==",
"dev": true,
"dependencies": {
"@babel/helper-validator-identifier": "^7.22.20",
"chalk": "^2.4.2",
@ -2542,9 +2542,7 @@
"node_modules/@types/minimist": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.3.tgz",
"integrity": "sha512-ZYFzrvyWUNhaPomn80dsMNgMeXxNWZBdkuG/hWlUvXvbdUH8ZERNBGXnU87McuGcWDsyzX2aChCv/SVN348k3A==",
"dev": true,
"peer": true
"integrity": "sha512-ZYFzrvyWUNhaPomn80dsMNgMeXxNWZBdkuG/hWlUvXvbdUH8ZERNBGXnU87McuGcWDsyzX2aChCv/SVN348k3A=="
},
"node_modules/@types/node": {
"version": "20.8.4",
@ -2558,9 +2556,7 @@
"node_modules/@types/normalize-package-data": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.2.tgz",
"integrity": "sha512-lqa4UEhhv/2sjjIQgjX8B+RBjj47eo0mzGasklVJ78UKGQY1r0VpB9XHDaZZO9qzEFDdy4MrXLuEaSmPrPSe/A==",
"dev": true,
"peer": true
"integrity": "sha512-lqa4UEhhv/2sjjIQgjX8B+RBjj47eo0mzGasklVJ78UKGQY1r0VpB9XHDaZZO9qzEFDdy4MrXLuEaSmPrPSe/A=="
},
"node_modules/@types/semver": {
"version": "7.5.3",
@ -3222,7 +3218,6 @@
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
"integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
"dev": true,
"dependencies": {
"color-convert": "^1.9.0"
},
@ -3262,8 +3257,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz",
"integrity": "sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==",
"dev": true,
"peer": true,
"engines": {
"node": ">=0.10.0"
}
@ -3447,9 +3440,7 @@
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
"integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
"dev": true,
"optional": true,
"peer": true,
"license": "MIT",
"engines": {
"node": ">=6"
}
@ -3458,9 +3449,7 @@
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-6.2.2.tgz",
"integrity": "sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==",
"dev": true,
"optional": true,
"peer": true,
"license": "MIT",
"dependencies": {
"camelcase": "^5.3.1",
"map-obj": "^4.0.0",
@ -3497,7 +3486,6 @@
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
"integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
"dev": true,
"dependencies": {
"ansi-styles": "^3.2.1",
"escape-string-regexp": "^1.0.5",
@ -3562,7 +3550,6 @@
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
"dev": true,
"dependencies": {
"color-name": "1.1.3"
}
@ -3570,8 +3557,7 @@
"node_modules/color-name": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
"integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
"dev": true
"integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="
},
"node_modules/colord": {
"version": "2.9.3",
@ -3750,8 +3736,6 @@
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
"integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==",
"dev": true,
"peer": true,
"engines": {
"node": ">=0.10.0"
}
@ -3760,8 +3744,6 @@
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-1.1.1.tgz",
"integrity": "sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==",
"dev": true,
"peer": true,
"dependencies": {
"decamelize": "^1.1.0",
"map-obj": "^1.0.0"
@ -3777,8 +3759,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz",
"integrity": "sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==",
"dev": true,
"peer": true,
"engines": {
"node": ">=0.10.0"
}
@ -3839,8 +3819,6 @@
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
"integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==",
"dev": true,
"peer": true,
"dependencies": {
"is-arrayish": "^0.2.1"
}
@ -3896,7 +3874,6 @@
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
"integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
"dev": true,
"engines": {
"node": ">=0.8.0"
}
@ -4544,8 +4521,6 @@
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/hard-rejection/-/hard-rejection-2.1.0.tgz",
"integrity": "sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==",
"dev": true,
"peer": true,
"engines": {
"node": ">=6"
}
@ -4554,7 +4529,6 @@
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/has/-/has-1.0.4.tgz",
"integrity": "sha512-qdSAmqLF6209RFj4VVItywPMbm3vWylknmB3nvNiUIs72xAimcM8nVYxYr7ncvZq5qzk9MKIZR8ijqD/1QuYjQ==",
"dev": true,
"engines": {
"node": ">= 0.4.0"
}
@ -4563,7 +4537,6 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
"integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
"dev": true,
"engines": {
"node": ">=4"
}
@ -4581,8 +4554,6 @@
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz",
"integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==",
"dev": true,
"peer": true,
"dependencies": {
"lru-cache": "^6.0.0"
},
@ -4594,8 +4565,6 @@
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
"dev": true,
"peer": true,
"dependencies": {
"yallist": "^4.0.0"
},
@ -4606,9 +4575,7 @@
"node_modules/hosted-git-info/node_modules/yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
"dev": true,
"peer": true
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
},
"node_modules/html-tags": {
"version": "3.3.1",
@ -4677,9 +4644,7 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz",
"integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==",
"dev": true,
"optional": true,
"peer": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
@ -4710,9 +4675,7 @@
"node_modules/is-arrayish": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
"integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==",
"dev": true,
"peer": true
"integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="
},
"node_modules/is-binary-path": {
"version": "2.1.0",
@ -4730,7 +4693,6 @@
"version": "2.13.0",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.0.tgz",
"integrity": "sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==",
"dev": true,
"dependencies": {
"has": "^1.0.3"
},
@ -4791,8 +4753,6 @@
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz",
"integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==",
"dev": true,
"peer": true,
"engines": {
"node": ">=0.10.0"
}
@ -4816,8 +4776,7 @@
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
"dev": true
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
},
"node_modules/js-yaml": {
"version": "4.1.0",
@ -4852,9 +4811,7 @@
"node_modules/json-parse-even-better-errors": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
"integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
"dev": true,
"peer": true
"integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="
},
"node_modules/json-schema-traverse": {
"version": "0.4.1",
@ -4905,8 +4862,6 @@
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
"integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==",
"dev": true,
"peer": true,
"engines": {
"node": ">=0.10.0"
}
@ -4949,9 +4904,7 @@
"node_modules/lines-and-columns": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
"dev": true,
"peer": true
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="
},
"node_modules/locate-path": {
"version": "6.0.0",
@ -5024,8 +4977,6 @@
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.3.0.tgz",
"integrity": "sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==",
"dev": true,
"peer": true,
"engines": {
"node": ">=8"
},
@ -5055,9 +5006,7 @@
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/meow/-/meow-9.0.0.tgz",
"integrity": "sha512-+obSblOQmRhcyBt62furQqRAQpNyWXo8BuQ5bN7dG8wmwQ+vwHKp/rCFD4CrTP8CsDQD1sjoZ94K417XEUk8IQ==",
"dev": true,
"optional": true,
"peer": true,
"license": "MIT",
"dependencies": {
"@types/minimist": "^1.2.0",
"camelcase-keys": "^6.2.2",
@ -5083,9 +5032,7 @@
"version": "0.18.1",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.18.1.tgz",
"integrity": "sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==",
"dev": true,
"optional": true,
"peer": true,
"license": "(MIT OR CC0-1.0)",
"engines": {
"node": ">=10"
},
@ -5140,8 +5087,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz",
"integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==",
"dev": true,
"peer": true,
"engines": {
"node": ">=4"
}
@ -5162,8 +5107,6 @@
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/minimist-options/-/minimist-options-4.1.0.tgz",
"integrity": "sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==",
"dev": true,
"peer": true,
"dependencies": {
"arrify": "^1.0.1",
"is-plain-obj": "^1.1.0",
@ -5228,8 +5171,6 @@
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-3.0.3.tgz",
"integrity": "sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==",
"dev": true,
"peer": true,
"dependencies": {
"hosted-git-info": "^4.0.1",
"is-core-module": "^2.5.0",
@ -5244,8 +5185,6 @@
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
"dev": true,
"peer": true,
"dependencies": {
"yallist": "^4.0.0"
},
@ -5257,8 +5196,6 @@
"version": "7.5.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
"integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
"dev": true,
"peer": true,
"dependencies": {
"lru-cache": "^6.0.0"
},
@ -5272,9 +5209,7 @@
"node_modules/normalize-package-data/node_modules/yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
"dev": true,
"peer": true
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
},
"node_modules/normalize-path": {
"version": "3.0.0",
@ -5369,9 +5304,7 @@
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
"dev": true,
"optional": true,
"peer": true,
"license": "MIT",
"engines": {
"node": ">=6"
}
@ -5392,8 +5325,6 @@
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
"integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==",
"dev": true,
"peer": true,
"dependencies": {
"@babel/code-frame": "^7.0.0",
"error-ex": "^1.3.1",
@ -5411,7 +5342,6 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
"dev": true,
"engines": {
"node": ">=8"
}
@ -5437,8 +5367,7 @@
"node_modules/path-parse": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
"dev": true
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="
},
"node_modules/path-type": {
"version": "4.0.0",
@ -5659,9 +5588,7 @@
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz",
"integrity": "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==",
"dev": true,
"optional": true,
"peer": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
@ -5670,9 +5597,7 @@
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz",
"integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==",
"dev": true,
"optional": true,
"peer": true,
"license": "MIT",
"dependencies": {
"@types/normalize-package-data": "^2.4.0",
"normalize-package-data": "^2.5.0",
@ -5687,9 +5612,7 @@
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz",
"integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==",
"dev": true,
"optional": true,
"peer": true,
"license": "MIT",
"dependencies": {
"find-up": "^4.1.0",
"read-pkg": "^5.2.0",
@ -5706,9 +5629,7 @@
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
"integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
"dev": true,
"optional": true,
"peer": true,
"license": "MIT",
"dependencies": {
"locate-path": "^5.0.0",
"path-exists": "^4.0.0"
@ -5721,9 +5642,7 @@
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
"integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
"dev": true,
"optional": true,
"peer": true,
"license": "MIT",
"dependencies": {
"p-locate": "^4.1.0"
},
@ -5735,9 +5654,7 @@
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
"integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
"dev": true,
"optional": true,
"peer": true,
"license": "MIT",
"dependencies": {
"p-try": "^2.0.0"
},
@ -5752,9 +5669,7 @@
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
"integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
"dev": true,
"optional": true,
"peer": true,
"license": "MIT",
"dependencies": {
"p-limit": "^2.2.0"
},
@ -5766,9 +5681,7 @@
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz",
"integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==",
"dev": true,
"optional": true,
"peer": true,
"license": "(MIT OR CC0-1.0)",
"engines": {
"node": ">=8"
}
@ -5777,17 +5690,13 @@
"version": "2.8.9",
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz",
"integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==",
"dev": true,
"optional": true,
"peer": true
"license": "ISC"
},
"node_modules/read-pkg/node_modules/normalize-package-data": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz",
"integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==",
"dev": true,
"optional": true,
"peer": true,
"license": "BSD-2-Clause",
"dependencies": {
"hosted-git-info": "^2.1.4",
"resolve": "^1.10.0",
@ -5799,9 +5708,7 @@
"version": "5.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
"integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
"dev": true,
"optional": true,
"peer": true,
"license": "ISC",
"bin": {
"semver": "bin/semver"
}
@ -5810,9 +5717,7 @@
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz",
"integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==",
"dev": true,
"optional": true,
"peer": true,
"license": "(MIT OR CC0-1.0)",
"engines": {
"node": ">=8"
}
@ -5833,9 +5738,7 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz",
"integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==",
"dev": true,
"optional": true,
"peer": true,
"license": "MIT",
"dependencies": {
"indent-string": "^4.0.0",
"strip-indent": "^3.0.0"
@ -5929,7 +5832,6 @@
"version": "1.22.8",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz",
"integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==",
"dev": true,
"dependencies": {
"is-core-module": "^2.13.0",
"path-parse": "^1.0.7",
@ -6180,8 +6082,6 @@
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz",
"integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==",
"dev": true,
"peer": true,
"dependencies": {
"spdx-expression-parse": "^3.0.0",
"spdx-license-ids": "^3.0.0"
@ -6190,16 +6090,12 @@
"node_modules/spdx-exceptions": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz",
"integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==",
"dev": true,
"peer": true
"integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A=="
},
"node_modules/spdx-expression-parse": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz",
"integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==",
"dev": true,
"peer": true,
"dependencies": {
"spdx-exceptions": "^2.1.0",
"spdx-license-ids": "^3.0.0"
@ -6208,9 +6104,7 @@
"node_modules/spdx-license-ids": {
"version": "3.0.16",
"resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.16.tgz",
"integrity": "sha512-eWN+LnM3GR6gPu35WxNgbGl8rmY1AEmoMDvL/QD6zYmPWgywxWqJWNdLGT+ke8dKNWrcYgYjPpG5gbTfghP8rw==",
"dev": true,
"peer": true
"integrity": "sha512-eWN+LnM3GR6gPu35WxNgbGl8rmY1AEmoMDvL/QD6zYmPWgywxWqJWNdLGT+ke8dKNWrcYgYjPpG5gbTfghP8rw=="
},
"node_modules/string-width": {
"version": "4.2.3",
@ -6243,9 +6137,7 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz",
"integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==",
"dev": true,
"optional": true,
"peer": true,
"license": "MIT",
"dependencies": {
"min-indent": "^1.0.0"
},
@ -6688,7 +6580,6 @@
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
"integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
"dev": true,
"dependencies": {
"has-flag": "^3.0.0"
},
@ -6737,7 +6628,6 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
"dev": true,
"engines": {
"node": ">= 0.4"
},
@ -6870,9 +6760,7 @@
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-3.0.1.tgz",
"integrity": "sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==",
"dev": true,
"optional": true,
"peer": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
@ -7030,8 +6918,6 @@
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",
"integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==",
"dev": true,
"peer": true,
"dependencies": {
"spdx-correct": "^3.0.0",
"spdx-expression-parse": "^3.0.0"
@ -7656,8 +7542,6 @@
"version": "20.2.9",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz",
"integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==",
"dev": true,
"peer": true,
"engines": {
"node": ">=10"
}

View file

@ -55,5 +55,8 @@
"vue": "latest",
"vue-select": "^4.0.0-beta.6",
"vue-tsc": "1.6.0"
},
"dependencies": {
"meow": "^9.0.0"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

After

Width:  |  Height:  |  Size: 130 KiB

View file

@ -1,13 +1,13 @@
<p>Bonjour,</p>
<p>Un nouveau ticket (n°{{ $ticket->id }}) a été créé sur le support :</p>
<p>Un nouveau ticket (n°{{ $ticketId }}) a été créé sur le support :</p>
<ul>
<li>Projet : {{ $ticket->site->name }}</li>
<li>Titre : {{ $ticket->title }}</li>
<li>Type : {{ $ticket->type->label() }}</li>
<li>Créé par : {{ $ticket->user()->withTrashed()->firstOrFail()->name }}</li>
<li>Projet : {{ $siteName }}</li>
<li>Titre : {{ $ticketTitle }}</li>
<li>Type : {{ $ticketType->label() }}</li>
<li>Créé par : {{ $userName }}</li>
</ul>
<p>
Description :<br>
{!! clean(nl2br($ticket->text)) !!}
Description :<br>
{!! clean(nl2br($ticketDescription)) !!}
</p>
<p>Lien vers le ticket : <a href="{{ route('bo.ticket.show', $ticket->id) }}" traget="_blank">Ouvrir dans un nouvel onglet</a></p>
<p>Lien vers le ticket : <a href="{{ route('bo.ticket.show', $ticketId) }}" traget="_blank">Ouvrir dans un nouvel onglet</a></p>

View file

@ -12,13 +12,13 @@ use App\Http\Controllers\Bo\PackageController;
use App\Http\Controllers\Bo\SiteController;
use App\Http\Controllers\Bo\TicketController;
use App\Http\Controllers\Bo\TicketTypeController;
use App\Http\Controllers\Bo\BrevoController;
use App\Http\Controllers\Bo\BrevoWebhookController;
use App\Http\Controllers\Bo\UserController;
use App\Http\Middleware\SecureHeaders;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Route;
Route::post('/brevo-in', [BrevoController::class, 'createTicket']);
Route::post('/brevo-in', [BrevoWebhookController::class, 'webhookCallReceived']);
Route::middleware('admin-secure')
->group(function () {
@ -130,7 +130,8 @@ Route::middleware('admin-secure')
->middleware('can:estimate,ticket')
->name('estimateSubmit');
Route::post('/{ticket}/estimate/answer', [
EstimationController::class, 'estimatePendingSubmit'
EstimationController::class,
'estimatePendingSubmit'
])
->middleware('can:validateEstimate,ticket')
->name('estimatePendingSubmit');

View file

@ -15,6 +15,6 @@ class ExampleTest extends TestCase
{
$response = $this->get('/');
$response->assertStatus(200);
$response->assertStatus(302);
}
}

View file

@ -1,18 +0,0 @@
<?php
namespace Tests\Unit;
use PHPUnit\Framework\TestCase;
class ExampleTest extends TestCase
{
/**
* A basic test example.
*
* @return void
*/
public function testBasicTest()
{
$this->assertTrue(true);
}
}

View file

@ -0,0 +1,78 @@
<?php
declare(strict_types=1);
namespace Tests\Unit\Services;
use App\Dto\BrevoWebHookDto;
use App\Enums\Tickets\TicketStatusEnum;
use App\Enums\Tickets\TicketTypeEnum;
use App\Models\Site;
use App\Models\Ticket;
use App\Models\User;
use App\Services\AutomaticTicketCreator;
use App\Services\BrevoWebookParser;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Mockery;
use Mockery\MockInterface;
use Tests\TestCase;
class AutomaticTicketCreatorTest extends TestCase
{
use RefreshDatabase;
private AutomaticTicketCreator $creator;
private BrevoWebookParser|MockInterface $parserMock;
protected function setUp(): void
{
parent::setUp();
$this->parserMock = Mockery::mock(BrevoWebookParser::class);
$this->creator = new AutomaticTicketCreator($this->parserMock);
}
public function testCanCreateTicketWithProjectManagerAndBrand(): void
{
// GIVEN
$site = Site::factory()->create();
$manager = User::factory()->create();
$rawHtml = 'Simili HTML dynamique';
$dto = new BrevoWebHookDto(
plainTextContent: 'Contenu parsé',
brand: 'LDC Service'
);
$this->parserMock
->shouldReceive('parseRawHtml')
->once()
->with($rawHtml)
->andReturn($dto);
// WHEN
$ticket = $this->creator->createTicket($site, $manager, $rawHtml);
// THEN
$this->assertInstanceOf(Ticket::class, $ticket);
$this->assertEquals($site->id, $ticket->site_id);
$this->assertEquals(TicketTypeEnum::mailing, $ticket->type);
$this->assertEquals(TicketStatusEnum::estimateTodo, $ticket->status);
$this->assertEquals('Demande de mail en masse pour LDC Service', $ticket->title);
$this->assertStringContainsString('Contenu :', $ticket->text);
$this->assertStringContainsString('Contenu parsé', $ticket->text);
$this->assertFalse($ticket->is_internal);
$this->assertTrue($ticket->user->is($manager));
$this->assertTrue($ticket->assignedUser->is($manager));
$this->assertDatabaseHas('tickets', [
'id' => $ticket->id,
'created_by' => $manager->id,
'assigned_to' => $manager->id,
'title' => 'Demande de mail en masse pour LDC Service'
]);
}
}

View file

@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
namespace Tests\Unit\Services;
use App\Dto\BrevoWebHookDto;
use App\Services\BrevoWebookParser;
use PHPUnit\Framework\TestCase;
class BrevoWebookParserTest extends TestCase
{
private BrevoWebookParser $parser;
protected function setUp(): void
{
parent::setUp();
$this->parser = new BrevoWebookParser();
}
public function testCanExtractDataWithRegexUsingPipedFormat(): void
{
// GIVEN
$rawHtml = <<<TEXT
Détails de la demande :\r\n\r\nOutil envoi de mails en masse\r\n\r\n\r\nDescription\r\nReponse\r\n\r\n\r\nMatricule et Nom du demandeur\r\nK00112323 : toto jean\r\n\r\n\r\nEmail du demandeur\r\ntoto.jean@ldc.fr : Jean\r\n\r\n\r\nMarque / Filiale\r\nLDC Service\r\n\r\n\r\nPour quels besoins avez-vous besoin d'envoyer des emails en masse ?\r\nL'aigüe ambiguïté exige qu'on s'y penche. Œil, cœur, bœuf: la naïveté est un mythe. Test accents: àéèïôùç. Maman a acheté 15,99&euro; de pâte (soit 10%)! Symboles: & ~ # | \\ ^ @ % * $ £ ¥ ¤ + = - _ Parenthèses: (test) [test] {test} &lt;test&gt;. Cyrillique: Добрый день. Arabe: مرحبا. Emojis: 😀🚀🐛. Test XSS: &lt;script&gt;alert('XSS');&lt;/script&gt; Test SQL: SELECT * FROM users WHERE nom = 'O\\'Brian'; DROP TABLE test; -- Fin du texte 1. 1234567890. Remplissage pour atteindre un peu moins de 500 chars.\r\n\r\n\r\nDate de première envoi souhaité\r\n30/06/2026\r\n\r\n\r\nCommentaire\r\nTest de sauts de ligne pour le second champ.\r\n\r\n\tTabulation et espaces multiples.\r\nJSON :\r\n{'id': 999, 'name': 'L'Étranger', 'tags': ['&lt;img src=x onerror=alert(1)&gt;']}\r\n\r\nVérification Regex :\r\nEmail: jean.dupont+test@exemple.co.uk\r\nURL: https://www.exemple.com/api/v1/data?p=1#ancre\r\nTél: +33(0)123456789\r\nIPv4: 192.168.1.254 | IPv6: 2001:db8::8a2e:370:7334\r\n\r\nCe second bloc vérifie que les données structurées et les liens survivent au transfert. Remplissage pour atteindre la limite de sept cents caractères sans la dépasser. Chinois: 这是一个测试. Japonais: こんにちは世界. Test des retours chariots invisibles. Fin du second bloc de test.\r\n\r\n\r\n
TEXT;
// WHEN
$dto = $this->parser->parseRawHtml($rawHtml);
// THEN
$this->assertInstanceOf(BrevoWebHookDto::class, $dto);
$this->assertEquals('LDC Service', $dto->brand);
$this->assertEquals($rawHtml, $dto->plainTextContent);
}
public function testHandlesMissingFieldsGracefully(): void
{
// GIVEN
$rawHtml = "<p>Contenu classique sans le formalisme du tableau markdown</p>";
// WHEN
$dto = $this->parser->parseRawHtml($rawHtml);
// THEN
$this->assertNull($dto->brand);
$this->assertEquals("Contenu classique sans le formalisme du tableau markdown", $dto->plainTextContent);
}
}