Passed
Push — master ( 667027...052b73 )
by Sylvain
04:09
created

AccountRepository::clearCache()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
ccs 2
cts 2
cp 1
crap 1
rs 10
c 0
b 0
f 0
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 11
    public function getAccessibleSubQuery(?User $user): string
40
    {
41 11
        if (!$user) {
42 1
            return '-1';
43
        }
44
45 10
        if (in_array($user->getRole(), [User::ROLE_RESPONSIBLE, User::ROLE_ADMINISTRATOR], true)) {
46 2
            return $this->getAllIdsQuery();
47
        }
48
49 8
        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 12
    public function getOneById(int $id): Account
64
    {
65 12
        $this->getAclFilter()->setEnabled(false);
66 12
        $account = $this->findOneById($id);
67 12
        $this->getAclFilter()->setEnabled(true);
68 12
        if (!$account) {
69 1
            throw new \Exception('Account #' . $id . ' not found');
70
        }
71
72 12
        return $account;
1 ignored issue
show
Bug Best Practice introduced by
The expression return $account returns the type Application\Model\AbstractModel which includes types incompatible with the type-hinted return Application\Model\Account.
Loading history...
73
    }
74
75
    /**
76
     * This will return, and potentially create, an account for the given user
77
     *
78
     * @param User $user
79
     *
80
     * @return Account
81
     */
82 16
    public function getOrCreate(User $user): Account
83
    {
84
        // If an account already exists, because getOrCreate was called once before without flushing in between,
85
        // then can return immediately
86 16
        if ($user->getAccount()) {
87 11
            return $user->getAccount();
88
        }
89
90 8
        $this->getAclFilter()->setEnabled(false);
91 8
        $account = $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

91
        /** @scrutinizer ignore-call */ 
92
        $account = $this->findOneByOwner($user);
Loading history...
92 8
        $this->getAclFilter()->setEnabled(true);
93
94 8
        if (!$account) {
95 8
            $account = new Account();
96 8
            $this->getEntityManager()->persist($account);
97 8
            $account->setOwner($user);
98 8
            $account->setType(AccountTypeType::LIABILITY);
99 8
            $account->setName($user->getName());
100
101 8
            $maxCode = $this->getEntityManager()->getConnection()->fetchColumn('SELECT MAX(code) FROM account WHERE parent_id = ' . self::PARENT_ACCOUNT_ID_FOR_USER);
102 8
            $newCode = ++$maxCode;
103 8
            $account->setCode((string) $newCode);
104
105 8
            $parent = $this->getOneById(self::PARENT_ACCOUNT_ID_FOR_USER);
106 8
            $account->setParent($parent);
107
        }
108
109 8
        return $account;
110
    }
111
112
    /**
113
     * Sum balance by account type
114
     *
115
     * @API\Input(type="AccountType")
116
     *
117
     * @param string $accountType
118
     *
119
     * @return Money
120
     */
121 1
    public function totalBalanceByType(string $accountType): Money
122
    {
123 1
        $qb = $this->getEntityManager()->getConnection()->createQueryBuilder()
124 1
            ->select('SUM(balance)')
125 1
            ->from($this->getClassMetadata()->getTableName())
126 1
            ->where('type = :type');
127
128 1
        $qb->setParameter('type', $accountType);
129
130 1
        $result = $qb->execute();
131
132 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

132
        return Money::CHF(/** @scrutinizer ignore-type */ $result->fetchColumn());
Loading history...
133
    }
134
135
    /**
136
     * Calculate the total balance of all child accounts of a group account
137
     *
138
     * @param Account $parentAccount
139
     *
140
     * @throws \Doctrine\DBAL\DBALException
141
     *
142
     * @return Money
143
     */
144 1
    public function totalBalanceByParent(Account $parentAccount): Money
145
    {
146 1
        if ($parentAccount->getType() !== AccountTypeType::GROUP) {
147 1
            throw new InvalidArgumentException(sprintf(
148 1
                'Cannot compute total balance for Account #%d of type %s',
149 1
                $parentAccount->getId(),
150 1
                $parentAccount->getType()
151
            ));
152
        }
153
154 1
        $cacheKey = Utility::getCacheKey(func_get_args());
155 1
        if (array_key_exists($cacheKey, $this->totalBalanceCache)) {
156
            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...
157
        }
158
159 1
        $connection = $this->getEntityManager()->getConnection();
160
161 1
        $sql = 'WITH RECURSIVE child AS
162
          (SELECT id, parent_id, `type`, balance
163
           FROM account WHERE id = ?
164
           UNION
165
           SELECT account.id, account.parent_id, account.type, account.balance
166
           FROM account
167
           JOIN child ON account.parent_id = child.id)
168
        SELECT SUM(balance) FROM child WHERE `type` <> ?';
169
170 1
        $result = $connection->executeQuery($sql, [$parentAccount->getId(), AccountTypeType::GROUP]);
171
172 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

172
        return Money::CHF(/** @scrutinizer ignore-type */ $result->fetchColumn());
Loading history...
173
    }
174
175
    /**
176
     * Update accounts' balance
177
     *
178
     * @param null|Account $account the account to update, or null for all accounts
179
     *
180
     * @throws \Doctrine\DBAL\DBALException
181
     */
182
    public function updateAccountBalance(?Account $account = null): void
183
    {
184
        $connection = $this->getEntityManager()->getConnection();
185
        $sql = 'CALL update_account_balance(?)';
186
187
        if ($account) {
188
            $connection->executeQuery($sql, [$account->getId()]);
189
        } else {
190
            foreach ($this->findAll() as $a) {
191
                $connection->executeQuery($sql, [$a->getId()]);
192
            }
193
        }
194
    }
195
}
196