Failed Conditions
Push — master ( 76f788...13b360 )
by Sylvain
08:55
created

Account   A

Complexity

Total Complexity 32

Size/Duplication

Total Lines 312
Duplicated Lines 0 %

Test Coverage

Coverage 92.78%

Importance

Changes 2
Bugs 0 Features 1
Metric Value
wmc 32
eloc 78
c 2
b 0
f 1
dl 0
loc 312
ccs 90
cts 97
cp 0.9278
rs 9.84

21 Methods

Rating   Name   Duplication   Size   Complexity  
A getType() 0 3 1
A creditTransactionLineAdded() 0 3 1
A __construct() 0 7 1
A getBalance() 0 3 1
A getChildren() 0 3 1
B getBalanceAtDate() 0 67 8
A getTotalBalance() 0 3 1
A setParent() 0 10 3
A debitTransactionLineAdded() 0 3 1
A setOwner() 0 10 3
A setCode() 0 3 1
A debitTransactionLineRemoved() 0 3 1
A getDebitTransactionLines() 0 3 1
A getFullName() 0 3 1
A setBalance() 0 3 1
A creditTransactionLineRemoved() 0 3 1
A getParent() 0 3 1
A setType() 0 3 1
A getCode() 0 3 1
A getCreditTransactionLines() 0 3 1
A getOwnerForCreation() 0 3 1
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Application\Model;
6
7
use Application\DBAL\Types\AccountTypeType;
8
use Application\Repository\AccountRepository;
9
use Application\Repository\TransactionLineRepository;
10
use Application\Traits\HasIban;
11
use Cake\Chronos\Date;
12
use Doctrine\Common\Collections\ArrayCollection;
13
use Doctrine\Common\Collections\Collection;
14
use Doctrine\ORM\Mapping as ORM;
15
use Ecodev\Felix\Api\Exception;
16
use Ecodev\Felix\Model\Traits\HasName;
17
use GraphQL\Doctrine\Annotation as API;
18
use Money\Money;
19
use PDO;
20
21
/**
22
 * Financial account
23
 *
24
 * @ORM\Entity(repositoryClass="Application\Repository\AccountRepository")
25
 * @ORM\AssociationOverrides({
26
 *     @ORM\AssociationOverride(
27
 *         name="owner",
28
 *         inversedBy="accounts",
29
 *         joinColumns=@ORM\JoinColumn(unique=true, onDelete="SET NULL")
30
 *     )
31
 * })
32
 */
