Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
36 / 36
100.00% covered (success)
100.00%
3 / 3
CRAP
100.00% covered (success)
100.00%
1 / 1
TransactionFinder
100.00% covered (success)
100.00%
36 / 36
100.00% covered (success)
100.00%
3 / 3
16
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 findByAccountId
100.00% covered (success)
100.00%
31 / 31
100.00% covered (success)
100.00%
1 / 1
13
 isValidDate
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2
3declare(strict_types=1);
4
5namespace App\Domain\Transaction\Service;
6
7use App\Domain\Exception\NotFoundException;
8use App\Domain\Exception\ValidationException;
9use App\Domain\Transaction\Data\TransactionData;
10use App\Domain\Transaction\Repository\TransactionRepository;
11
12use function in_array;
13
14class TransactionFinder
15{
16    private const VALID_TYPES = [
17        'investment',
18        'withdrawal',
19        'transfer_in',
20        'transfer_out',
21        'interest',
22        'fee',
23        'loan_payment',
24    ];
25
26    private const MAX_LIMIT = 100;
27
28    public function __construct(
29        private readonly TransactionRepository $repository,
30    ) {}
31
32    /**
33     * Find transactions by account ID with pagination and filtering.
34     *
35     * @param int $accountId
36     * @param int $page
37     * @param int $limit
38     * @param ?string $type
39     * @param ?string $startDate
40     * @param ?string $endDate
41     *
42     * @throws NotFoundException If account does not exist
43     * @throws ValidationException If parameters fail validation
44     *
45     * @return array{data: list<TransactionData>, total: int, page: int, limit: int}
46     */
47    public function findByAccountId(
48        int $accountId,
49        int $page = 1,
50        int $limit = 10,
51        ?string $type = null,
52        ?string $startDate = null,
53        ?string $endDate = null,
54    ): array {
55        // Validate account exists
56        if (!$this->repository->accountExists($accountId)) {
57            throw new NotFoundException("Account with ID {$accountId} not found");
58        }
59
60        // Validate page
61        if ($page < 1) {
62            throw new ValidationException('Page must be at least 1');
63        }
64
65        // Validate and clamp limit
66        if ($limit < 1) {
67            throw new ValidationException('Limit must be at least 1');
68        }
69        $limit = min($limit, self::MAX_LIMIT);
70
71        // Validate transaction type if provided
72        if ($type !== null && !in_array($type, self::VALID_TYPES, true)) {
73            throw new ValidationException(
74                'Invalid transaction type. Must be one of: ' . implode(', ', self::VALID_TYPES),
75            );
76        }
77
78        // Validate date format if provided
79        if ($startDate !== null && !$this->isValidDate($startDate)) {
80            throw new ValidationException('Invalid startDate format. Use YYYY-MM-DD');
81        }
82
83        if ($endDate !== null && !$this->isValidDate($endDate)) {
84            throw new ValidationException('Invalid endDate format. Use YYYY-MM-DD');
85        }
86
87        // Validate date range
88        if ($startDate !== null && $endDate !== null && $startDate > $endDate) {
89            throw new ValidationException('startDate cannot be after endDate');
90        }
91
92        $result = $this->repository->findByAccountId(
93            accountId: $accountId,
94            page: $page,
95            limit: $limit,
96            type: $type,
97            startDate: $startDate,
98            endDate: $endDate,
99        );
100
101        return [
102            'data' => $result['data'],
103            'total' => $result['total'],
104            'page' => $page,
105            'limit' => $limit,
106        ];
107    }
108
109    /**
110     * Validate date string format (YYYY-MM-DD).
111     *
112     * @param string $date
113     */
114    private function isValidDate(string $date): bool
115    {
116        if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $date)) {
117            return false;
118        }
119
120        $parts = explode('-', $date);
121
122        return checkdate((int)$parts[1], (int)$parts[2], (int)$parts[0]);
123    }
124}