Completed
Push — master ( 3b66c0...5eff18 )
by Sylvain
14:13
created

AccountRepository::getNextCodeAvailable()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 4
c 1
b 0
f 0
nc 1
nop 0
dl 0
loc 7
ccs 0
cts 0
cp 0
crap 2
rs 10
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Application\Repository;
6
7
use Application\DBAL\Types\AccountTypeType;
8
use Application\Model\Account;
9
use Application\Model\User;
10
use Application\Utility;
11
use Doctrine\DBAL\Exception\InvalidArgumentException;
12
use Money\Money;
13
14
class AccountRepository extends AbstractRepository implements LimitedAccessSubQueryInterface
15
{
16
    private const PARENT_ACCOUNT_ID_FOR_USER = 10011;
17
    const ACCOUNT_ID_FOR_BANK = 10025;
18
19
    /**
20
     * @var string[]
21
     */
22
    private $totalBalanceCache = [];
23
24
    /**
25
     * Clear all caches
26
     */
27 1
    public function clearCache(): void
28
    {
29 1
        $this->totalBalanceCache = [];
30 1
    }
31
32
    /**
33
     * Returns pure SQL to get ID of all objects that are accessible to given user.
34
     *
35
     * @param null|User $user
36
     *
37
     * @return string
38
     */
39 12
    public function getAccessibleSubQuery(?User $user): string
40
    {
41 12
        if (!$user) {
42 1
            return '-1';
43
        }
44
45 11
        if (in_array($user->getRole(), [User::ROLE_RESPONSIBLE, User::ROLE_ADMINISTRATOR], true)) {
46 2
            return $this->getAllIdsQuery();
47
        }
48
49 9
        return $this->getAllIdsForOwnerQuery($user);
50
    }
51
52
    /**
53
     * Unsecured way to get a account from its ID.
54
     *
55
     * This should only be used in tests or controlled environment.
56
     *
57
     * @param int $id
58
     *
59
     * @throws \Exception
60
     *
61
     * @return Account
62
     */
63 13
    public function getOneById(int $id): Account
64
    {
65
        $account = $this->getAclFilter()->runWithoutAcl(function () use ($id) {
66 13
            return $this->findOneById($id);
67 13
        });
68
69 13
        if (!$account) {
70 1
            throw new \Exception('Account #' . $id . ' not found');
71
        }
72
73 13
        return $account;
74
    }
75
76
    /**
77
     * This will return, and potentially create, an account for the given user
78
     *
79
     * @param User $user
80
     *
81
     * @return Account
82
     */
83 17
    public function getOrCreate(User $user): Account
84
    {
85
        // If an account already exists, because getOrCreate was called once before without flushing in between,
86
        // then can return immediately
87 17
        if ($user->getAccount()) {
88 12
            return $user->getAccount();
89
        }
90
91
        $account = $this->getAclFilter()->runWithoutAcl(function () use ($user) {
92 8
            return $this->findOneByOwner($user);
0 ignored issues
show
Bug introduced by
The method findOneByOwner() does not exist on Application\Repository\AccountRepository. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

92
            return $this->/** @scrutinizer ignore-call */ findOneByOwner($user);
Loading history...
93 8
        });
94
95 8
        if (!$account) {
96 8
            $account = new Account();
97 8
            $this->getEntityManager()->persist($account);
98 8
            $account->setOwner($user);
99 8
            $account->setType(AccountTypeType::LIABILITY);
100 8
            $account->setName($user->getName());
101
102 8
            $maxCode = $this->getEntityManager()->getConnection()->fetchColumn('SELECT MAX(code) FROM account WHERE parent_id = ' . self::PARENT_ACCOUNT_ID_FOR_USER);
103 8
            $newCode = ++$maxCode;
104 8
            $account->setCode($newCode);
105
106 8
            $parent = $this->getOneById(self::PARENT_ACCOUNT_ID_FOR_USER);
107 8
            $account->setParent($parent);
108
        }
109
110 8
        return $account;
111
    }
112
113
    /**
114
     * Sum balance by account type
115
     *
116
     * @API\Input(type="AccountType")
117
     *
118
     * @param string $accountType
119
     *
120
     * @return Money
121
     */
122 1
    public function totalBalanceByType(string $accountType): Money
123
    {
124 1
        $qb = $this->getEntityManager()->getConnection()->createQueryBuilder()
125 1
            ->select('SUM(balance)')
126 1
            ->from($this->getClassMetadata()->getTableName())
127 1
            ->where('type = :type');
128
129 1
        $qb->setParameter('type', $accountType);
130
131 1
        $result = $qb->execute();
132
133 1
        return Money::CHF($result->fetchColumn());
0 ignored issues
show
Bug introduced by
It seems like $result->fetchColumn() can also be of type false; however, parameter $amount of Money\Money::CHF() does only seem to accept integer|string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

133
        return Money::CHF(/** @scrutinizer ignore-type */ $result->fetchColumn());
Loading history...
134
    }
135
136
    /**
137
     * Calculate the total balance of all child accounts of a group account
138
     *
139
     * @param Account $parentAccount
140
     *
141
     * @throws \Doctrine\DBAL\DBALException
142
     *
143
     * @return Money
144
     */
145 1
    public function totalBalanceByParent(Account $parentAccount): Money
146
    {
147 1
        if ($parentAccount->getType() !== AccountTypeType::GROUP) {
148 1
            throw new InvalidArgumentException(sprintf(
149 1
                'Cannot compute total balance for Account #%d of type %s',
150 1
                $parentAccount->getId(),
151 1
                $parentAccount->getType()
152
            ));
153
        }
154
155 1
        $cacheKey = Utility::getCacheKey(func_get_args());
156 1
        if (array_key_exists($cacheKey, $this->totalBalanceCache)) {
157
            return $this->totalBalanceCache[$cacheKey];
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->totalBalanceCache[$cacheKey] returns the type string which is incompatible with the type-hinted return Money\Money.
Loading history...
158
        }
159
160 1
        $connection = $this->getEntityManager()->getConnection();
161
162 1
        $sql = 'WITH RECURSIVE child AS
163
          (SELECT id, parent_id, `type`, balance
164
           FROM account WHERE id = ?
165
           UNION
166
           SELECT account.id, account.parent_id, account.type, account.balance
167
           FROM account
168
           JOIN child ON account.parent_id = child.id)
169
        SELECT SUM(balance) FROM child WHERE `type` <> ?';
170
171 1
        $result = $connection->executeQuery($sql, [$parentAccount->getId(), AccountTypeType::GROUP]);
172
173 1
        return Money::CHF($result->fetchColumn());
0 ignored issues
show
Bug introduced by
It seems like $result->fetchColumn() can also be of type false; however, parameter $amount of Money\Money::CHF() does only seem to accept integer|string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

173
        return Money::CHF(/** @scrutinizer ignore-type */ $result->fetchColumn());
Loading history...
174
    }
175
176
    /**
177
     * Update accounts' balance
178
     *
179
     * @param null|Account $account the account to update, or null for all accounts
180
     *
181
     * @throws \Doctrine\DBAL\DBALException
182
     */
183
    public function updateAccountBalance(?Account $account = null): void
184
    {
185
        $connection = $this->getEntityManager()->getConnection();
186
        $sql = 'CALL update_account_balance(?)';
187
188
        if ($account) {
189
            $connection->executeQuery($sql, [$account->getId()]);
190
        } else {
191
            foreach ($this->findAll() as $a) {
192
                $connection->executeQuery($sql, [$a->getId()]);
193
            }
194
        }
195
    }
196
197
    /**
198
     * Return the next available Account code
199
     *
200
     * @return int
201
     */
202
    public function getNextCodeAvailable(): int
203
    {
204
        $qb = _em()->getConnection()->createQueryBuilder()
205
            ->select('IFNULL(MAX(a.code) + 1, 1)')
206
            ->from('account', 'a');
207
208
        return (int) $qb->execute()->fetchColumn();
209
    }
210
}
211