From 847ba1c2f89e52a2fb1a11c95b511fa856533e21 Mon Sep 17 00:00:00 2001 From: javier Date: Tue, 21 Jul 2026 12:18:29 +0200 Subject: [PATCH] fix(sync-api): normalizar client_updated_at ISO-8601 a datetime MySQL MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit La app envía client_updated_at como ISO-8601 con milisegundos y Z (2026-07-21T10:10:25.006Z). MySQL rechaza ese literal en columnas DATETIME/TIMESTAMP → SQLSTATE[22007] y la operación /sync fallaba (inspection.create, etc.). Se normaliza con Carbon a 'Y-m-d H:i:s' en la zona horaria de la app, una sola vez en handle(), antes de cada handler. Co-Authored-By: Claude Opus 4.8 --- app/Http/Controllers/Api/V1/SyncController.php | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/app/Http/Controllers/Api/V1/SyncController.php b/app/Http/Controllers/Api/V1/SyncController.php index cf1b7a3..ea4a011 100644 --- a/app/Http/Controllers/Api/V1/SyncController.php +++ b/app/Http/Controllers/Api/V1/SyncController.php @@ -52,6 +52,20 @@ class SyncController extends Controller { $uuid = $op['uuid']; + // The client sends `client_updated_at` as ISO-8601 with milliseconds and a + // trailing Z (e.g. 2026-07-21T10:10:25.006Z). MySQL's DATETIME/TIMESTAMP + // columns reject that literal, so normalise it to `Y-m-d H:i:s` (UTC) once + // here, before it reaches any handler. Invalid/empty values become null. + if (! empty($op['client_updated_at'])) { + try { + $op['client_updated_at'] = Carbon::parse($op['client_updated_at']) + ->timezone(config('app.timezone')) + ->format('Y-m-d H:i:s'); + } catch (\Throwable) { + $op['client_updated_at'] = null; + } + } + // Op-level idempotency: if this operation was already applied, replay its result. $prior = SyncLog::where('op_uuid', $uuid) ->where('entity', $op['entity'])->where('op', $op['op'])->first();