Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
33.66% covered (danger)
33.66%
34 / 101
50.00% covered (danger)
50.00%
4 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
RegisterAction
33.66% covered (danger)
33.66%
34 / 101
50.00% covered (danger)
50.00%
4 / 8
124.38
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
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
2.03
 isCompleteRegistration
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 handleCompleteRegistration
0.00% covered (danger)
0.00%
0 / 31
0.00% covered (danger)
0.00%
0 / 1
6
 handleSimpleRegistration
100.00% covered (success)
100.00%
22 / 22
100.00% covered (success)
100.00%
1 / 1
1
 validateSimpleRegistrationInput
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 validateCompleteRegistrationInput
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
12
 getClientIp
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
30
1<?php
2
3declare(strict_types=1);
4
5namespace App\Action\Auth;
6
7use App\Domain\Auth\Service\AuthService;
8use App\Domain\Auth\Service\RegistrationService;
9use InvalidArgumentException;
10use Psr\Http\Message\ResponseInterface;
11use Psr\Http\Message\ServerRequestInterface;
12
13use function array_key_exists;
14use function json_encode;
15use function sprintf;
16
17/**
18 * Register new user.
19 *
20 * POST /api/auth/register
21 *
22 * Supports two modes:
23 * 1. Simple registration (username, email, password) - creates user only
24 * 2. Complete registration (includes investor data) - creates investor, user, and account atomically
25 */
26final readonly class RegisterAction
27{
28    public function __construct(
29        private readonly AuthService $authService,
30        private readonly RegistrationService $registrationService,
31    ) {}
32
33    public function __invoke(
34        ServerRequestInterface $request,
35        ResponseInterface $response,
36    ): ResponseInterface {
37        // Get request data
38        $data = (array)$request->getParsedBody();
39
40        // Determine registration mode based on presence of investor fields
41        $isCompleteRegistration = $this->isCompleteRegistration($data);
42
43        if ($isCompleteRegistration) {
44            return $this->handleCompleteRegistration($request, $response, $data);
45        }
46
47        return $this->handleSimpleRegistration($response, $data);
48    }
49
50    /**
51     * Check if this is a complete registration (with investor data).
52     *
53     * @param array<string, mixed> $data
54     */
55    private function isCompleteRegistration(array $data): bool
56    {
57        // If firstName is present, assume complete registration
58        return array_key_exists('firstName', $data) && !empty($data['firstName']);
59    }
60
61    /**
62     * Handle complete registration with investor and account creation.
63     *
64     * @param array<string, mixed> $data
65     * @param ServerRequestInterface $request
66     * @param ResponseInterface $response
67     */
68    private function handleCompleteRegistration(
69        ServerRequestInterface $request,
70        ResponseInterface $response,
71        array $data,
72    ): ResponseInterface {
73        // Validate required fields for complete registration
74        $this->validateCompleteRegistrationInput($data);
75
76        // Get client info for session tracking
77        $ipAddress = $this->getClientIp($request);
78        $userAgent = $request->getHeaderLine('User-Agent') ?: null;
79
80        // Perform atomic registration
81        $result = $this->registrationService->registerComplete(
82            data: $data,
83            ipAddress: $ipAddress,
84            userAgent: $userAgent,
85        );
86
87        $responseData = [
88            'success' => true,
89            'message' => 'Registration completed successfully',
90            'data' => [
91                'user' => [
92                    'userId' => $result['user']->userId,
93                    'username' => $result['user']->username,
94                    'email' => $result['user']->email,
95                    'role' => $result['user']->role,
96                ],
97                'investorId' => $result['investorId'],
98                'accountId' => $result['accountId'],
99                'accountNumber' => $result['accountNumber'],
100                'accessToken' => $result['tokens']->accessToken,
101                'refreshToken' => $result['tokens']->refreshToken,
102                'tokenType' => $result['tokens']->tokenType,
103                'expiresIn' => $result['tokens']->expiresIn,
104            ],
105        ];
106
107        $response->getBody()->write((string)json_encode($responseData));
108
109        return $response
110            ->withHeader('Content-Type', 'application/json')
111            ->withStatus(201);
112    }
113
114    /**
115     * Handle simple registration (user only, for admin/special cases).
116     *
117     * @param array<string, mixed> $data
118     * @param ResponseInterface $response
119     */
120    private function handleSimpleRegistration(
121        ResponseInterface $response,
122        array $data,
123    ): ResponseInterface {
124        $this->validateSimpleRegistrationInput($data);
125        $user = $this->authService->register(
126            username: $data['username'],
127            email: $data['email'],
128            password: $data['password'],
129            investorId: $data['investorId'] ?? null,
130            role: $data['role'] ?? 'investor',
131        );
132
133        // Return success response (don't expose the user password hash)
134        $responseData = [
135            'success' => true,
136            'message' => 'User registered successfully',
137            'data' => [
138                'userId' => $user->userId,
139                'username' => $user->username,
140                'email' => $user->email,
141                'role' => $user->role,
142            ],
143        ];
144
145        $response->getBody()->write((string)json_encode($responseData));
146
147        return $response
148            ->withHeader('Content-Type', 'application/json')
149            ->withStatus(201);
150    }
151
152    /**
153     * Validate simple registration input.
154     *
155     * @param array<string, mixed> $data
156     *
157     * @throws InvalidArgumentException
158     */
159    private function validateSimpleRegistrationInput(array $data): void
160    {
161        $requiredFields = ['username', 'email', 'password'];
162
163        foreach ($requiredFields as $field) {
164            if (empty($data[$field])) {
165                throw new InvalidArgumentException(
166                    sprintf('Missing required field: %s', $field),
167                );
168            }
169        }
170    }
171
172    /**
173     * Validate complete registration input (basic presence check).
174     * Detailed validation is done in RegistrationService.
175     *
176     * @param array<string, mixed> $data
177     *
178     * @throws InvalidArgumentException
179     */
180    private function validateCompleteRegistrationInput(array $data): void
181    {
182        $requiredFields = [
183            'username',
184            'email',
185            'password',
186            'firstName',
187            'lastName',
188            'dateOfBirth',
189            'phone',
190            'addressLine1',
191            'city',
192            'state',
193            'zipCode',
194            'country',
195        ];
196
197        foreach ($requiredFields as $field) {
198            if (empty($data[$field])) {
199                throw new InvalidArgumentException(
200                    sprintf('Missing required field: %s', $field),
201                );
202            }
203        }
204    }
205    /**
206     * Get client IP address.
207     * @param ServerRequestInterface $request
208     */
209    private function getClientIp(ServerRequestInterface $request): ?string
210    {
211        $serverParams = $request->getServerParams();
212
213        $headers = [
214            'HTTP_CF_CONNECTING_IP',
215            'HTTP_X_REAL_IP',
216            'HTTP_X_FORWARDED_FOR',
217            'REMOTE_ADDR',
218        ];
219
220        foreach ($headers as $header) {
221            if (!empty($serverParams[$header])) {
222                $ip = $serverParams[$header];
223
224                if ($header === 'HTTP_X_FORWARDED_FOR') {
225                    $ips = explode(',', $ip);
226                    $ip = trim($ips[0]);
227                }
228
229                if (filter_var($ip, FILTER_VALIDATE_IP)) {
230                    return $ip;
231                }
232            }
233        }
234
235        return null;
236    }
237}