Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
84.85% covered (warning)
84.85%
28 / 33
33.33% covered (danger)
33.33%
1 / 3
CRAP
0.00% covered (danger)
0.00%
0 / 1
UpdateMyNotificationPreferencesAction
84.85% covered (warning)
84.85%
28 / 33
33.33% covered (danger)
33.33%
1 / 3
14.68
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 __invoke
93.75% covered (success)
93.75%
15 / 16
0.00% covered (danger)
0.00%
0 / 1
4.00
 validatePreferences
75.00% covered (warning)
75.00%
12 / 16
0.00% covered (danger)
0.00%
0 / 1
10.27
1<?php
2
3declare(strict_types=1);
4
5namespace App\Action\Notification;
6
7use App\Domain\Auth\Data\UserAuthData;
8use App\Domain\Exception\AuthenticationException;
9use App\Domain\Exception\BadRequestException;
10use App\Domain\Notification\Service\NotificationPreferenceService;
11use App\Renderer\JsonRenderer;
12use InvalidArgumentException;
13use Psr\Http\Message\ResponseInterface;
14use Psr\Http\Message\ServerRequestInterface;
15
16/**
17 * PUT /api/me/notification-preferences
18 *
19 * Body: { "preferences": [ {"notificationType": string, "channel": string, "enabled": bool}, ... ] }
20 *
21 * Bulk-updates the requester's preferences. Service rejects types
22 * outside the requester's role (e.g., an investor toggling an admin.*
23 * preference) — that surfaces as a 400.
24 */
25final readonly class UpdateMyNotificationPreferencesAction
26{
27    public function __construct(
28        private NotificationPreferenceService $preferences,
29        private JsonRenderer $renderer,
30    ) {}
31
32    public function __invoke(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
33    {
34        $user = $request->getAttribute('user');
35        if (!$user instanceof UserAuthData) {
36            throw new AuthenticationException('User not authenticated');
37        }
38
39        $body = (array)$request->getParsedBody();
40        $rawPreferences = $body['preferences'] ?? null;
41
42        if (!is_array($rawPreferences)) {
43            throw new BadRequestException('Body must include a "preferences" array.');
44        }
45
46        $preferences = $this->validatePreferences($rawPreferences);
47
48        try {
49            $this->preferences->updatePreferences($user->userId, $user->role, $preferences);
50        } catch (InvalidArgumentException $e) {
51            throw new BadRequestException($e->getMessage());
52        }
53
54        // Return the freshly-updated set so clients don't need a follow-up GET.
55        $items = $this->preferences->getPreferencesForUser($user->userId, $user->role);
56
57        return $this->renderer->json($response, [
58            'success' => true,
59            'data' => ['preferences' => $items],
60        ]);
61    }
62
63    /**
64     * Coerce raw request input into the typed shape the service expects.
65     * Bad shapes raise BadRequest at the boundary so the service contract
66     * stays clean.
67     *
68     * @param array<int|string, mixed> $raw
69     * @return list<array{notificationType: string, channel: string, enabled: bool}>
70     */
71    private function validatePreferences(array $raw): array
72    {
73        $out = [];
74        foreach (array_values($raw) as $i => $entry) {
75            if (!is_array($entry)) {
76                throw new BadRequestException(sprintf('preferences[%d] must be an object.', $i));
77            }
78            if (!isset($entry['notificationType']) || !is_string($entry['notificationType'])) {
79                throw new BadRequestException(sprintf('preferences[%d].notificationType must be a string.', $i));
80            }
81            if (!isset($entry['channel']) || !is_string($entry['channel'])) {
82                throw new BadRequestException(sprintf('preferences[%d].channel must be a string.', $i));
83            }
84            if (!array_key_exists('enabled', $entry) || !is_bool($entry['enabled'])) {
85                throw new BadRequestException(sprintf('preferences[%d].enabled must be a boolean.', $i));
86            }
87
88            $out[] = [
89                'notificationType' => $entry['notificationType'],
90                'channel' => $entry['channel'],
91                'enabled' => $entry['enabled'],
92            ];
93        }
94
95        return $out;
96    }
97}