Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
86.67% covered (warning)
86.67%
13 / 15
75.00% covered (warning)
75.00%
6 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
Transaction
86.67% covered (warning)
86.67%
13 / 15
75.00% covered (warning)
75.00%
6 / 8
12.34
0.00% covered (danger)
0.00%
0 / 1
 auditName
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 auditLabel
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 account
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 toAccount
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 category
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 reverses
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 isReversed
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 signedAmountFor
87.50% covered (warning)
87.50%
7 / 8
0.00% covered (danger)
0.00%
0 / 1
5.05
1<?php
2
3namespace App\Models;
4
5use App\Support\Auditable;
6use Illuminate\Database\Eloquent\Model;
7
8/**
9 * One ledger line: income, expense, or a transfer between two accounts.
10 * Immutable once posted — see App\Services\Accounting\AccountingService::reverse()
11 * for how a correction is made (a new opposite-signed row, never an edit).
12 */
13class Transaction extends Model
14{
15    use Auditable;
16
17    protected $fillable = [
18        'date', 'account_id', 'to_account_id', 'category_id', 'type', 'amount',
19        'ref', 'note', 'party_type', 'party_id', 'party_name',
20        'source', 'source_id', 'is_reconciled', 'reverses_transaction_id', 'reversed_at', 'created_by',
21    ];
22
23    protected $casts = [
24        'date' => 'date',
25        'amount' => 'decimal:2',
26        'is_reconciled' => 'boolean',
27        'reversed_at' => 'datetime',
28    ];
29
30    protected function auditName(): string
31    {
32        return 'transaction';
33    }
34
35    protected function auditLabel(): ?string
36    {
37        return ucfirst($this->type) . ' — ' . money($this->amount);
38    }
39
40    public function account()
41    {
42        return $this->belongsTo(Account::class);
43    }
44
45    public function toAccount()
46    {
47        return $this->belongsTo(Account::class, 'to_account_id');
48    }
49
50    public function category()
51    {
52        return $this->belongsTo(AccountCategory::class, 'category_id');
53    }
54
55    public function reverses()
56    {
57        return $this->belongsTo(self::class, 'reverses_transaction_id');
58    }
59
60    public function isReversed(): bool
61    {
62        return $this->reversed_at !== null;
63    }
64
65    /** Signed effect of this row on the given account's balance (+ in, − out). */
66    public function signedAmountFor(int $accountId): float
67    {
68        $amount = (float) $this->amount;
69
70        if ($this->type === 'transfer') {
71            if ($this->account_id === $accountId) {
72                return -$amount;
73            }
74            if ($this->to_account_id === $accountId) {
75                return $amount;
76            }
77
78            return 0.0;
79        }
80
81        return $this->type === 'income' ? $amount : -$amount;
82    }
83}