Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
29 / 29
100.00% covered (success)
100.00%
12 / 12
CRAP
100.00% covered (success)
100.00%
1 / 1
Expense
100.00% covered (success)
100.00%
29 / 29
100.00% covered (success)
100.00%
12 / 12
14
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 record
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
1
 update
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 belongsToTrip
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 id
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 tripId
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 money
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
 description
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 spentAt
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 createdAt
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 assertAmount
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
1<?php
2
3declare(strict_types=1);
4
5namespace App\Expense\Domain;
6
7use App\Expense\Domain\ValueObject\ExpenseId;
8use App\Expense\Domain\ValueObject\TripId;
9use App\Shared\Domain\ValueObject\Currency;
10use App\Shared\Domain\ValueObject\Money;
11use DateTimeImmutable;
12use Doctrine\ORM\Mapping as ORM;
13use InvalidArgumentException;
14
15#[ORM\Entity]
16#[ORM\Table(name: 'expenses')]
17final class Expense
18{
19    private function __construct(
20        #[ORM\Id]
21        #[ORM\Column(type: 'expense_id')]
22        private readonly ExpenseId $id,
23        #[ORM\Column(name: 'trip_id', type: 'expense_trip_id')]
24        private readonly TripId $tripId,
25        #[ORM\Column(type: 'integer')]
26        private int $amount,
27        // An expense is expressed in its trip's currency (enforced at creation).
28        #[ORM\Column(type: 'currency')]
29        private readonly Currency $currency,
30        #[ORM\Column(type: 'category', length: 20)]
31        private Category $category,
32        #[ORM\Column]
33        private string $description,
34        #[ORM\Column(name: 'spent_at', type: 'date_immutable')]
35        private DateTimeImmutable $spentAt,
36        #[ORM\Column(name: 'created_at', type: 'datetime_immutable')]
37        private readonly DateTimeImmutable $createdAt,
38    ) {
39    }
40
41    /**
42     * @param Currency $tripCurrency the currency fixed by the owning trip; the
43     *                               expense amount must be expressed in it
44     */
45    public static function record(
46        ExpenseId $id,
47        TripId $tripId,
48        Currency $tripCurrency,
49        Money $amount,
50        Category $category,
51        string $description,
52        DateTimeImmutable $spentAt,
53    ): self {
54        self::assertAmount($amount, $tripCurrency);
55
56        return new self(
57            $id,
58            $tripId,
59            $amount->amount(),
60            $tripCurrency,
61            $category,
62            trim($description),
63            $spentAt,
64            new DateTimeImmutable(),
65        );
66    }
67
68    /**
69     * Mutate the editable attributes. Currency stays fixed (it is the trip's
70     * currency, set at creation); the amount must therefore stay in it.
71     */
72    public function update(Money $amount, Category $category, string $description, DateTimeImmutable $spentAt): void
73    {
74        self::assertAmount($amount, $this->currency);
75
76        $this->amount = $amount->amount();
77        $this->category = $category;
78        $this->description = trim($description);
79        $this->spentAt = $spentAt;
80    }
81
82    public function belongsToTrip(TripId $tripId): bool
83    {
84        return $this->tripId->equals($tripId);
85    }
86
87    public function id(): ExpenseId
88    {
89        return $this->id;
90    }
91
92    public function tripId(): TripId
93    {
94        return $this->tripId;
95    }
96
97    public function money(): Money
98    {
99        return new Money($this->amount, $this->currency);
100    }
101
102    public function category(): Category
103    {
104        return $this->category;
105    }
106
107    public function description(): string
108    {
109        return $this->description;
110    }
111
112    public function spentAt(): DateTimeImmutable
113    {
114        return $this->spentAt;
115    }
116
117    public function createdAt(): DateTimeImmutable
118    {
119        return $this->createdAt;
120    }
121
122    private static function assertAmount(Money $amount, Currency $currency): void
123    {
124        if (!$amount->currency()->equals($currency)) {
125            throw new InvalidArgumentException('Expense currency must match the trip currency.');
126        }
127        if ($amount->amount() < 0) {
128            throw new InvalidArgumentException('Expense amount cannot be negative.');
129        }
130    }
131}