Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
2 / 2
CRAP
100.00% covered (success)
100.00%
1 / 1
ExpenseViewFactory
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
2 / 2
4
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
 create
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
3
1<?php
2
3declare(strict_types=1);
4
5namespace App\Expense\Application\Query;
6
7use App\Currency\Application\ExchangeRateProvider;
8use App\Expense\Domain\Expense;
9use App\Shared\Domain\ValueObject\Currency;
10use Throwable;
11
12use function round;
13
14/**
15 * Builds an ExpenseView, adding the amount converted into the user's preferred
16 * currency. Conversion is view-time only and best-effort: if the rate provider
17 * fails the view is still returned, just without the converted amount. The rate
18 * is taken as of the expense's spend date (not "today"), so historical figures
19 * stay stable and never drift — see ExchangeRateProvider for the full decision.
20 */
21final readonly class ExpenseViewFactory
22{
23    public function __construct(private ExchangeRateProvider $rates)
24    {
25    }
26
27    public function create(Expense $expense, Currency $preferred): ExpenseView
28    {
29        $tripCurrency = $expense->money()->currency();
30        if ($tripCurrency->equals($preferred)) {
31            return ExpenseView::fromExpense($expense);
32        }
33
34        try {
35            $rate = $this->rates->rate($tripCurrency, $preferred, $expense->spentAt());
36        } catch (Throwable) {
37            // ponytail: rate unavailable → show the expense without conversion, never 500 a read
38            return ExpenseView::fromExpense($expense);
39        }
40
41        // Scale across differing minor-unit exponents (e.g. EUR 2dp → JPY 0dp).
42        $amount = $expense->money()->amount();
43        $converted = (int) round($amount / (10 ** $tripCurrency->minorUnits()) * $rate * (10 ** $preferred->minorUnits()));
44
45        return ExpenseView::fromExpense($expense, $converted, $preferred->code());
46    }
47}