Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 26
0.00% covered (danger)
0.00%
0 / 3
CRAP
0.00% covered (danger)
0.00%
0 / 1
TransactionService
0.00% covered (danger)
0.00%
0 / 26
0.00% covered (danger)
0.00%
0 / 3
132
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 createTransaction
0.00% covered (danger)
0.00%
0 / 24
0.00% covered (danger)
0.00%
0 / 1
90
 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\NotFoundException;
8use App\Domain\Transaction\Data\TransactionData;
9use App\Domain\Transaction\Repository\TransactionRepository;
10use DomainException;
11use InvalidArgumentException;
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        'deposit',
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 DomainException If account does not exist or is not active
44     * @throws InvalidArgumentException If parameters are invalid
45     *
46     * @return TransactionData The created transaction
47     */
48    public function createTransaction(
49        int $accountId,
50        string $transactionType,
51        string $amount,
52        ?string $description = null,
53    ): TransactionData {
54        // Validate transaction type
55        if (!in_array($transactionType, self::VALID_TYPES, true)) {
56            throw new InvalidArgumentException(
57                'Invalid transaction type. Must be one of: ' . implode(', ', self::VALID_TYPES),
58            );
59        }
60
61        // Validate amount is positive
62        $amountFloat = (float)$amount;
63        if ($amountFloat <= 0) {
64            throw new InvalidArgumentException('Amount must be greater than zero');
65        }
66
67        // Validate account exists
68        if (!$this->repository->accountExists($accountId)) {
69            throw new NotFoundException("Account with ID {$accountId} not found");
70        }
71
72        // Check account status (must be active or pending for deposits)
73        $accountStatus = $this->repository->getAccountStatus($accountId);
74
75        if ($accountStatus === 'closed') {
76            throw new DomainException('Cannot create transaction on a closed account');
77        }
78
79        if ($accountStatus === 'frozen' && $transactionType !== 'interest') {
80            throw new DomainException('Cannot create transaction on a frozen account');
81        }
82
83        // For withdrawals, check sufficient balance
84        if (in_array($transactionType, ['withdrawal', 'transfer_out', 'fee', 'loan_payment'], true)) {
85            $availableBalance = $this->repository->getAccountAvailableBalance($accountId);
86            if ($amountFloat > (float)$availableBalance) {
87                throw new DomainException('Insufficient available balance for this transaction');
88            }
89        }
90
91        // Create the transaction
92        return $this->repository->create(
93            accountId: $accountId,
94            transactionType: $transactionType,
95            amount: $amount,
96            description: $description,
97        );
98    }
99
100    /**
101     * Get valid transaction types.
102     *
103     * @return array<string>
104     */
105    public function getValidTypes(): array
106    {
107        return self::VALID_TYPES;
108    }
109}