Failed Conditions
Push — master ( 7e6aaf...81a376 )
by Adrien
03:56
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\Enum\AccountType;
8
use Application\Model\Account;
9
use Application\Model\User;
10
use Doctrine\ORM\Query;
11
use Ecodev\Felix\Repository\LimitedAccessSubQuery;
12
use Exception;
13
use Money\Money;
14
15
/**
16
 * @extends AbstractRepository<Account>
17
 *
18
 * @method null|Account findOneByCode(int $code)
19
 */
20
class AccountRepository extends AbstractRepository implements LimitedAccessSubQuery
21
{
22
    /**
23
     * In memory max code that keep being incremented if we create several account at once without flushing in DB.
24
     */
25
    private ?int $maxCode = null;
26
27
    /**
28
     * Clear all caches.
29
     */
30 150
    public function clearCache(): void
31
    {
32 150
        $this->maxCode = null;
33
    }
34
35
    /**
36
     * Returns pure SQL to get ID of all objects that are accessible to given user.
37
     *
38
     * @param null|User $user
39
     */
40 25
    public function getAccessibleSubQuery(?\Ecodev\Felix\Model\User $user): string
41
    {
42 25
        if (!$user) {
43 1
            return '-1';
44
        }
45
46 24
        if (in_array($user->getRole(), [
47 24
            User::ROLE_TRAINER,
48 24
            User::ROLE_ACCOUNTING_VERIFICATOR,
49 24
            User::ROLE_FORMATION_RESPONSIBLE,
50 24
            User::ROLE_RESPONSIBLE,
51 24
            User::ROLE_ADMINISTRATOR,
52 24
        ], true)) {
53 13
            return '';
54
        }
55
56 11
        return $this->getAllIdsForFamilyQuery($user);
57
    }
58
59
    /**
60
     * Unsecured way to get a account from its ID.
61
     *
62
     * This should only be used in tests or controlled environment.
63
     */
64 5
    public function getOneById(int $id): Account
65
    {
66 5
        $account = $this->getAclFilter()->runWithoutAcl(fn () => $this->findOneById($id));
67
68 5
        if (!$account) {
69 1
            throw new Exception('Account #' . $id . ' not found');
70
        }
71
72 5
        return $account;
73
    }
74
75
    /**
76
     * This will return, and potentially create, an account for the given user.
77
     */
78 33
    public function getOrCreate(User $user): Account
79
    {
80
        global $container;
81
82
        // If an account already exists, because getOrCreate was called once before without flushing in between,
83
        // then can return immediately
84 33
        if ($user->getAccount()) {
85 19
            return $user->getAccount();
86
        }
87
88
        // If user have an owner, then create account for the owner instead
89 16
        if ($user->getOwner()) {
90
            $user = $user->getOwner();
91
        }
92
93 16
        $account = $this->getAclFilter()->runWithoutAcl(fn () => $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

93
        $account = $this->getAclFilter()->runWithoutAcl(fn () => $this->/** @scrutinizer ignore-call */ findOneByOwner($user));
Loading history...
94
95 16
        if (!$account) {
96 16
            $account = new Account();
97 16
            $this->getEntityManager()->persist($account);
98 16
            $account->setOwner($user);
99 16
            $account->setType(AccountType::Liability);
100 16
            $account->setName($user->getName());
101
102 16
            $config = $container->get('config');
103 16
            $parentCode = (int) $config['accounting']['customerDepositsAccountCode'];
104 16
            $parent = $this->getAclFilter()->runWithoutAcl(fn () => $this->findOneByCode($parentCode));
105
106
            // Find the max account code, using the liability parent code as prefix
107 16
            if (!$this->maxCode) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->maxCode of type integer|null is loosely compared to false; this is ambiguous if the integer can be 0. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
108 15
                $maxQuery = 'SELECT MAX(code) FROM account WHERE code LIKE ' . $this->getEntityManager()->getConnection()->quote($parent->getCode() . '%');
109 15
                $this->maxCode = (int) $this->getEntityManager()->getConnection()->fetchOne($maxQuery);
110
111
                // If there is no child account yet, reserve enough digits for many users
112 15
                if ($this->maxCode === $parent->getCode()) {
113 1
                    $this->maxCode = $parent->getCode() * 10000;
114
                }
115
            }
116
117 16
            $nextCode = ++$this->maxCode;
118 16
            $account->setCode($nextCode);
119
120 16
            $account->setParent($parent);
121
        }
122
123 16
        return $account;
124
    }
125
126
    /**
127
     * Sum balance by account type.
128
     */
129 2
    public function totalBalanceByType(AccountType $accountType): Money
130
    {
131 2
        $qb = $this->getEntityManager()->getConnection()->createQueryBuilder()
132 2
            ->select('SUM(balance)')
133 2
            ->from($this->getClassMetadata()->getTableName())
134 2
            ->where('type = :type');
135
136 2
        $qb->setParameter('type', $accountType->value);
137
138 2
        $result = $qb->executeQuery();
139
140 2
        return Money::CHF($result->fetchOne());
141
    }
142
143
    /**
144
     * Update all accounts' balance.
145
     */
146 1
    public function updateAccountsBalance(): void
147
    {
148 1
        $connection = $this->getEntityManager()->getConnection();
149 1
        $sql = 'CALL update_account_balance(0)';
150 1
        $connection->executeQuery($sql);
151
    }
152
153
    /**
154
     * Return the next available Account code.
155
     */
156 3
    public function getNextCodeAvailable(?Account $parent): int
157
    {
158 3
        $connection = $this->getEntityManager()->getConnection();
159
160 3
        return (int) $connection->fetchOne('SELECT IFNULL(MAX(code) + 1, 1) FROM account WHERE IF(:parent IS NULL, parent_id IS NULL, parent_id = :parent)', [
161 3
            'parent' => $parent?->getId(),
162 3
        ]);
163
    }
164
165 1
    public function getRootAccountsQuery(): Query
166
    {
167 1
        $qb = $this->createQueryBuilder('account')
168 1
            ->andWhere('account.parent IS NULL')
169 1
            ->orderBy('account.code');
170
171 1
        return $qb->getQuery();
172
    }
173
174 2
    public function deleteAccountOfNonFamilyOwnerWithoutAnyTransactions(): int
175
    {
176 2
        $sql = <<<STRING
177
                DELETE account FROM account
178
                INNER JOIN user ON account.owner_id = user.id
179
                AND user.owner_id IS NOT NULL
180
                AND user.owner_id != user.id
181
                WHERE
182
                account.id NOT IN (SELECT credit_id FROM transaction_line WHERE credit_id IS NOT NULL)
183
                AND account.id NOT IN (SELECT debit_id FROM transaction_line WHERE debit_id IS NOT NULL) 
184 2
            STRING;
185
186
        /** @var int $count */
187 2
        $count = $this->getEntityManager()->getConnection()->executeStatement($sql);
188
189 2
        return $count;
190
    }
191
192
    /**
193
     * Native query to return the IDs of myself and all recursive descendants
194
     * of the one passed as parameter.
195
     */
196 3
    private function getSelfAndDescendantsSubQuery(int $itemId): string
197
    {
198 3
        $table = $this->getClassMetadata()->table['name'];
199
200 3
        $connection = $this->getEntityManager()->getConnection();
201 3
        $table = $connection->quoteIdentifier($table);
202
203
        /** @var string $id */
204 3
        $id = $connection->quote($itemId);
205
206 3
        $entireHierarchySql = "
207
            WITH RECURSIVE parent AS (
208 3
                    SELECT $table.id, $table.parent_id FROM $table WHERE $table.id IN ($id)
209
                    UNION
210 3
                    SELECT $table.id, $table.parent_id FROM $table JOIN parent ON $table.parent_id = parent.id
211
                )
212 3
            SELECT id FROM parent ORDER BY id";
213
214 3
        return trim($entireHierarchySql);
215
    }
216
217
    /**
218
     * Whether the account, or any of its subaccounts, has any transaction at all.
219
     */
220 4
    public function hasTransaction(Account $account): bool
221
    {
222 4
        $id = $account->getId();
223 4
        if (!$id) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $id of type integer|null is loosely compared to false; this is ambiguous if the integer can be 0. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
224 1
            return false;
225
        }
226
227 3
        $wholeHierarchy = $this->getSelfAndDescendantsSubQuery($id);
228
229 3
        $hierarchyHasSomeTransactionLines = $this->getEntityManager()->getConnection()->fetchOne(
230 3
            <<<SQL
231 3
                SELECT EXISTS (
232
                    SELECT * FROM transaction_line
233 3
                    INNER JOIN ($wholeHierarchy) AS tmp ON transaction_line.credit_id = tmp.id OR transaction_line.debit_id = tmp.id
234
                );
235 3
                SQL
236 3
        );
237
238 3
        return (bool) $hierarchyHasSomeTransactionLines;
239
    }
240
}
241