Fix offline route lifecycle projection

This commit is contained in:
admin
2026-07-07 22:29:35 +02:00
parent 13159c576e
commit 37407e2737
10 changed files with 900 additions and 180 deletions
@@ -0,0 +1,143 @@
package pl.firmatpp.kierowca.ui
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Test
import pl.firmatpp.kierowca.data.model.DriverRouteDto
import pl.firmatpp.kierowca.data.upload.RouteActionEntity
import pl.firmatpp.kierowca.data.upload.RouteActionStatus
import pl.firmatpp.kierowca.data.upload.RouteActionType
class DriverRouteProjectionTest {
@Test
fun pendingStartProjectsPlannedRouteAsActiveWithLoadingWeight() {
val projection = projectDriverRoute(
route = route(status = "ZAPLANOWANA"),
actions = listOf(action(RouteActionType.Start, RouteActionStatus.Pending, weight = 12.5)),
)
assertEquals("W TRAKCIE", projection.route.status)
assertEquals(12.5, projection.route.loadingWeight)
assertEquals("active", projection.route.trackingStatus)
assertTrue(projection.loadingWeightPending)
assertFalse(projection.unloadingWeightPending)
assertEquals(RouteLifecycleAction.Finish, routeLifecyclePrimaryAction(projection.route, "2026-06-30", today)?.action)
}
@Test
fun pendingFinishProjectsActiveRouteAsFinishedWithUnloadingWeight() {
val projection = projectDriverRoute(
route = route(status = "W TRAKCIE", loadingWeight = 12.5),
actions = listOf(action(RouteActionType.Finish, RouteActionStatus.Syncing, weight = 11.8, occurredAt = "2026-06-30T09:10:00Z")),
)
assertEquals("ZAKOŃCZONA", projection.route.status)
assertEquals(11.8, projection.route.unloadingWeight)
assertEquals("finished", projection.route.trackingStatus)
assertEquals("2026-06-30T09:10:00Z", projection.route.completedAt)
assertTrue(projection.unloadingWeightPending)
assertNull(routeLifecyclePrimaryAction(projection.route, "2026-06-30", today))
}
@Test
fun startAndFinishAreAppliedChronologicallySoFinishWins() {
val projection = projectDriverRoute(
route = route(status = "ZAPLANOWANA"),
actions = listOf(
action(RouteActionType.Finish, RouteActionStatus.Pending, weight = 11.8, createdAt = 2000),
action(RouteActionType.Start, RouteActionStatus.Pending, weight = 12.5, createdAt = 1000),
),
)
assertEquals("ZAKOŃCZONA", projection.route.status)
assertEquals(12.5, projection.route.loadingWeight)
assertEquals(11.8, projection.route.unloadingWeight)
assertEquals("finished", projection.route.trackingStatus)
}
@Test
fun conflictKeepsLocalRouteValuesAndMarksStageAsAttention() {
val projection = projectDriverRoute(
route = route(status = "W TRAKCIE", loadingWeight = 12.5),
actions = listOf(action(RouteActionType.Finish, RouteActionStatus.FailedConflict, weight = 11.8)),
)
val steps = routeFlowSteps(projection.route, projection.visibleActions)
val callout = routeSyncCallout(projection.visibleActions)
assertEquals("ZAKOŃCZONA", projection.route.status)
assertEquals(11.8, projection.route.unloadingWeight)
assertEquals(RouteFlowStepState.NeedsAttention, steps[2].state)
assertEquals("Kurs wymaga obsługi. Dane wpisane w telefonie zostały zachowane.", callout?.text)
assertTrue(callout?.isConflict ?: false)
}
@Test
fun failedPermanentIsVisibleAndNeedsAttention() {
val projection = projectDriverRoute(
route = route(status = "ZAPLANOWANA"),
actions = listOf(action(RouteActionType.Start, RouteActionStatus.FailedPermanent, weight = 12.5)),
)
val steps = routeFlowSteps(projection.route, projection.visibleActions)
assertEquals("W TRAKCIE", projection.route.status)
assertEquals(12.5, projection.route.loadingWeight)
assertEquals(RouteFlowStepState.NeedsAttention, steps[0].state)
}
@Test
fun noLocalActionsLeavesRouteUnchanged() {
val route = route(status = "ZAPLANOWANA")
val projection = projectDriverRoute(route, emptyList())
assertEquals(route, projection.route)
assertFalse(projection.loadingWeightPending)
assertFalse(projection.unloadingWeightPending)
assertEquals(emptyList<RouteActionEntity>(), projection.visibleActions)
}
private fun route(
status: String,
loadingWeight: Double? = null,
unloadingWeight: Double? = null,
): DriverRouteDto =
DriverRouteDto(
id = "1",
startsAt = "",
originName = "Baza",
destinationName = "Instalacja",
contractorName = "TPP",
contractName = "Kontrakt",
contractCode = "TPP-1",
relationLabel = "Baza -> Instalacja",
status = status,
loadingWeight = loadingWeight,
unloadingWeight = unloadingWeight,
distanceKm = 1.0,
notes = null,
truck = null,
)
private fun action(
type: String,
status: String,
weight: Double,
occurredAt: String = "2026-06-30T07:40:00Z",
createdAt: Long = 1000,
): RouteActionEntity =
RouteActionEntity(
clientActionId = "action-$type-$status-$createdAt",
routeId = "1",
action = type,
weight = weight,
occurredAt = occurredAt,
photoClientRequestIdsJson = "[]",
status = status,
createdAtEpochMillis = createdAt,
)
private companion object {
val today = java.time.LocalDate.parse("2026-06-30")
}
}
@@ -9,6 +9,9 @@ import pl.firmatpp.kierowca.data.model.DriverRouteDto
import pl.firmatpp.kierowca.data.model.DispatchSheetReminderDto
import pl.firmatpp.kierowca.data.model.RoutePhotoDto
import pl.firmatpp.kierowca.data.upload.PhotoUploadEntity
import pl.firmatpp.kierowca.data.upload.RouteActionEntity
import pl.firmatpp.kierowca.data.upload.RouteActionStatus
import pl.firmatpp.kierowca.data.upload.RouteActionType
class DriverUiRulesTest {
private val today = LocalDate.parse("2026-06-30")
@@ -285,7 +288,86 @@ class DriverUiRulesTest {
assertEquals(DriverScreen.FinishRoute, activeRouteNotificationTarget(arrived = true))
}
private fun route(status: String): DriverRouteDto =
@Test
fun plannedRouteShowsLoadingAsNextStepAndStartAction() {
val route = route(status = "ZAPLANOWANA")
val steps = routeFlowSteps(route, emptyList())
val action = routeLifecyclePrimaryAction(route, "2026-06-30", today)
assertEquals(listOf("Załadunek", "W trasie", "Rozładunek"), steps.map { it.title })
assertEquals(RouteFlowStepState.Todo, steps[0].state)
assertEquals(RouteFlowStepState.Todo, steps[1].state)
assertEquals(RouteFlowStepState.Todo, steps[2].state)
assertEquals("Rozpocznij kurs", action?.label)
assertEquals(RouteLifecycleAction.Start, action?.action)
}
@Test
fun activeRouteShowsTransitStateAndFinishAction() {
val route = route(status = "W TRAKCIE", loadingWeight = 12.5)
val steps = routeFlowSteps(route, emptyList())
val action = routeLifecyclePrimaryAction(route, "2026-06-30", today)
assertEquals(RouteFlowStepState.Confirmed, steps[0].state)
assertEquals(RouteFlowStepState.LocalComplete, steps[1].state)
assertEquals(RouteFlowStepState.Todo, steps[2].state)
assertEquals("Zakończ kurs", action?.label)
assertEquals(RouteLifecycleAction.Finish, action?.action)
}
@Test
fun finishedRouteShowsCompletedStagesAndNoPrimaryAction() {
val route = route(status = "ZAKOŃCZONA", loadingWeight = 12.5, unloadingWeight = 11.8)
val steps = routeFlowSteps(route, emptyList())
val action = routeLifecyclePrimaryAction(route, "2026-06-30", today)
assertEquals(RouteFlowStepState.Confirmed, steps[0].state)
assertEquals(RouteFlowStepState.Confirmed, steps[1].state)
assertEquals(RouteFlowStepState.Confirmed, steps[2].state)
assertEquals(null, action)
}
@Test
fun pendingLocalRouteActionShowsSyncingCallout() {
val actions = listOf(action(RouteActionType.Start, RouteActionStatus.WaitingForPhotos))
val steps = routeFlowSteps(route(status = "W TRAKCIE", loadingWeight = 12.5), actions)
val callout = routeSyncCallout(actions)
assertEquals(RouteFlowStepState.Syncing, steps[0].state)
assertEquals("Czekam na wysłanie zdjęć etapu, potem wyślę zmianę statusu.", callout?.text)
assertFalse(callout?.isConflict ?: true)
}
@Test
fun failedConflictRouteActionRequiresAttention() {
val actions = listOf(action(RouteActionType.Finish, RouteActionStatus.FailedConflict, "Kurs został przepisany."))
val steps = routeFlowSteps(route(status = "W TRAKCIE", loadingWeight = 12.5), actions)
val callout = routeSyncCallout(actions)
assertEquals(RouteFlowStepState.NeedsAttention, steps[2].state)
assertEquals("Kurs wymaga obsługi. Dane wpisane w telefonie zostały zachowane.", callout?.text)
assertTrue(callout?.isConflict ?: false)
}
@Test
fun routeStageSubmitBlockerExplainsMissingRequirements() {
assertEquals("Podaj wagę.", routeStageSubmitBlocker("", serverPhotoCount = 1, localUploadCount = 0))
assertEquals("Waga musi być większa od zera.", routeStageSubmitBlocker("0", serverPhotoCount = 1, localUploadCount = 0))
assertEquals("Dodaj co najmniej jedno zdjęcie etapu.", routeStageSubmitBlocker("12,5", serverPhotoCount = 0, localUploadCount = 0))
assertEquals(null, routeStageSubmitBlocker("12,5", serverPhotoCount = 0, localUploadCount = 1))
}
private fun route(
status: String,
loadingWeight: Double? = null,
unloadingWeight: Double? = null,
photos: List<RoutePhotoDto> = emptyList(),
): DriverRouteDto =
DriverRouteDto(
id = "1",
startsAt = "",
@@ -296,9 +378,24 @@ class DriverUiRulesTest {
contractCode = "TPP-1",
relationLabel = "Baza -> Instalacja",
status = status,
loadingWeight = loadingWeight,
unloadingWeight = unloadingWeight,
distanceKm = 1.0,
notes = null,
truck = null,
photos = photos,
)
private fun action(type: String, status: String, lastError: String? = null): RouteActionEntity =
RouteActionEntity(
clientActionId = "action-$type-$status",
routeId = "1",
action = type,
weight = 12.5,
occurredAt = "2026-06-30T07:40:00Z",
photoClientRequestIdsJson = "[]",
status = status,
lastError = lastError,
)
private fun upload(status: String, serverPhotoId: String? = null): PhotoUploadEntity =
@@ -4,6 +4,10 @@ import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test
import pl.firmatpp.kierowca.data.model.DriverLeaveRequestDto
import pl.firmatpp.kierowca.data.model.DriverRouteDto
import pl.firmatpp.kierowca.data.upload.RouteActionEntity
import pl.firmatpp.kierowca.data.upload.RouteActionStatus
import pl.firmatpp.kierowca.data.upload.RouteActionType
import pl.firmatpp.kierowca.ui.theme.AppThemeMode
class DriverUiStateTest {
@@ -51,6 +55,21 @@ class DriverUiStateTest {
assertEquals(1, refreshed.leaveRequests.size)
}
@Test
fun displayRoutesAndSelectedRouteUseLocalRouteActionsProjection() {
val route = route(status = "ZAPLANOWANA")
val state = DriverUiState(
routes = listOf(route),
selectedRoute = route,
visibleRouteActions = listOf(action(RouteActionType.Start, RouteActionStatus.Pending, weight = 12.5)),
)
assertEquals("W TRAKCIE", state.displayRoutes.single().status)
assertEquals(12.5, state.displayRoutes.single().loadingWeight)
assertEquals("W TRAKCIE", state.displaySelectedRoute?.status)
assertEquals(12.5, state.displaySelectedRoute?.loadingWeight)
}
private fun leaveRequest(id: String, status: String): DriverLeaveRequestDto =
DriverLeaveRequestDto(
id = id,
@@ -59,4 +78,31 @@ class DriverUiStateTest {
type = "URLOP",
status = status,
)
private fun route(status: String): DriverRouteDto =
DriverRouteDto(
id = "1",
startsAt = "",
originName = "Baza",
destinationName = "Instalacja",
contractorName = "TPP",
contractName = "Kontrakt",
contractCode = "TPP-1",
relationLabel = "Baza -> Instalacja",
status = status,
distanceKm = 1.0,
notes = null,
truck = null,
)
private fun action(type: String, status: String, weight: Double): RouteActionEntity =
RouteActionEntity(
clientActionId = "action-$type-$status",
routeId = "1",
action = type,
weight = weight,
occurredAt = "2026-06-30T07:40:00Z",
photoClientRequestIdsJson = "[]",
status = status,
)
}