Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
57.69% covered (warning)
57.69%
15 / 26
33.33% covered (danger)
33.33%
1 / 3
CRAP
0.00% covered (danger)
0.00%
0 / 1
TransactionService
57.69% covered (warning)
57.69%
15 / 26
33.33% covered (danger)
33.33%
1 / 3
20.16
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
 createTransaction
58.33% covered (warning)
58.33%
14 / 24
0.00% covered (danger)
0.00%
0 / 1
14.86
 getValidTypes
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3declare(strict_types=1);
4
5namespace App\Domain\Transaction\Service;
6
7use App\Domain\Exception\ConflictException;
8use App\Domain\Exception\NotFoundException;
9use App\Domain\Exception\ValidationException;
10use App\Domain\Transaction\Data\TransactionData;
11use App\Domain\Transaction\Repository\TransactionRepository;
12
13use function in_array;
14
15/**
16 * Service for creating and managing transactions.
17 */
18final readonly class TransactionService
19{
20    private const VALID_TYPES = [
21        'investment',
22        'withdrawal',
23        'transfer_in',
24        'transfer_out',
25        'interest',
26        'fee',
27        'loan_disbursement',
28        'loan_payment',
29    ];
30
31    public function __construct(
32        private TransactionRepository $repository,
33    ) {}
34
35    /**
36     * Create a new transaction.
37     *
38     * @param int $accountId The account ID
39     * @param string $transactionType The type of transaction
40     * @param string $amount The transaction amount (positive value)
41     * @param string|null $description Optional description
42     *
43     * @throws ConflictException If account is closed or frozen
44     * @throws NotFoundException If account does not exist
45     * @throws ValidationException If parameters fail validation
46     *
47     * @return TransactionData The created transaction
48     */
49    public function createTransaction(
50        int $accountId,
51        string $transactionType,
52        string $amount,
53        ?string $description = null,
54    ): TransactionData {
55        // Validate transaction type
56        if (!in_array($transactionType, self::VALID_TYPES, true)) {
57            throw new ValidationException(
58                'Invalid transaction type. Must be one of: ' . implode(', ', self::VALID_TYPES),
59            );
60        }
61
62        // Validate amount is positive
63        $amountFloat = (float)$amount;
64        if ($amountFloat <= 0) {
65            throw new ValidationException('Amount must be greater than zero');
66        }
67
68        // Validate account exists
69        if (!$this->repository->accountExists($accountId)) {
70            throw new NotFoundException("Account with ID {$accountId} not found");
71        }
72
73        // Check account status (must be active or pending for investments)
74        $accountStatus = $this->repository->getAccountStatus($accountId);
75
76        if ($accountStatus === 'closed') {
77            throw new ConflictException('Cannot create transaction on a closed account');
78        }
79
80        if ($accountStatus === 'frozen' && $transactionType !== 'interest') {
81            throw new ConflictException('Cannot create transaction on a frozen account');
82        }
83
84        // For withdrawals, check sufficient balance
85        if (in_array($transactionType, ['withdrawal', 'transfer_out', 'fee', 'loan_payment'], true)) {
86            $availableBalance = $this->repository->getAccountAvailableBalance($accountId);
87            if ($amountFloat > (float)$availableBalance) {
88                throw new ValidationException('Insufficient available balance for this transaction');
89            }
90        }
91
92        // Create the transaction
93        return $this->repository->create(
94            accountId: $accountId,
95            transactionType: $transactionType,
96            amount: $amount,
97            description: $description,
98        );
99    }
100
101    /**
102     * Get valid transaction types.
103     *
104     * @return array<string>
105     */
106    public function getValidTypes(): array
107    {
108        return self::VALID_TYPES;
109    }
110}