33
class Account extends AbstractModel
34
{
35
    use HasName;
36
    use HasIban;
37
38
    /**
39
     * @var Money
40
     *
41
     * @ORM\Column(type="Money", options={"default" = 0})
42
     */
43
    private $balance;
44
45
    /**
46
     * @var Account
47
     * @ORM\ManyToOne(targetEntity="Account", inversedBy="children")
48
     * @ORM\JoinColumns({
49
     *     @ORM\JoinColumn(onDelete="CASCADE")
50
     * })
51
     */
52
    private $parent;
53
54
    /**
55
     * @var Collection
56
     * @ORM\OneToMany(targetEntity="Account", mappedBy="parent")
57
     * @ORM\OrderBy({"code" = "ASC"})
58
     */
59
    private $children;
60
61
    /**
62
     * @var string
63
     *
64
     * @ORM\Column(type="AccountType", length=10)
65
     */
66
    private $type;
67
68
    /**
69
     * @var int
70
     *
71
     * @ORM\Column(type="integer", nullable=false, unique=true, options={"unsigned" = true})
72
     */
73
    private $code;
74
75
    /**
76
     * @var Collection
77
     * @ORM\OneToMany(targetEntity="TransactionLine", mappedBy="debit")
78
     */
79
    private $debitTransactionLines;
80
81
    /**
82
     * @var Collection
83
     * @ORM\OneToMany(targetEntity="TransactionLine", mappedBy="credit")
84
     */
85
    private $creditTransactionLines;
86
87
    /**
88
     * @var Money
89
     *
90
     * @ORM\Column(type="Money", options={"default" = 0})
91
     */
92
    private $totalBalance;
93
94
    /**
95
     * Constructor
96
     */
97 22
    public function __construct()
98
    {
99 22
        $this->balance = Money::CHF(0);
100 22
        $this->totalBalance = Money::CHF(0);
101 22
        $this->children = new ArrayCollection();
102 22
        $this->debitTransactionLines = new ArrayCollection();
103 22
        $this->creditTransactionLines = new ArrayCollection();
104 22
    }
105
106
    /**
107
     * Get full name including code and name
108
     */
109
    public function getFullName(): string
110
    {
111
        return implode(' - ', array_filter([$this->getCode(), $this->getName()]));
112
    }
113
114
    /**
115
     * Assign the account to an user
116
     */
117 14
    public function setOwner(?User $owner): void
118
    {
119 14
        if ($this->getOwner()) {
120 1
            $this->getOwner()->accountRemoved();
121
        }
122
123 14
        parent::setOwner($owner);
124
125 14
        if ($this->getOwner()) {
126 14
            $owner->accountAdded($this);
0 ignored issues
show
Bug introduced by
The method accountAdded() does not exist on null. ( Ignorable by Annotation )

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

126
            $owner->/** @scrutinizer ignore-call */ 
127
                    accountAdded($this);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
127
        }
128 14
    }
129
130
    /**
131
     * Only members' liability accounts must have an owner
132
     * and there must be only an account per member
133
     */
134 9
    protected function getOwnerForCreation(): ?User
135
    {
136 9
        return null;
137
    }
138
139
    /**
140
     * Set balance
141
     *
142
     * @API\Exclude
143
     */
144 2
    public function setBalance(Money $balance): void
145
    {
146 2
        $this->balance = $balance;
147 2
    }
148
149 9
    public function getBalance(): Money
150
    {
151 9
        return $this->balance;
152
    }
153
154
    /**
155
     * Total balance, recursively including all child account if this account is a group
156
     */
157 1
    public function getTotalBalance(): Money
158
    {
159 1
        return $this->totalBalance;
160
    }
161
162
    /**
163
     * Historical account's balance at a date in the past
164
     */
165 2
    public function getBalanceAtDate(Date $date): Money
166
    {
167 2
        $today = Date::today();
168
169 2
        if ($date->greaterThan($today)) {
170
            throw new Exception('Cannot compute balance of account #' . $this->getId() . ' in the future on ' . $date->format('d.m.Y'));
171
        }
172
173 2
        if ($date->equals($today)) {
174
            if ($this->getType() === AccountTypeType::GROUP) {
175
                return $this->getTotalBalance();
176
            }
177
178
            return $this->getBalance();
179
        }
180
181 2
        $connection = _em()->getConnection();
182
183 2
        if ($this->getType() === AccountTypeType::GROUP) {
184
185
            // Get all child accounts that are not group account (= they have their own balance)
186 1
            $sql = 'WITH RECURSIVE child AS
187
              (SELECT id, parent_id, `type`, balance
188
               FROM account WHERE id = ?
189
               UNION
190
               SELECT account.id, account.parent_id, account.type, account.balance
191
               FROM account
192
               JOIN child ON account.parent_id = child.id)
193
            SELECT child.id FROM child WHERE `type` <> ?';
194
195 1
            $result = $connection->executeQuery($sql, [$this->getId(), AccountTypeType::GROUP]);
196
197 1
            $ids = $result->fetchAll(PDO::FETCH_COLUMN);
0 ignored issues
show
Deprecated Code introduced by
The function Doctrine\DBAL\Driver\ResultStatement::fetchAll() has been deprecated: Use fetchAllNumeric(), fetchAllAssociative() or fetchFirstColumn() instead. ( Ignorable by Annotation )

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

197
            $ids = /** @scrutinizer ignore-deprecated */ $result->fetchAll(PDO::FETCH_COLUMN);

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
198
199 1
            $totals = [];
200 1
            $totalForChildren = Money::CHF(0);
201
202
            /** @var AccountRepository $accountRepository */
203 1
            $accountRepository = _em()->getRepository(self::class);
204 1
            foreach ($ids as $idAccount) {
205 1
                $child = $accountRepository->getOneById((int) $idAccount);
206 1
                $childBalance = $child->getBalanceAtDate($date);
207 1
                $totalForChildren = $totalForChildren->add($childBalance);
208 1
                $totals[(int) $idAccount] = $totalForChildren;
209
            }
210
211 1
            return $totalForChildren;
212
        }
213
214
        /** @var TransactionLineRepository $transactionLineRepository */
215 2
        $transactionLineRepository = _em()->getRepository(TransactionLine::class);
216
217 2
        $totalDebit = $transactionLineRepository->totalBalance($this, null, null, $date);
218 2
        $totalCredit = $transactionLineRepository->totalBalance(null, $this, null, $date);
219 2
        if (in_array($this->getType(), [
220 2
            AccountTypeType::LIABILITY,
221 2
            AccountTypeType::EQUITY,
222 2
            AccountTypeType::REVENUE,
223 2
        ], true)) {
224 1
            $balance = $totalCredit->subtract($totalDebit);
225 2
        } elseif (in_array($this->getType(), [AccountTypeType::ASSET, AccountTypeType::EXPENSE], true)) {
226 2
            $balance = $totalDebit->subtract($totalCredit);
227
        } else {
228
            throw new Exception('Do not know how to compute past balance of account #' . $this->getId() . ' of type ' . $this->getType());
229
        }
230
231 2
        return $balance;
232
    }
233
234
    /**
235
     * Set parent
236
     *
237
     * @param null|Account $parent
238
     */
239 10
    public function setParent(?self $parent): void
240
    {
241 10
        if ($this->getParent()) {
242 1
            $this->getParent()->getChildren()->removeElement($this);
243
        }
244
245 10
        $this->parent = $parent;
246
247 10
        if ($this->getParent()) {
248 10
            $this->getParent()->getChildren()->add($this);
249
        }
250 10
    }
251
252
    /**
253
     * @return null|Account
254
     */
255 10
    public function getParent(): ?self
256
    {
257 10
        return $this->parent;
258
    }
259
260 10
    public function getChildren(): Collection
261
    {
262 10
        return $this->children;
263
    }
264
265
    /**
266
     * Set type
267
     *
268
     * @API\Input(type="AccountType")
269
     */
270 9
    public function setType(string $type): void
271
    {
272 9
        $this->type = $type;
273 9
    }
274
275
    /**
276
     * Get type
277
     *
278
     * @API\Field(type="AccountType")
279
     */
280 4
    public function getType(): string
281
    {
282 4
        return $this->type;
283
    }
284
285
    /**
286
     * Set code
287
     */
288 9
    public function setCode(int $code): void
289
    {
290 9
        $this->code = $code;
291 9
    }
292
293
    /**
294
     * Get code
295
     */
296 10
    public function getCode(): int
297
    {
298 10
        return $this->code;
299
    }
300
301
    /**
302
     * Notify when a transaction line is added
303
     * This should only be called by TransactionLine::setDebit()
304
     */
305 15
    public function debitTransactionLineAdded(TransactionLine $transactionLine): void
306
    {
307 15
        $this->debitTransactionLines->add($transactionLine);
308 15
    }
309
310
    /**
311
     * Notify when a transaction line is removed
312
     * This should only be called by TransactionLine::setDebit()
313
     */
314 1
    public function debitTransactionLineRemoved(TransactionLine $transactionLine): void
315
    {
316 1
        $this->debitTransactionLines->removeElement($transactionLine);
317 1
    }
318
319 5
    public function getDebitTransactionLines(): Collection
320
    {
321 5
        return $this->debitTransactionLines;
322
    }
323
324
    /**
325
     * Notify when a transaction line is added
326
     * This should only be called by TransactionLine::setCredit()
327
     */
328 18
    public function creditTransactionLineAdded(TransactionLine $transactionLine): void
329
    {
330 18
        $this->creditTransactionLines->add($transactionLine);
331 18
    }
332
333
    /**
334
     * Notify when a transaction line is removed
335
     * This should only be called by TransactionLine::setCredit()
336
     */
337 1
    public function creditTransactionLineRemoved(TransactionLine $transactionLine): void
338
    {
339 1
        $this->creditTransactionLines->removeElement($transactionLine);
340 1
    }
341
342 5
    public function getCreditTransactionLines(): Collection
343
    {
344 5
        return $this->creditTransactionLines;
345
    }
346
}
347