Fix offline route lifecycle projection
This commit is contained in:
@@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user