fix(invoices): send invoice amounts as exact cent-rounded decimals
Playwright Tests / test (push) Canceled after 0s

Stornorechnung saves still failed: the VAT amount was formatted invariantly
("-615.3757999999999") and re-parsed under the server's de-DE culture as
-6153757999999999, overflowing numeric(10,3) in fds__setInvoice.

- FdsInvoiceData.BuildInvoiceParams: amount params are decimal(10,x) via
  AmountParam/ParseAmount (culture-independent, rounded to cents); amounts
  beyond +/-9,999,999.99 throw InvoiceAmountOutOfRangeException.
- InvoiceService.RegisterInvoiceAsync: refuses such an invoice with a logged
  error (EventId InvoiceAmountRejected) instead of storing a wrong amount.
- Invoice editor: vat_val is cent-rounded decimal, no float noise.
- Tests: InvoiceAmountParamTests replaces the float-based Storno test.
- Bump Azure.Storage.Blobs 12.30.0; OCORE/OCORE_web submodules (AngleSharp
  1.8.3, Azure.Storage.Files.Shares 12.28.0).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Stefan
2026-10-01 10:57:41 +02:00
co-authored by Claude Opus 5.5
parent 0a8384ab33
commit d0002ccdad
9 changed files with 267 additions and 56 deletions
@@ -1,29 +1,22 @@
-- Read-only check for amounts mangled by the OCORE SQL_Float sign bug (fixed 2026-10-01).
-- Before the fix, a negative amount posted as "-19.95" was parsed with the server's de-DE culture
-- ("." = thousands separator) and stored scaled up, e.g. -19.95 -> -1995. Positive amounts were fine.
-- Affected: fds__invoices header totals (Stornos / credits) and fds__invoice_details service totals
-- written through fds__setInvoice / fds__createInvoice / fds__createInvoice_Details, plus reminder
-- amounts written by ReminderService. Line items (fds__invoice_items) were NOT affected.
-- Read-only check for amounts mangled by the SQL_Float culture bug (fixed 2026-10-01).
-- No corrections are to be made from this: issued invoices are not changed retrospectively.
--
-- Signal: the header net total disagrees with the sum of the invoice's own line items.
-- 1) Invoices with a negative header total, compared against their persisted line items.
-- What was at risk: only the VAT amount of a negative invoice (Storno / Gutschrift / Korrektur).
-- FdsInvoiceData.HighestVat formatted it invariantly ("-615.38"), and the old OCORE SQL_Float parsed a
-- negative value with the server's de-DE culture ("." = thousands separator) -> -61538. When the VAT
-- carried float noise ("-615.3757999999999") the result overflowed numeric(10,3) and the save failed
-- (the reported error); without noise it would have been stored scaled by 10/100/1000.
-- The header totals (InvoiceBalance / _net) and service totals were formatted and parsed in the same
-- culture and were never affected. Negative amounts themselves are legitimate.
--
-- Bug signature: InvoiceVAT_net1 = (InvoiceBalance - InvoiceBalance_net) * 10 / 100 / 1000.
SELECT i.[Id], i.[InvoiceId], i.[InvoiceType], i.[IsFinal], i.[DateCreated], i.[DateModified],
i.[InvoiceBalance], i.[InvoiceBalance_net], i.[InvoiceVAT_net1],
d.[InvoiceService_net], d.[InvoiceService_VAT],
items_net = it.[items_net],
suspicious = CASE WHEN it.[items_net] IS NOT NULL
AND ABS(i.[InvoiceBalance_net] - it.[items_net]) > 0.05 THEN 1 ELSE 0 END
vat_from_totals = i.[InvoiceBalance] - i.[InvoiceBalance_net],
vat_scaled_by_bug = CASE WHEN EXISTS (SELECT 1 FROM (VALUES (10), (100), (1000)) f(n)
WHERE i.[InvoiceBalance] - i.[InvoiceBalance_net] <> 0
AND ABS(i.[InvoiceVAT_net1] - (i.[InvoiceBalance] - i.[InvoiceBalance_net]) * f.n) <= 0.05 * f.n)
THEN 1 ELSE 0 END
FROM [dbo].[fds__invoices] i
LEFT JOIN [dbo].[fds__invoice_details] d ON d.[InvId] = i.[Id]
OUTER APPLY (SELECT [items_net] = SUM(x.[value_total])
FROM [dbo].[fds__invoice_items] x
WHERE x.[InvId] = i.[Id]) it
WHERE i.[InvoiceBalance] < 0 OR i.[InvoiceBalance_net] < 0
OR d.[InvoiceService_net] < 0 OR d.[InvoiceService_VAT] < 0
ORDER BY suspicious DESC, i.[DateModified] DESC;
-- 2) Reminders with a negative amount (same helper).
SELECT *
FROM [dbo].[fds__reminder]
WHERE [amount] < 0 OR [amount_payed] < 0;
WHERE i.[InvoiceBalance] < 0 OR i.[InvoiceBalance_net] < 0 OR i.[InvoiceVAT_net1] < 0
ORDER BY vat_scaled_by_bug DESC, i.[DateModified] DESC;