From 3bfb5f978842983c22dbc776501cd36f9ed89eb5 Mon Sep 17 00:00:00 2001 From: admin Date: Thu, 23 Jul 2026 15:02:21 +0200 Subject: [PATCH] =?UTF-8?q?Usu=C5=84=20nieaktualny=20konflikt=20zako=C5=84?= =?UTF-8?q?czenia=20kursu?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kierowca/data/upload/RouteActionDao.kt | 31 +++++++++++++++++++ .../kierowca/data/upload/RouteActionWorker.kt | 25 ++++++++++++++- .../RouteActionServerReconciliationTest.kt | 30 ++++++++++++++++++ 3 files changed, 85 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/pl/firmatpp/kierowca/data/upload/RouteActionDao.kt b/app/src/main/java/pl/firmatpp/kierowca/data/upload/RouteActionDao.kt index c449b0a..5c7c84c 100644 --- a/app/src/main/java/pl/firmatpp/kierowca/data/upload/RouteActionDao.kt +++ b/app/src/main/java/pl/firmatpp/kierowca/data/upload/RouteActionDao.kt @@ -38,6 +38,37 @@ interface RouteActionDao { ) suspend fun unconfirmedFinishesAfter(routeId: String, afterEpochMillis: Long): List + @Query( + """ + SELECT EXISTS( + SELECT 1 FROM route_actions + WHERE routeId = :routeId + AND action = 'finish' + AND status = 'CONFIRMED' + AND createdAtEpochMillis > :afterEpochMillis + ) + """, + ) + suspend fun hasConfirmedFinishAfter(routeId: String, afterEpochMillis: Long): Boolean + + @Query( + """ + UPDATE route_actions + SET status = 'CONFIRMED', + lastError = NULL, + updatedAtEpochMillis = :updatedAt + WHERE routeId = :routeId + AND action = 'finish' + AND createdAtEpochMillis < :beforeEpochMillis + AND status != 'CONFIRMED' + """, + ) + suspend fun confirmSupersededFinishes( + routeId: String, + beforeEpochMillis: Long, + updatedAt: Long = System.currentTimeMillis(), + ) + @Query("SELECT * FROM route_actions WHERE status != 'CONFIRMED'") suspend fun allUnsent(): List diff --git a/app/src/main/java/pl/firmatpp/kierowca/data/upload/RouteActionWorker.kt b/app/src/main/java/pl/firmatpp/kierowca/data/upload/RouteActionWorker.kt index 09744f2..3ee3dcb 100644 --- a/app/src/main/java/pl/firmatpp/kierowca/data/upload/RouteActionWorker.kt +++ b/app/src/main/java/pl/firmatpp/kierowca/data/upload/RouteActionWorker.kt @@ -33,6 +33,7 @@ class RouteActionWorker( if (!NetworkMonitor(applicationContext).isCurrentlyValidated()) return Result.retry() val clientActionId = inputData.getString(KEY_CLIENT_ACTION_ID) ?: return Result.failure() val action = dao.find(clientActionId) ?: return Result.failure() + if (action.status == RouteActionStatus.Confirmed) return Result.success() val startPrerequisite = if (action.action == RouteActionType.Finish) { dao.latestStartBefore(action.routeId, action.createdAtEpochMillis) } else { @@ -100,6 +101,9 @@ class RouteActionWorker( lastError = null, attemptIncrement = 0, ) + if (action.action == RouteActionType.Finish) { + dao.confirmSupersededFinishes(action.routeId, action.createdAtEpochMillis) + } syncRepository.cacheConfirmedRoute(response.route, response.route.routeDate) DriverSyncWorker.enqueue(applicationContext, response.route.routeDate, response.route.id) if (action.action == RouteActionType.Start) { @@ -130,13 +134,23 @@ class RouteActionWorker( "server_route_status" to serverRoute?.driverLifecycleStatus(), ), ) - if (serverRoute != null && routeActionMatchesServer(action, photoClientRequestIds, serverRoute)) { + val supersededByConfirmedFinish = action.action == RouteActionType.Finish && + dao.hasConfirmedFinishAfter(action.routeId, action.createdAtEpochMillis) + if (serverRoute != null && + ( + routeActionMatchesServer(action, photoClientRequestIds, serverRoute) || + routeActionWasSuperseded(action, supersededByConfirmedFinish, serverRoute) + ) + ) { dao.updateStatus( clientActionId = clientActionId, status = RouteActionStatus.Confirmed, lastError = null, attemptIncrement = 0, ) + if (action.action == RouteActionType.Finish) { + dao.confirmSupersededFinishes(action.routeId, action.createdAtEpochMillis) + } syncRepository.cacheConfirmedRoute(serverRoute, serverRoute.routeDate) DriverSyncWorker.enqueue(applicationContext, serverRoute.routeDate, serverRoute.id) return@getOrElse Result.success() @@ -233,6 +247,15 @@ internal fun routeActionMatchesServer( return weightMatches && notesMatch && photosMatch } +internal fun routeActionWasSuperseded( + action: RouteActionEntity, + hasConfirmedFinishAfter: Boolean, + serverRoute: DriverRouteDto, +): Boolean = + action.action == RouteActionType.Finish && + hasConfirmedFinishAfter && + serverRoute.driverLifecycleStatus() == "ZAKOŃCZONA" + internal fun routeActionConflictMessage(message: String, serverRoute: DriverRouteDto?): String { val serverStatus = serverRoute?.driverLifecycleStatus() ?: return message val operationalStatus = serverRoute.operationalStatus diff --git a/app/src/test/java/pl/firmatpp/kierowca/data/upload/RouteActionServerReconciliationTest.kt b/app/src/test/java/pl/firmatpp/kierowca/data/upload/RouteActionServerReconciliationTest.kt index 57d12f3..abc2fd9 100644 --- a/app/src/test/java/pl/firmatpp/kierowca/data/upload/RouteActionServerReconciliationTest.kt +++ b/app/src/test/java/pl/firmatpp/kierowca/data/upload/RouteActionServerReconciliationTest.kt @@ -50,6 +50,36 @@ class RouteActionServerReconciliationTest { ) } + @Test + fun laterConfirmedFinishSupersedesAnOlderConflictingFinish() { + assertTrue( + routeActionWasSuperseded( + action = finish(weight = 11.8, notes = "Starsza uwaga"), + hasConfirmedFinishAfter = true, + serverRoute = route( + status = "ZAKOŃCZONA", + weight = null, + notes = null, + ), + ), + ) + } + + @Test + fun completedServerRouteAloneDoesNotDiscardConflictingLocalData() { + assertFalse( + routeActionWasSuperseded( + action = finish(weight = 11.8, notes = "Nadal ważna uwaga"), + hasConfirmedFinishAfter = false, + serverRoute = route( + status = "ZAKOŃCZONA", + weight = null, + notes = null, + ), + ), + ) + } + private fun finish(weight: Double?, notes: String?): RouteActionEntity = RouteActionEntity( clientActionId = "finish-1",