Passed
Push — master ( b7a44b...607d06 )
by Adrien
08:56
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
20
/**
21
 * Financial account
22
 *
23
 * @ORM\Entity(repositoryClass="Application\Repository\AccountRepository")
24
 * @ORM\AssociationOverrides({
25
 *     @ORM\AssociationOverride(
26
 *         name="owner",
27
 *         inversedBy="accounts",
28
 *         joinColumns=@ORM\JoinColumn(unique=true, onDelete="SET NULL")
29
 *     )
30
 * })
31
 */
32
class Account extends AbstractModel
33
{
34
    use HasName;
35
    use HasIban;
36
37
    /**
38
     * @var Money
39
     *
40
     * @ORM\Column(type="Money", options={"default" = 0})
41
     */
42
    private $balance;
43
44
    /**
45
     * @var Account
46
     * @ORM\ManyToOne(targetEntity="Account", inversedBy="children")
47
     * @ORM\JoinColumns({
48
     *     @ORM\JoinColumn(onDelete="CASCADE")
49
     * })
50
     */
51
    private $parent;
52
53
    /**
54
     * @var Collection
55
     * @ORM\OneToMany(targetEntity="Account", mappedBy="parent")
56
     * @ORM\OrderBy({"code" = "ASC"})
57
     */
58
    private $children;
59
60
    /**
61
     * @var string
62
     *
63
     * @ORM\Column(type="AccountType", length=10)
64
     */
65
    private $type;
66
67
    /**
68
     * @var int
69
     *
70
     * @ORM\Column(type="integer", nullable=false, unique=true, options={"unsigned" = true})
71
     */
72
    private $code;
73
74
    /**
75
     * @var Collection
76
     * @ORM\OneToMany(targetEntity="TransactionLine", mappedBy="debit")
77
     */
78
    private $debitTransactionLines;
79
80
    /**
81
     * @var Collection
82
     * @ORM\OneToMany(targetEntity="TransactionLine", mappedBy="credit")
83
     */
84
    private $creditTransactionLines;
85
86
    /**
87
     * @var Money
88
     *
89
     * @ORM\Column(type="Money", options={"default" = 0})
90
     */
91
    private $totalBalance;
92
93
    /**
94
     * Constructor
95
     */
96 22
    public function __construct()
97
    {
98 22
        $this->balance = Money::CHF(0);
99 22
        $this->totalBalance = Money::CHF(0);
100 22
        $this->children = new ArrayCollection();
101 22
        $this->debitTransactionLines = new ArrayCollection();
102 22
        $this->creditTransactionLines = new ArrayCollection();
103 22
    }
104
105
    /**
106
     * Get full name including code and name
107
     */
108
    public function getFullName(): string
109
    {
110
        return implode(' - ', array_filter([$this->getCode(), $this->getName()]));
111
    }
112
113
    /**
114
     * Assign the account to an user
115
     */
116 14
    public function setOwner(?User $owner): void
117
    {
118 14
        if ($this->getOwner()) {
119 1
            $this->getOwner()->accountRemoved();
120
        }
121
122 14
        parent::setOwner($owner);
123
124 14
        if ($this->getOwner()) {
125 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

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

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

196
            /** @scrutinizer ignore-call */ 
197
            $ids = $result->fetchFirstColumn();

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