Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
15.62% covered (danger)
15.62%
5 / 32
62.50% covered (warning)
62.50%
5 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
IncomeExpenseReport
15.62% covered (danger)
15.62%
5 / 32
62.50% covered (warning)
62.50%
5 / 8
46.44
0.00% covered (danger)
0.00%
0 / 1
 key
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 label
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 group
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 feature
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 description
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 columns
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 rows
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
2
 summary
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3namespace App\Reports\Finance;
4
5use App\Models\Transaction;
6use App\Reports\Report;
7use Illuminate\Support\Collection;
8
9/** A P&L-lite: every posted income/expense transaction in range, grouped by category. */
10class IncomeExpenseReport extends Report
11{
12    public function key(): string
13    {
14        return 'income-expense';
15    }
16
17    public function label(): string
18    {
19        return 'Income vs Expense (P&L)';
20    }
21
22    public function group(): string
23    {
24        return 'Finance';
25    }
26
27    public function feature(): ?string
28    {
29        return 'accounting';
30    }
31
32    public function description(): ?string
33    {
34        return 'Income and expense totals by category for the selected range — reversed entries excluded.';
35    }
36
37    public function columns(): array
38    {
39        return [
40            ['key' => 'category', 'label' => 'Category'],
41            ['key' => 'type', 'label' => 'Type', 'render' => fn ($r) => ucfirst($r->type)],
42            ['key' => 'amount', 'label' => 'Amount', 'align' => 'right', 'render' => fn ($r) => money($r->amount)],
43        ];
44    }
45
46    public function rows(array $filters): Collection
47    {
48        return Transaction::query()
49            ->whereIn('type', ['income', 'expense'])
50            ->whereNull('reversed_at')
51            ->whereDate('date', '>=', $filters['from'])
52            ->whereDate('date', '<=', $filters['to'])
53            ->with('category')
54            ->get()
55            ->groupBy(fn ($t) => $t->type . '|' . ($t->category->name ?? 'Uncategorized'))
56            ->map(fn ($rows) => (object) [
57                'category' => $rows->first()->category->name ?? 'Uncategorized',
58                'type' => $rows->first()->type,
59                'amount' => (float) $rows->sum('amount'),
60            ])
61            ->sortBy('type')
62            ->values();
63    }
64
65    public function summary(Collection $rows, array $filters): array
66    {
67        $income = $rows->where('type', 'income')->sum('amount');
68        $expense = $rows->where('type', 'expense')->sum('amount');
69
70        return [
71            'Income' => money($income),
72            'Expense' => money($expense),
73            'Net' => money($income - $expense),
74        ];
75    }
76}