F2.3 Android: agregar contrato móvil de Hallazgos

This commit is contained in:
2026-09-07 08:29:13 -03:00
parent dd4199cd91
commit e7dd2a7284
@@ -0,0 +1,160 @@
package com.korexlabs.dhinspeccion.data
import android.content.Context
import com.korexlabs.dhinspeccion.BuildConfig
import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import okhttp3.OkHttpClient
import retrofit2.HttpException
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.Header
import retrofit2.http.POST
import retrofit2.http.Path
data class FieldFindingAct(
val id: String,
val code: String,
val status: String,
)
data class FieldFindingCatalogItem(
val id: String,
val categoryId: String,
val code: String,
val sourceNumber: Int,
val title: String,
val legalBasis: String? = null,
val glossary: String? = null,
val suggestedSeverity: Int? = null,
val revision: Int = 1,
val categoryName: String? = null,
)
data class FieldFindingOther(
val enabled: Boolean = true,
val code: String = "OTHER",
val label: String = "OTROS",
val help: String? = null,
)
data class FieldFindingCatalog(
val typeConfigured: Boolean = false,
val configurationReason: String? = null,
val items: List<FieldFindingCatalogItem> = emptyList(),
val other: FieldFindingOther = FieldFindingOther(),
)
data class FieldFindingItem(
val id: String,
val actId: String,
val assetId: String,
val catalogItemId: String? = null,
val findingNumber: Int,
val code: String,
val status: String,
val title: String,
val description: String,
val severity: Int? = null,
val suggestedSeverity: Int? = null,
val correctionDueOn: String? = null,
)
data class FieldFindingOptionsResponse(
val act: FieldFindingAct,
val capture: CaptureStatus = CaptureStatus(),
val catalog: FieldFindingCatalog,
val findings: List<FieldFindingItem> = emptyList(),
val canAddAnother: Boolean = true,
)
data class CreateFieldFindingRequest(
val catalogItemId: String? = null,
val customTitle: String? = null,
val customLegalBasis: String? = null,
val description: String,
val severity: Int? = null,
val correctionDueOn: String? = null,
)
data class FieldFindingCreateResponse(
val act: FieldFindingAct,
val capture: CaptureStatus = CaptureStatus(),
val finding: FieldFindingItem,
val canAddAnother: Boolean = true,
)
private interface FieldFindingsApi {
@GET("inspection-visits/{visitId}/field-findings/{assetId}/options")
suspend fun options(
@Header("Authorization") authorization: String,
@Path("visitId") visitId: String,
@Path("assetId") assetId: String,
): FieldFindingOptionsResponse
@POST("inspection-visits/{visitId}/field-findings/{assetId}")
suspend fun create(
@Header("Authorization") authorization: String,
@Path("visitId") visitId: String,
@Path("assetId") assetId: String,
@Body request: CreateFieldFindingRequest,
): FieldFindingCreateResponse
@POST("auth/mobile/refresh")
suspend fun refresh(@Body request: RefreshRequest): MobileSessionResponse
}
/**
* Cliente separado para F2.3. Comparte el almacén cifrado de sesión de la APK,
* pero mantiene el contrato de Hallazgos desacoplado del cliente F2.2.
*/
class FieldFindingsRepository(context: Context) {
private val store = SecureSessionStore(context.applicationContext)
private val refreshMutex = Mutex()
private val moshi = Moshi.Builder().addLast(KotlinJsonAdapterFactory()).build()
private val api: FieldFindingsApi = Retrofit.Builder()
.baseUrl(BuildConfig.API_BASE_URL)
.client(OkHttpClient.Builder().build())
.addConverterFactory(MoshiConverterFactory.create(moshi))
.build()
.create(FieldFindingsApi::class.java)
suspend fun options(visitId: String, assetId: String): FieldFindingOptionsResponse =
authorized { session ->
api.options("Bearer ${session.accessToken}", visitId, assetId)
}
suspend fun create(
visitId: String,
assetId: String,
request: CreateFieldFindingRequest,
): FieldFindingCreateResponse = authorized { session ->
api.create("Bearer ${session.accessToken}", visitId, assetId, request)
}
private suspend fun <T> authorized(block: suspend (StoredSession) -> T): T {
var session = store.load() ?: throw IllegalStateException("Sesión no iniciada")
try {
return block(session)
} catch (error: HttpException) {
if (error.code() != 401) throw error
}
session = refresh(session.refreshToken)
return block(session)
}
private suspend fun refresh(previousRefreshToken: String): StoredSession = refreshMutex.withLock {
val latest = store.load() ?: throw IllegalStateException("Sesión no iniciada")
if (latest.refreshToken != previousRefreshToken) return@withLock latest
try {
store.save(api.refresh(RefreshRequest(previousRefreshToken)))
} catch (error: Throwable) {
store.clear()
throw error
}
}
}