From 73f01d02283583355fbf0855a1e40524918e8b99 Mon Sep 17 00:00:00 2001 From: JiriUhlir <149317995+JiriUhlir@users.noreply.github.com> Date: Tue, 25 Aug 2026 10:51:45 +0200 Subject: [PATCH] oprava datumu: prijem data bez zony jako UTC SDK validuje Post/Patch modely pred odeslanim a u kazde polozky DateTime vyzaduje Kind == Utc. Datum bez zony, napriklad "2026-08-25", nacetl Newtonsoft jako Unspecified, takze POST /issued-invoices koncil chybou "DateTime must be in UTC format" jeste pred volanim iDokladu. - DateTimeZoneHandling.Utc: hodnota bez zony dostane Kind Utc bez posunu, hodnota s offsetem se prepocita do UTC - ExceptionHandlingMiddleware zachytava IdokladValidationException a vraci 400 se seznamem vadnych vlastnosti misto prazdne 500 Co-Authored-By: Claude Opus 5 (1M context) --- Infrastructure/ExceptionHandlingMiddleware.cs | 10 +++ Program.cs | 7 ++ documentation/datumy-utc.md | 71 +++++++++++++++++++ documentation/serializace-odpovedi.md | 10 ++- 4 files changed, 95 insertions(+), 3 deletions(-) create mode 100644 documentation/datumy-utc.md diff --git a/Infrastructure/ExceptionHandlingMiddleware.cs b/Infrastructure/ExceptionHandlingMiddleware.cs index b10d278..81b96ec 100644 --- a/Infrastructure/ExceptionHandlingMiddleware.cs +++ b/Infrastructure/ExceptionHandlingMiddleware.cs @@ -52,6 +52,16 @@ public sealed class ExceptionHandlingMiddleware ["idokladError"] = ex.AuthenticationError?.Error, }); } + catch (IdokladValidationException ex) + { + // The SDK validates Post/Patch models before sending them. This does not derive from + // IdokladBaseException, so without this catch the request ended as an empty 500. + _logger.LogWarning("iDoklad model validation failed: {Message}", ex.Message); + await WriteProblem(context, HttpStatusCode.BadRequest, ex.Message, new Dictionary + { + ["invalidProperties"] = ex.InvalidProperties?.Keys.ToArray(), + }); + } catch (IdokladBaseException ex) { // Other SDK-level failures (malformed responses, batch errors, etc.). diff --git a/Program.cs b/Program.cs index 0e12cbd..7d82936 100644 --- a/Program.cs +++ b/Program.cs @@ -49,6 +49,13 @@ builder.Services { options.SerializerSettings.NullValueHandling = NullValueHandling.Ignore; + // The SDK validates every DateTime member of a Post/Patch model with a UTC check and + // rejects the model when Kind is not Utc. Newtonsoft's default (RoundtripKind) leaves a + // date without a zone, e.g. "2026-08-25", as Unspecified, so such a request failed + // validation before it ever reached iDoklad. Utc marks the value as UTC without shifting + // it and converts values that do carry an offset. + options.SerializerSettings.DateTimeZoneHandling = DateTimeZoneHandling.Utc; + // Some IdokladSdk converters attached to model members only support reading (their // WriteJson throws NotImplementedException), which makes responses carrying such models // fail. The resolver bypasses them on the write path; the existing naming strategy is diff --git a/documentation/datumy-utc.md b/documentation/datumy-utc.md new file mode 100644 index 0000000..4da79cf --- /dev/null +++ b/documentation/datumy-utc.md @@ -0,0 +1,71 @@ +# Datumy v requestech a UTC kontrola SDK + +## Problém + +`POST /issued-invoices` (a stejně tak další Post/Patch endpointy) končil chybou HTTP 500 +s prázdným tělem. V logu kontejneru: + +```text +IdokladSdk.Exceptions.IdokladValidationException: Model is not valid. +DateTime must be in UTC format. +DateTime must be in UTC format. +DateTime must be in UTC format. +DateTime must be in UTC format + at IdokladSdk.Clients.BaseClient.ValidateModel[T](T model) + at IdokladSdk.Clients.BaseClient.PostAsync[TPostModel,TGetModel](String resource, TPostModel model, ...) +``` + +Chyby byly čtyři, protože model faktury má čtyři datumy: `DateOfIssue`, `DateOfMaturity`, +`DateOfTaxing` a `DateOfVatApplication`. + +Příčina: + +- Klient posílá datum bez časové zóny, například `"DateOfIssue":"2026-08-25"`. +- Newtonsoft s výchozím `DateTimeZoneHandling.RoundtripKind` takovou hodnotu načte jako + `DateTimeKind.Unspecified`. +- SDK před odesláním requestu validuje model a u každé položky `DateTime` vyžaduje `Kind == Utc`. + Validace tedy selhala ještě předtím, než se cokoli odeslalo do iDokladu. +- `IdokladValidationException` dědí z `System.ComponentModel.DataAnnotations.ValidationException`, + ne z `IdokladBaseException`. `ExceptionHandlingMiddleware` ji proto nezachytil a request skončil + jako neošetřená 500 bez těla. Z odpovědi nebylo poznat, co je špatně. + +Ověřeno, že s tím nesouvisí změna serializace z [serializace-odpovedi.md](serializace-odpovedi.md). +Deserializace stejného JSONu dává `Kind=Unspecified` shodně s původním `DefaultContractResolver` +i s novým `SdkContractResolver`. + +## Řešení + +| Soubor | Změna | +| --- | --- | +| `Program.cs` | `SerializerSettings.DateTimeZoneHandling = DateTimeZoneHandling.Utc` | +| `Infrastructure/ExceptionHandlingMiddleware.cs` | nový `catch (IdokladValidationException)` mapovaný na HTTP 400 | + +Detaily: + +- `DateTimeZoneHandling.Utc` u hodnoty bez zóny pouze nastaví `Kind` na `Utc`, hodnotu neposouvá. + Z `"2026-08-25"` vznikne `2026-08-25T00:00:00Z`, takže datum zůstává stejné. +- Hodnota, která zónu nese (například `"2026-08-25T10:00:00+02:00"`), se přepočítá do UTC. + To je požadované chování, iDoklad pracuje s UTC. +- Nastavení platí pro celý serializer, tedy pro všechny agendy, ne jen pro vydané faktury. +- Nový `catch` vrací `problem+json` se statusem 400, zprávou ze SDK a seznamem vadných vlastností + v poli `invalidProperties`. Validační chyba už neskončí jako prázdná 500. + +## Ověření + +1. `dotnet build -c Release` bez chyb. +2. Deserializace původního JSONu z logu klienta: + - před změnou všechny čtyři datumy `Kind=Unspecified` + - po změně všechny čtyři `Kind=Utc` a shodná hodnota data +3. Běh služby lokálně, `POST /issued-invoices` se stejným tělem, jaké dřív padalo: + - dříve: HTTP 500, prázdné tělo, v logu `DateTime must be in UTC format` + - nyní: HTTP 401 `invalid_client`, tedy request prošel validací a došel až k přihlášení + do iDokladu (test běžel s neplatnými credentials) +4. Model s chybějícími povinnými poli vrací HTTP 400 se seznamem chyb. + +Po nasazení musí projít: + +```text +POST https://services.csbot.cz/apps/idoklad/issued-invoices +``` + +s datumy ve tvaru `2026-08-25`. diff --git a/documentation/serializace-odpovedi.md b/documentation/serializace-odpovedi.md index 7980a5a..3ba3a8b 100644 --- a/documentation/serializace-odpovedi.md +++ b/documentation/serializace-odpovedi.md @@ -87,6 +87,10 @@ Očekávaný výsledek je HTTP 200 a v logu žádná `NotImplementedException`. ## Poznámka mimo rozsah opravy Pokud iDoklad vrátí odpověď, kterou SDK neumí zpracovat, vyhodí `IdokladValidationException`. -Ta není potomkem `IdokladBaseException`, takže ji `ExceptionHandlingMiddleware` nezachytí -a request skončí jako neošetřená 500 místo 502. Zjištěno při testu proti falešnému API, -neopravováno, protože to nesouvisí se serializací odpovědí. +Ta není potomkem `IdokladBaseException`, takže ji `ExceptionHandlingMiddleware` nezachytil +a request skončil jako neošetřená 500 místo 502. Zjištěno při testu proti falešnému API, +tehdy neopravováno, protože to nesouvisí se serializací odpovědí. + +Doplněno později: tato výjimka se ve skutečnosti vyhazuje hlavně při validaci Post/Patch modelu +před odesláním a způsobovala prázdné 500. Middleware ji nyní zachytává a vrací 400, +viz [datumy-utc.md](datumy-utc.md).