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