fix(costs): keep convertBooked at five parameters and update the tests to as-typed display rows

The display-currency check is its own helper (inDisplayCurrency), used by the Costs model and the shared trip page. The tests that pinned #2525's reading of a display-currency bill through the trip currency now pin the as-typed reading, which the balances use too; read in the trip currency, such a bill and its repayment may stay a euro cent apart.
This commit is contained in:
Maurice
2026-11-03 22:43:05 +01:00
parent a37a9c0fa8
commit 4585988dfd
12 changed files with 109 additions and 101 deletions
+6 -11
View File
@@ -2,7 +2,7 @@ import { describe, it, expect, beforeEach, vi } from 'vitest'
import { renderHook, waitFor } from '@testing-library/react'
import { http, HttpResponse } from 'msw'
import { server } from '../../tests/helpers/msw/server'
import { bookedInTrip, convertBooked, convertedLine, crossRate, fetchExchangeRates, fetchFreshRates, freshRates, clearExchangeRateCache, tripAmountOf, useExchangeRates, withFallbackFx } from './useExchangeRates'
import { bookedInTrip, convertBooked, convertedLine, inDisplayCurrency, crossRate, fetchExchangeRates, fetchFreshRates, freshRates, clearExchangeRateCache, tripAmountOf, useExchangeRates, withFallbackFx } from './useExchangeRates'
const FX_URL = 'https://api.frankfurter.dev/v2/rates'
@@ -198,16 +198,11 @@ describe('convertBooked', () => {
})
// #2965: read in dollars, a bill entered in dollars counts as typed, as the balances net it.
it('shows a row in the display currency as typed, whatever rate it was frozen at', () => {
const liveUsd = (amount: number, from: string | null | undefined): number => {
const rates: Record<string, number> = { USD: 1, EUR: 1 / 1.1398 }
const r = rates[(from || 'USD').toUpperCase()]
return r && r > 0 ? amount / r : amount
}
expect(convertBooked(801.76, 'USD', 1.17, 'EUR', liveUsd, 'USD')).toBe(801.76)
expect(convertBooked(801.76, 'usd', 1.17, 'EUR', liveUsd, 'USD')).toBe(801.76)
// Another currency still goes through the trip currency.
expect(convertBooked(801.76, 'USD', 1.17, 'EUR', liveUsd, 'GBP')).toBeCloseTo((801.76 / 1.17) * 1.1398, 6)
it('knows a row entered in the display currency, a NULL currency being the trip one', () => {
expect(inDisplayCurrency('usd', 'EUR', 'USD')).toBe(true)
expect(inDisplayCurrency(null, 'EUR', 'eur')).toBe(true)
expect(inDisplayCurrency(null, 'EUR', 'USD')).toBe(false)
expect(inDisplayCurrency('USD', 'EUR', 'GBP')).toBe(false)
})
})
+13 -5
View File
@@ -133,19 +133,27 @@ export function convertBooked(
frozenRate: number | null | undefined,
tripCurrency: string,
convertLive: (amount: number, from: string | null | undefined) => number,
displayCurrency?: string,
): number {
const trip = (tripCurrency || 'EUR').toUpperCase()
// A row entered in the display currency is shown as typed (#2965): through the
// trip currency at its frozen rate and back at today's, it drifted off the
// amount entered, and the balances (which net such rows as typed) disagreed.
if (displayCurrency && (rowCurrency || trip).toUpperCase() === displayCurrency.toUpperCase()) return amount
const inTrip = bookedInTrip(amount, rowCurrency, frozenRate, trip)
if (inTrip != null) return convertLive(inTrip, trip)
// A NULL currency means the trip's own, and then there was never anything to freeze.
return convertLive(amount, (rowCurrency || trip).toUpperCase())
}
/**
* Whether a row was entered in the display currency (#2965). Such a row counts as typed:
* through the trip currency at its frozen rate and back at today's it drifted off the
* amount entered, and the balances, which net such rows as typed, would disagree.
*/
export function inDisplayCurrency(
rowCurrency: string | null | undefined,
tripCurrency: string,
displayCurrency: string,
): boolean {
return ((rowCurrency || tripCurrency) || '').toUpperCase() === (displayCurrency || '').toUpperCase()
}
/**
* The amount a row was booked at in the trip currency, when it was booked through one: a
* foreign currency with a frozen rate. Null for a row in the trip currency, and for one