fix(billing): round the recurring figure the same way as the total
tests / pest (push) Successful in 7m33s Details
tests / assets (push) Successful in 21s Details
tests / release (push) Has been skipped Details

The note rounded VAT after aggregating while the total rounded per order, so
cent-level prices made the two disagree — and a customer who spots that stops
trusting every other number on the page.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
feat/portal-design
nexxo 2026-07-26 09:27:02 +02:00
parent 9fab251fb3
commit ac3f429958
2 changed files with 22 additions and 3 deletions

View File

@ -72,7 +72,9 @@
@php
$net = $pending->sum('amount_cents');
$gross = $pending->sum(fn ($o) => $o->grossCents());
$recurringNet = $pending->filter->isRecurring()->sum('amount_cents');
// Summed from the same per-order rounding the total uses: rounding
// after aggregating would let the two figures disagree by a cent.
$recurringGross = $pending->filter->isRecurring()->sum(fn ($o) => $o->grossCents());
@endphp
<div class="space-y-1.5 border-t border-line bg-surface-2 px-5 py-3.5 text-sm">
<div class="flex items-center justify-between text-muted">
@ -87,11 +89,11 @@
<span>{{ __('billing.cart.total_gross') }}</span>
<span class="font-mono">{{ $eur($gross) }}</span>
</div>
@if ($recurringNet > 0)
@if ($recurringGross > 0)
{{-- The number that actually matters every month, kept apart
from a one-off traffic top-up in the same cart. --}}
<p class="pt-1 text-xs text-faint">
{{ __('billing.cart.recurring_note', ['amount' => $eur((int) round($recurringNet * (1 + \App\Models\Order::taxRate())))]) }}
{{ __('billing.cart.recurring_note', ['amount' => $eur($recurringGross)]) }}
</p>
@endif
</div>

View File

@ -132,3 +132,20 @@ it('separates what recurs from a one-off top-up', function () {
Livewire\Livewire::actingAs($user)->test(Billing::class)->assertSee($expected);
});
it('keeps the recurring figure consistent with the total to the cent', function () {
[$customer, $user] = cartCustomer();
config()->set('provisioning.tax.rate_percent', 20);
// Cent-level prices: rounding after aggregating instead of per order makes
// the two figures disagree, and a customer who spots that stops trusting
// every other number on the page.
Order::factory()->for($customer)->create(['type' => 'storage', 'amount_cents' => 3, 'status' => 'pending']);
Order::factory()->for($customer)->create(['type' => 'storage', 'amount_cents' => 3, 'status' => 'pending']);
$expected = Illuminate\Support\Number::currency(0.08, in: 'EUR', locale: 'de');
Livewire\Livewire::actingAs($user)->test(Billing::class)
->assertSee($expected) // total gross
->assertSee(__('billing.cart.recurring_note', ['amount' => $expected])); // and the note
});