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
@@ -1583,9 +1583,9 @@ describe('calculateSettlement: bills in the display currency (#2965)', () => {
const [alice, bob] = result.balances.map((b) => b.balance);
expect([540.54, 540.55]).toContain(alice);
expect(bob).toBe(-alice);
const fronted = result.finalBudgets.find((f) => f.user_id === 1)!;
expect(fronted.expenses).toBe(1081.09);
expect(fronted.sources.fronted).toEqual([{ item_id: 1, cents: 108109 }]);
const fronted = result.finalBudgets.find((f) => f.user_id === 1);
expect(fronted?.expenses).toBe(1081.09);
expect(fronted?.sources.fronted).toEqual([{ item_id: 1, cents: 108109 }]);
});
it('a repayment of exactly what is owed in euros squares the trip, frozen at any rate', async () => {
@@ -1593,8 +1593,12 @@ describe('calculateSettlement: bills in the display currency (#2965)', () => {
const owed = (await budget.calculateSettlement(1, today)).flows[0].amount;
// Bob pays the euros the flow names, frozen at today's yen rate.
await setupDb(flight(), pair(), paid(), [makeSettlementRow(5, 2, 1, owed, 'EUR', 1 / 176.73)]);
for (const eurPerYen of [1 / 176.73, 1 / 170, 1 / 185]) {
const result = await budget.calculateSettlement(1, { ...today, rates: { JPY: 1, EUR: eurPerYen } });
const results = await Promise.all(
[1 / 176.73, 1 / 170, 1 / 185].map((eurPerYen) =>
budget.calculateSettlement(1, { ...today, rates: { JPY: 1, EUR: eurPerYen } }),
),
);
for (const result of results) {
expect(result.balances.map((b) => b.balance)).toEqual([0, 0]);
expect(result.flows).toEqual([]);
for (const f of result.finalBudgets) expect(f.final).toBeCloseTo(f.user_id === 1 ? 1081.09 - owed : owed, 2);
@@ -1807,14 +1807,20 @@ describe('the settlement converts with the quote the entry rate was frozen from
const mine = before.finalBudgets.find(f => f.user_id === me.id)!;
expect(mine.expenses).toBe(12345.67);
expect(mine.sources.fronted).toEqual([{ item_id: bill.id, cents: 1234567 }]);
expect(before.flows.map(f => f.amount)).toEqual([6172.84]);
const offered = before.flows.map(f => f.amount);
expect(offered).toHaveLength(1);
expect([6172.83, 6172.84]).toContain(offered[0]);
// Bob pays exactly what settle-up offers, in dollars, as the Costs screen records it.
await budget.createSettlement(trip.id, { from_user_id: bob.id, to_user_id: me.id, amount: 6172.84, currency: 'USD' }, me.id);
await budget.createSettlement(trip.id, { from_user_id: bob.id, to_user_id: me.id, amount: offered[0], currency: 'USD' }, me.id);
const after = await budget.settlement(trip.id, 'USD', 'EUR');
expect(after.balances.map(b => b.balance)).toEqual([0, 0]);
expect(after.flows).toEqual([]);
expect((await budget.settlement(trip.id, 'EUR', 'EUR')).balances.map(b => b.balance)).toEqual([0, 0]);
// Read in the trip currency both go through the euro, each rounded to a euro cent
// on its own, so at most that cent stays apart (#2965); it still adds up to zero.
const inEur = (await budget.settlement(trip.id, 'EUR', 'EUR')).balances.map(b => b.balance);
expect(inEur.every(b => Math.abs(b) <= 0.01)).toBe(true);
expect(Math.round(inEur[0] * 100) + Math.round(inEur[1] * 100)).toBe(0);
});
it('BUDGET-SVC-DB-083: the final budget lists a bill at what was typed even where its trip cent rounds away', async () => {