This commit is contained in:
2025-08-14 15:34:05 +03:00
parent ecf614e4c2
commit 7816bb3464
27 changed files with 1795 additions and 335 deletions

View File

@@ -1,6 +1,5 @@
// [PACKAGE] com.homebox.lens.data.api
// [FILE] HomeboxApiService.kt
package com.homebox.lens.data.api
import com.homebox.lens.data.api.dto.GroupStatisticsDto
@@ -8,7 +7,9 @@ import com.homebox.lens.data.api.dto.ItemCreateDto
import com.homebox.lens.data.api.dto.ItemOutDto
import com.homebox.lens.data.api.dto.ItemSummaryDto
import com.homebox.lens.data.api.dto.ItemUpdateDto
import com.homebox.lens.data.api.dto.LabelCreateDto
import com.homebox.lens.data.api.dto.LabelOutDto
import com.homebox.lens.data.api.dto.LabelSummaryDto
import com.homebox.lens.data.api.dto.LocationOutCountDto
import com.homebox.lens.data.api.dto.LoginFormDto
import com.homebox.lens.data.api.dto.PaginationResultDto
@@ -31,8 +32,6 @@ import retrofit2.http.Query
interface HomeboxApiService {
// [ENDPOINT] Auth
// [FIX] Явно указываем заголовок Content-Type, чтобы переопределить
// значение по умолчанию от Moshi, которое содержит "; charset=UTF-8".
@Headers("Content-Type: application/json")
@POST("v1/users/login")
suspend fun login(@Body loginForm: LoginFormDto): TokenResponseDto
@@ -65,9 +64,11 @@ interface HomeboxApiService {
@GET("v1/labels")
suspend fun getLabels(): List<LabelOutDto>
@POST("v1/labels")
suspend fun createLabel(@Body newLabel: LabelCreateDto): LabelSummaryDto
// [ENDPOINT] Statistics
@GET("v1/groups/statistics")
suspend fun getStatistics(): GroupStatisticsDto
}
// [END_FILE_HomeboxApiService.kt]
// [END_FILE_HomeboxApiService.kt]

View File

@@ -19,7 +19,7 @@ data class ItemOut(
@Json(name = "description") val description: String?,
@Json(name = "image") val image: String?,
@Json(name = "location") val location: LocationOut?,
@Json(name = "labels") val labels: List<LabelOut>,
@Json(name = "labels") val labels: List<LabelOutDto>,
@Json(name = "value") val value: BigDecimal?,
@Json(name = "createdAt") val createdAt: String?
)

View File

@@ -1,4 +1,23 @@
// [PACKAGE] com.homebox.lens.data.api.dto
// [FILE] LabelCreateDto.kt
// [SEMANTICS] data_transfer_object, label, create, api
package com.homebox.lens.data.api.dto
class LabelCreateDto {
}
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
/**
* [CONTRACT]
* DTO для тела запроса на создание метки (POST /v1/labels).
* @property name Название метки.
* @property color Цвет метки в формате HEX (например, "#FF0000").
* @property description Описание метки.
* @coherence_note Структура этого класса точно соответствует схеме `repo.LabelCreate` из OpenAPI.
*/
@JsonClass(generateAdapter = true)
data class LabelCreateDto(
@Json(name = "name") val name: String,
@Json(name = "color") val color: String?,
@Json(name = "description") val description: String? = null // Описание не используется в приложении, но может быть в API
)
// [END_FILE_LabelCreateDto.kt]

View File

@@ -0,0 +1,38 @@
// [PACKAGE] com.homebox.lens.data.api.dto
// [FILE] LabelSummaryDto.kt
// [SEMANTICS] data_transfer_object, label, summary, api, mapper
package com.homebox.lens.data.api.dto
import com.homebox.lens.domain.model.LabelSummary
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
/**
* [CONTRACT]
* DTO для ответа от API при создании метки.
* @coherence_note Структура этого класса точно соответствует схеме `repo.LabelSummary` из OpenAPI.
*/
@JsonClass(generateAdapter = true)
data class LabelSummaryDto(
@Json(name = "id") val id: String,
@Json(name = "name") val name: String,
@Json(name = "color") val color: String?,
@Json(name = "description") val description: String?,
@Json(name = "createdAt") val createdAt: String?,
@Json(name = "updatedAt") val updatedAt: String?
)
/**
* [CONTRACT]
* @summary Маппер из DTO в доменную модель.
* @return Объект доменной модели [LabelSummary].
* @sideeffect Отбрасывает поля, ненужные доменному слою (`color`, `description`, etc.),
* оставляя только `id` и `name`.
*/
fun LabelSummaryDto.toDomain(): LabelSummary {
return LabelSummary(
id = this.id,
name = this.name
)
}
// [END_FILE_LabelSummaryDto.kt]

View File

@@ -1,11 +1,10 @@
// [PACKAGE] com.homebox.lens.data.repository
// [FILE] ItemRepositoryImpl.kt
// [SEMANTICS] data_repository, implementation, items
// [SEMANTICS] data_repository, implementation, items, labels
package com.homebox.lens.data.repository
// [IMPORTS]
import com.homebox.lens.data.api.HomeboxApiService
import com.homebox.lens.data.api.dto.LabelCreateDto
import com.homebox.lens.data.api.dto.toDomain
import com.homebox.lens.data.api.dto.toDto
import com.homebox.lens.data.db.dao.ItemDao
@@ -16,99 +15,95 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import javax.inject.Inject
import javax.inject.Singleton
// [CORE-LOGIC]
/**
* [CONTRACT]
* Реализация репозитория для работы с данными о вещах.
* @param apiService Сервис для взаимодействия с Homebox API.
* @param itemDao DAO для доступа к локальной базе данных.
* [COHERENCE_NOTE] Метод 'login' был полностью удален из этого класса, так как его ответственность
* была передана в AuthRepositoryImpl. Это устраняет ошибку компиляции "'login' overrides nothing".
[CONTRACT]
Реализация репозитория для работы с данными о вещах.
@param apiService Сервис для взаимодействия с Homebox API.
@param itemDao DAO для доступа к локальной базе данных.
*/
@Singleton
class ItemRepositoryImpl @Inject constructor(
private val apiService: HomeboxApiService,
private val itemDao: ItemDao
) : ItemRepository {
// [DELETED] Метод login был здесь, но теперь он удален.
/**
* [CONTRACT] @see ItemRepository.createItem
[CONTRACT] @see ItemRepository.createItem
*/
override suspend fun createItem(newItemData: ItemCreate): ItemSummary {
val itemDto = newItemData.toDto()
val resultDto = apiService.createItem(itemDto)
return resultDto.toDomain()
}
/**
* [CONTRACT] @see ItemRepository.getItemDetails
[CONTRACT] @see ItemRepository.getItemDetails
*/
override suspend fun getItemDetails(itemId: String): ItemOut {
val resultDto = apiService.getItem(itemId)
return resultDto.toDomain()
}
/**
* [CONTRACT] @see ItemRepository.updateItem
[CONTRACT] @see ItemRepository.updateItem
*/
override suspend fun updateItem(itemId: String, item: ItemUpdate): ItemOut {
val itemDto = item.toDto()
val resultDto = apiService.updateItem(itemId, itemDto)
return resultDto.toDomain()
}
/**
* [CONTRACT] @see ItemRepository.deleteItem
[CONTRACT] @see ItemRepository.deleteItem
*/
override suspend fun deleteItem(itemId: String) {
apiService.deleteItem(itemId)
}
/**
* [CONTRACT] @see ItemRepository.syncInventory
[CONTRACT] @see ItemRepository.syncInventory
*/
override suspend fun syncInventory(page: Int, pageSize: Int): PaginationResult<ItemSummary> {
val resultDto = apiService.getItems(page = page, pageSize = pageSize)
return resultDto.toDomain { it.toDomain() }
}
/**
* [CONTRACT] @see ItemRepository.getStatistics
[CONTRACT] @see ItemRepository.getStatistics
*/
override suspend fun getStatistics(): GroupStatistics {
val resultDto = apiService.getStatistics()
return resultDto.toDomain()
}
/**
* [CONTRACT] @see ItemRepository.getAllLocations
[CONTRACT] @see ItemRepository.getAllLocations
*/
override suspend fun getAllLocations(): List<LocationOutCount> {
val resultDto = apiService.getLocations()
return resultDto.map { it.toDomain() }
}
/**
* [CONTRACT] @see ItemRepository.getAllLabels
[CONTRACT] @see ItemRepository.getAllLabels
*/
override suspend fun getAllLabels(): List<LabelOut> {
val resultDto = apiService.getLabels()
return resultDto.map { it.toDomain() }
}
/**
* [CONTRACT] @see ItemRepository.searchItems
[CONTRACT] @see ItemRepository.createLabel
*/
override suspend fun createLabel(newLabelData: LabelCreate): LabelSummary {
// [DATA-FLOW] Convert domain model to DTO for the API call.
val labelCreateDto = newLabelData.toDto()
// [ACTION] Call the API service.
val resultDto = apiService.createLabel(labelCreateDto)
// [DATA-FLOW] Convert the resulting DTO back to a domain model.
return resultDto.toDomain()
}
/**
[CONTRACT] @see ItemRepository.searchItems
*/
override suspend fun searchItems(query: String): PaginationResult<ItemSummary> {
val resultDto = apiService.getItems(query = query)
return resultDto.toDomain { it.toDomain() }
}
/**
* [CONTRACT] @see ItemRepository.getRecentlyAddedItems
[CONTRACT] @see ItemRepository.getRecentlyAddedItems
*/
override fun getRecentlyAddedItems(limit: Int): Flow<List<ItemSummary>> {
return itemDao.getRecentlyAddedItems(limit).map { entities ->
@@ -116,4 +111,17 @@ class ItemRepositoryImpl @Inject constructor(
}
}
}
// [HELPER] Mapper function for LabelCreate
/**
[CONTRACT]
@summary Маппер из доменной модели LabelCreate в DTO LabelCreateDto.
@return DTO-объект [LabelCreateDto].
*/
private fun LabelCreate.toDto(): LabelCreateDto {
return LabelCreateDto(
name = this.name,
color = this.color,
description = null // Description is not part of the domain model for creation.
)
}
// [END_FILE_ItemRepositoryImpl.kt]