initial commit

This commit is contained in:
2025-08-06 11:28:28 +03:00
commit b63eca8440
88 changed files with 3392 additions and 0 deletions

73
data/build.gradle.kts Normal file
View File

@@ -0,0 +1,73 @@
// [FILE] data/build.gradle.kts
// [PURPOSE] Build script for the data module.
plugins {
id("com.android.library")
id("org.jetbrains.kotlin.android")
id("com.google.dagger.hilt.android")
id("kotlin-kapt")
}
android {
namespace = "com.homebox.lens.data"
compileSdk = Versions.compileSdk
defaultConfig {
minSdk = Versions.minSdk
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
}
dependencies {
// [MODULE_DEPENDENCY] Domain module
implementation(project(":domain"))
// [DEPENDENCY] AndroidX
implementation(Libs.coreKtx)
// [DEPENDENCY] Coroutines
implementation(Libs.coroutinesCore)
// [DEPENDENCY] Networking (Retrofit, Gson)
implementation(Libs.retrofit)
implementation(Libs.converterGson)
implementation(Libs.okhttp)
implementation(Libs.okhttpLoggingInterceptor)
// [DEPENDENCY] Database (Room)
implementation(Libs.roomRuntime)
implementation(Libs.roomKtx)
kapt(Libs.roomCompiler)
// [DEPENDENCY] DI (Hilt)
implementation(Libs.hiltAndroid)
kapt(Libs.hiltCompiler)
// [DEPENDENCY] Testing
testImplementation(Libs.junit)
androidTestImplementation(Libs.extJunit)
androidTestImplementation(Libs.espressoCore)
}
kapt {
correctErrorTypes = true
}
// [END_FILE_data/build.gradle.kts]

View File

@@ -0,0 +1,63 @@
// [PACKAGE] com.homebox.lens.data.api
// [FILE] HomeboxApiService.kt
package com.homebox.lens.data.api
import com.homebox.lens.data.api.dto.GroupStatisticsDto
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.LabelOutDto
import com.homebox.lens.data.api.dto.LocationOutCountDto
import com.homebox.lens.data.api.dto.PaginationResultDto
import retrofit2.Response
import retrofit2.http.Body
import retrofit2.http.DELETE
import retrofit2.http.GET
import retrofit2.http.POST
import retrofit2.http.PUT
import retrofit2.http.Path
import retrofit2.http.Query
// [CONTRACT]
/**
* [ENTITY: Interface('HomeboxApiService')]
* [PURPOSE] Определяет эндпоинты для взаимодействия с Homebox API, используя DTO.
*/
interface HomeboxApiService {
// [ENDPOINT] Items
@GET("v1/items")
suspend fun getItems(
@Query("q") query: String? = null,
@Query("page") page: Int? = null,
@Query("pageSize") pageSize: Int? = null
): PaginationResultDto<ItemSummaryDto>
@POST("v1/items")
suspend fun createItem(@Body item: ItemCreateDto): ItemSummaryDto
@GET("v1/items/{id}")
suspend fun getItem(@Path("id") itemId: String): ItemOutDto
@PUT("v1/items/{id}")
suspend fun updateItem(@Path("id") itemId: String, @Body item: ItemUpdateDto): ItemOutDto
@DELETE("v1/items/{id}")
suspend fun deleteItem(@Path("id") itemId: String): Response<Unit>
// [ENDPOINT] Locations
@GET("v1/locations")
suspend fun getLocations(): List<LocationOutCountDto>
// [ENDPOINT] Labels
@GET("v1/labels")
suspend fun getLabels(): List<LabelOutDto>
// [ENDPOINT] Statistics
@GET("v1/groups/statistics")
suspend fun getStatistics(): GroupStatisticsDto
}
// [END_FILE_HomeboxApiService.kt]

View File

@@ -0,0 +1,30 @@
// [PACKAGE] com.homebox.lens.data.api.dto
// [FILE] CustomFieldDto.kt
// [SEMANTICS] data_transfer_object, custom_field
// [IMPORTS]
import com.google.gson.annotations.SerializedName
import com.homebox.lens.domain.model.CustomField
// [CORE-LOGIC]
/**
* [CONTRACT]
* DTO для кастомного поля.
*/
data class CustomFieldDto(
@SerializedName("name") val name: String,
@SerializedName("value") val value: String,
@SerializedName("type") val type: String
)
/**
* [CONTRACT]
* Маппер из CustomFieldDto в доменную модель CustomField.
*/
fun CustomFieldDto.toDomain(): CustomField {
return CustomField(
name = this.name,
value = this.value,
type = this.type
)
}

View File

@@ -0,0 +1,32 @@
// [PACKAGE] com.homebox.lens.data.api.dto
// [FILE] GroupStatisticsDto.kt
// [SEMANTICS] data_transfer_object, statistics
// [IMPORTS]
import com.google.gson.annotations.SerializedName
import com.homebox.lens.domain.model.GroupStatistics
// [CORE-LOGIC]
/**
* [CONTRACT]
* DTO для статистики.
*/
data class GroupStatisticsDto(
@SerializedName("items") val items: Int,
@SerializedName("labels") val labels: Int,
@SerializedName("locations") val locations: Int,
@SerializedName("totalValue") val totalValue: Double
)
/**
* [CONTRACT]
* Маппер из GroupStatisticsDto в доменную модель GroupStatistics.
*/
fun GroupStatisticsDto.toDomain(): GroupStatistics {
return GroupStatistics(
items = this.items,
labels = this.labels,
locations = this.locations,
totalValue = this.totalValue
)
}

View File

@@ -0,0 +1,33 @@
// [PACKAGE] com.homebox.lens.data.api.dto
// [FILE] ImageDto.kt
// [SEMANTICS] data_transfer_object, image
// [IMPORTS]
import com.google.gson.annotations.SerializedName
import com.homebox.lens.domain.model.Image
// [CORE-LOGIC]
/**
* [CONTRACT]
* DTO для изображения.
* @property id Уникальный идентификатор.
* @property path Путь к файлу.
* @property isPrimary Является ли основным.
*/
data class ImageDto(
@SerializedName("id") val id: String,
@SerializedName("path") val path: String,
@SerializedName("isPrimary") val isPrimary: Boolean
)
/**
* [CONTRACT]
* Маппер из ImageDto в доменную модель Image.
*/
fun ImageDto.toDomain(): Image {
return Image(
id = this.id,
path = this.path,
isPrimary = this.isPrimary
)
}

View File

@@ -0,0 +1,36 @@
// [PACKAGE] com.homebox.lens.data.api.dto
// [FILE] ItemAttachmentDto.kt
// [SEMANTICS] data_transfer_object, attachment
// [IMPORTS]
import com.google.gson.annotations.SerializedName
import com.homebox.lens.domain.model.ItemAttachment
// [CORE-LOGIC]
/**
* [CONTRACT]
* DTO для вложения.
*/
data class ItemAttachmentDto(
@SerializedName("id") val id: String,
@SerializedName("name") val name: String,
@SerializedName("path") val path: String,
@SerializedName("type") val type: String,
@SerializedName("createdAt") val createdAt: String,
@SerializedName("updatedAt") val updatedAt: String
)
/**
* [CONTRACT]
* Маппер из ItemAttachmentDto в доменную модель ItemAttachment.
*/
fun ItemAttachmentDto.toDomain(): ItemAttachment {
return ItemAttachment(
id = this.id,
name = this.name,
path = this.path,
type = this.type,
createdAt = this.createdAt,
updatedAt = this.updatedAt
)
}

View File

@@ -0,0 +1,50 @@
// [PACKAGE] com.homebox.lens.data.api.dto
// [FILE] ItemCreateDto.kt
// [SEMANTICS] data_transfer_object, item_creation
// [IMPORTS]
import com.google.gson.annotations.SerializedName
import com.homebox.lens.domain.model.ItemCreate
// [CORE-LOGIC]
/**
* [CONTRACT]
* DTO для создания вещи.
*/
data class ItemCreateDto(
@SerializedName("name") val name: String,
@SerializedName("assetId") val assetId: String?,
@SerializedName("description") val description: String?,
@SerializedName("notes") val notes: String?,
@SerializedName("serialNumber") val serialNumber: String?,
@SerializedName("quantity") val quantity: Int?,
@SerializedName("value") val value: Double?,
@SerializedName("purchasePrice") val purchasePrice: Double?,
@SerializedName("purchaseDate") val purchaseDate: String?,
@SerializedName("warrantyUntil") val warrantyUntil: String?,
@SerializedName("locationId") val locationId: String?,
@SerializedName("parentId") val parentId: String?,
@SerializedName("labelIds") val labelIds: List<String>?
)
/**
* [CONTRACT]
* Маппер из доменной модели ItemCreate в ItemCreateDto.
*/
fun ItemCreate.toDto(): ItemCreateDto {
return ItemCreateDto(
name = this.name,
assetId = this.assetId,
description = this.description,
notes = this.notes,
serialNumber = this.serialNumber,
quantity = this.quantity,
value = this.value,
purchasePrice = this.purchasePrice,
purchaseDate = this.purchaseDate,
warrantyUntil = this.warrantyUntil,
locationId = this.locationId,
parentId = this.parentId,
labelIds = this.labelIds
)
}

View File

@@ -0,0 +1,66 @@
// [PACKAGE] com.homebox.lens.data.api.dto
// [FILE] ItemDto.kt
package com.homebox.lens.data.api.dto
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import java.math.BigDecimal
// [CONTRACT]
/**
* [ENTITY: DataClass('ItemOut')]
* [PURPOSE] DTO для полной информации о вещи (GET /v1/items/{id}).
*/
@JsonClass(generateAdapter = true)
data class ItemOut(
@Json(name = "id") val id: String,
@Json(name = "name") val name: String,
@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 = "value") val value: BigDecimal?,
@Json(name = "createdAt") val createdAt: String?
)
/**
* [ENTITY: DataClass('ItemSummary')]
* [PURPOSE] DTO для краткой информации о вещи в списках (GET /v1/items).
*/
@JsonClass(generateAdapter = true)
data class ItemSummary(
@Json(name = "id") val id: String,
@Json(name = "name") val name: String,
@Json(name = "image") val image: String?,
@Json(name = "location") val location: LocationOut?,
@Json(name = "createdAt") val createdAt: String?
)
/**
* [ENTITY: DataClass('ItemCreate')]
* [PURPOSE] DTO для создания новой вещи (POST /v1/items).
*/
@JsonClass(generateAdapter = true)
data class ItemCreate(
@Json(name = "name") val name: String,
@Json(name = "description") val description: String?,
@Json(name = "locationId") val locationId: String?,
@Json(name = "labelIds") val labelIds: List<String>?,
@Json(name = "value") val value: BigDecimal?
)
/**
* [ENTITY: DataClass('ItemUpdate')]
* [PURPOSE] DTO для обновления вещи (PUT /v1/items/{id}).
*/
@JsonClass(generateAdapter = true)
data class ItemUpdate(
@Json(name = "name") val name: String,
@Json(name = "description") val description: String?,
@Json(name = "locationId") val locationId: String?,
@Json(name = "labelIds") val labelIds: List<String>?,
@Json(name = "value") val value: BigDecimal?
)
// [END_FILE_ItemDto.kt]

View File

@@ -0,0 +1,68 @@
// [PACKAGE] com.homebox.lens.data.api.dto
// [FILE] ItemOutDto.kt
// [SEMANTICS] data_transfer_object, item_detailed
// [IMPORTS]
import com.google.gson.annotations.SerializedName
import com.homebox.lens.domain.model.ItemOut
// [CORE-LOGIC]
/**
* [CONTRACT]
* DTO для полной модели вещи.
*/
data class ItemOutDto(
@SerializedName("id") val id: String,
@SerializedName("name") val name: String,
@SerializedName("assetId") val assetId: String?,
@SerializedName("description") val description: String?,
@SerializedName("notes") val notes: String?,
@SerializedName("serialNumber") val serialNumber: String?,
@SerializedName("quantity") val quantity: Int,
@SerializedName("isArchived") val isArchived: Boolean,
@SerializedName("value") val value: Double,
@SerializedName("purchasePrice") val purchasePrice: Double?,
@SerializedName("purchaseDate") val purchaseDate: String?,
@SerializedName("warrantyUntil") val warrantyUntil: String?,
@SerializedName("location") val location: LocationOutDto?,
@SerializedName("parent") val parent: ItemSummaryDto?,
@SerializedName("children") val children: List<ItemSummaryDto>,
@SerializedName("labels") val labels: List<LabelOutDto>,
@SerializedName("attachments") val attachments: List<ItemAttachmentDto>,
@SerializedName("images") val images: List<ImageDto>,
@SerializedName("fields") val fields: List<CustomFieldDto>,
@SerializedName("maintenance") val maintenance: List<MaintenanceEntryDto>,
@SerializedName("createdAt") val createdAt: String,
@SerializedName("updatedAt") val updatedAt: String
)
/**
* [CONTRACT]
* Маппер из ItemOutDto в доменную модель ItemOut.
*/
fun ItemOutDto.toDomain(): ItemOut {
return ItemOut(
id = this.id,
name = this.name,
assetId = this.assetId,
description = this.description,
notes = this.notes,
serialNumber = this.serialNumber,
quantity = this.quantity,
isArchived = this.isArchived,
value = this.value,
purchasePrice = this.purchasePrice,
purchaseDate = this.purchaseDate,
warrantyUntil = this.warrantyUntil,
location = this.location?.toDomain(),
parent = this.parent?.toDomain(),
children = this.children.map { it.toDomain() },
labels = this.labels.map { it.toDomain() },
attachments = this.attachments.map { it.toDomain() },
images = this.images.map { it.toDomain() },
fields = this.fields.map { it.toDomain() },
maintenance = this.maintenance.map { it.toDomain() },
createdAt = this.createdAt,
updatedAt = this.updatedAt
)
}

View File

@@ -0,0 +1,44 @@
// [PACKAGE] com.homebox.lens.data.api.dto
// [FILE] ItemSummaryDto.kt
// [SEMANTICS] data_transfer_object, item_summary
// [IMPORTS]
import com.google.gson.annotations.SerializedName
import com.homebox.lens.domain.model.ItemSummary
// [CORE-LOGIC]
/**
* [CONTRACT]
* DTO для сокращенной модели вещи.
*/
data class ItemSummaryDto(
@SerializedName("id") val id: String,
@SerializedName("name") val name: String,
@SerializedName("assetId") val assetId: String?,
@SerializedName("image") val image: ImageDto?,
@SerializedName("isArchived") val isArchived: Boolean,
@SerializedName("labels") val labels: List<LabelOutDto>,
@SerializedName("location") val location: LocationOutDto?,
@SerializedName("value") val value: Double,
@SerializedName("createdAt") val createdAt: String,
@SerializedName("updatedAt") val updatedAt: String
)
/**
* [CONTRACT]
* Маппер из ItemSummaryDto в доменную модель ItemSummary.
*/
fun ItemSummaryDto.toDomain(): ItemSummary {
return ItemSummary(
id = this.id,
name = this.name,
assetId = this.assetId,
image = this.image?.toDomain(),
isArchived = this.isArchived,
labels = this.labels.map { it.toDomain() },
location = this.location?.toDomain(),
value = this.value,
createdAt = this.createdAt,
updatedAt = this.updatedAt
)
}

View File

@@ -0,0 +1,52 @@
// [PACKAGE] com.homebox.lens.data.api.dto
// [FILE] ItemUpdateDto.kt
// [SEMANTICS] data_transfer_object, item_update
// [IMPORTS]
import com.google.gson.annotations.SerializedName
import com.homebox.lens.domain.model.ItemUpdate
// [CORE-LOGIC]
/**
* [CONTRACT]
* DTO для обновления вещи.
*/
data class ItemUpdateDto(
@SerializedName("name") val name: String?,
@SerializedName("assetId") val assetId: String?,
@SerializedName("description") val description: String?,
@SerializedName("notes") val notes: String?,
@SerializedName("serialNumber") val serialNumber: String?,
@SerializedName("quantity") val quantity: Int?,
@SerializedName("isArchived") val isArchived: Boolean?,
@SerializedName("value") val value: Double?,
@SerializedName("purchasePrice") val purchasePrice: Double?,
@SerializedName("purchaseDate") val purchaseDate: String?,
@SerializedName("warrantyUntil") val warrantyUntil: String?,
@SerializedName("locationId") val locationId: String?,
@SerializedName("parentId") val parentId: String?,
@SerializedName("labelIds") val labelIds: List<String>?
)
/**
* [CONTRACT]
* Маппер из доменной модели ItemUpdate в ItemUpdateDto.
*/
fun ItemUpdate.toDto(): ItemUpdateDto {
return ItemUpdateDto(
name = this.name,
assetId = this.assetId,
description = this.description,
notes = this.notes,
serialNumber = this.serialNumber,
quantity = this.quantity,
isArchived = this.isArchived,
value = this.value,
purchasePrice = this.purchasePrice,
purchaseDate = this.purchaseDate,
warrantyUntil = this.warrantyUntil,
locationId = this.locationId,
parentId = this.parentId,
labelIds = this.labelIds
)
}

View File

@@ -0,0 +1,20 @@
// [PACKAGE] com.homebox.lens.data.api.dto
// [FILE] LabelDto.kt
package com.homebox.lens.data.api.dto
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
// [CONTRACT]
/**
* [ENTITY: DataClass('LabelOut')]
* [PURPOSE] DTO для информации о метке.
*/
@JsonClass(generateAdapter = true)
data class LabelOut(
@Json(name = "id") val id: String,
@Json(name = "name") val name: String
)
// [END_FILE_LabelDto.kt]

View File

@@ -0,0 +1,36 @@
// [PACKAGE] com.homebox.lens.data.api.dto
// [FILE] LabelOutDto.kt
// [SEMANTICS] data_transfer_object, label
// [IMPORTS]
import com.google.gson.annotations.SerializedName
import com.homebox.lens.domain.model.LabelOut
// [CORE-LOGIC]
/**
* [CONTRACT]
* DTO для метки.
*/
data class LabelOutDto(
@SerializedName("id") val id: String,
@SerializedName("name") val name: String,
@SerializedName("color") val color: String,
@SerializedName("isArchived") val isArchived: Boolean,
@SerializedName("createdAt") val createdAt: String,
@SerializedName("updatedAt") val updatedAt: String
)
/**
* [CONTRACT]
* Маппер из LabelOutDto в доменную модель LabelOut.
*/
fun LabelOutDto.toDomain(): LabelOut {
return LabelOut(
id = this.id,
name = this.name,
color = this.color,
isArchived = this.isArchived,
createdAt = this.createdAt,
updatedAt = this.updatedAt
)
}

View File

@@ -0,0 +1,31 @@
// [PACKAGE] com.homebox.lens.data.api.dto
// [FILE] LocationDto.kt
package com.homebox.lens.data.api.dto
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
// [CONTRACT]
/**
* [ENTITY: DataClass('LocationOut')]
* [PURPOSE] DTO для информации о местоположении.
*/
@JsonClass(generateAdapter = true)
data class LocationOut(
@Json(name = "id") val id: String,
@Json(name = "name") val name: String
)
/**
* [ENTITY: DataClass('LocationOutCount')]
* [PURPOSE] DTO для информации о местоположении со счетчиком вещей.
*/
@JsonClass(generateAdapter = true)
data class LocationOutCount(
@Json(name = "id") val id: String,
@Json(name = "name") val name: String,
@Json(name = "itemCount") val itemCount: Int
)
// [END_FILE_LocationDto.kt]

View File

@@ -0,0 +1,38 @@
// [PACKAGE] com.homebox.lens.data.api.dto
// [FILE] LocationOutCountDto.kt
// [SEMANTICS] data_transfer_object, location, count
// [IMPORTS]
import com.google.gson.annotations.SerializedName
import com.homebox.lens.domain.model.LocationOutCount
// [CORE-LOGIC]
/**
* [CONTRACT]
* DTO для местоположения со счетчиком.
*/
data class LocationOutCountDto(
@SerializedName("id") val id: String,
@SerializedName("name") val name: String,
@SerializedName("color") val color: String,
@SerializedName("isArchived") val isArchived: Boolean,
@SerializedName("itemCount") val itemCount: Int,
@SerializedName("createdAt") val createdAt: String,
@SerializedName("updatedAt") val updatedAt: String
)
/**
* [CONTRACT]
* Маппер из LocationOutCountDto в доменную модель LocationOutCount.
*/
fun LocationOutCountDto.toDomain(): LocationOutCount {
return LocationOutCount(
id = this.id,
name = this.name,
color = this.color,
isArchived = this.isArchived,
itemCount = this.itemCount,
createdAt = this.createdAt,
updatedAt = this.updatedAt
)
}

View File

@@ -0,0 +1,36 @@
// [PACKAGE] com.homebox.lens.data.api.dto
// [FILE] LocationOutDto.kt
// [SEMANTICS] data_transfer_object, location
// [IMPORTS]
import com.google.gson.annotations.SerializedName
import com.homebox.lens.domain.model.LocationOut
// [CORE-LOGIC]
/**
* [CONTRACT]
* DTO для местоположения.
*/
data class LocationOutDto(
@SerializedName("id") val id: String,
@SerializedName("name") val name: String,
@SerializedName("color") val color: String,
@SerializedName("isArchived") val isArchived: Boolean,
@SerializedName("createdAt") val createdAt: String,
@SerializedName("updatedAt") val updatedAt: String
)
/**
* [CONTRACT]
* Маппер из LocationOutDto в доменную модель LocationOut.
*/
fun LocationOutDto.toDomain(): LocationOut {
return LocationOut(
id = this.id,
name = this.name,
color = this.color,
isArchived = this.isArchived,
createdAt = this.createdAt,
updatedAt = this.updatedAt
)
}

View File

@@ -0,0 +1,40 @@
// [PACKAGE] com.homebox.lens.data.api.dto
// [FILE] MaintenanceEntryDto.kt
// [SEMANTICS] data_transfer_object, maintenance
// [IMPORTS]
import com.google.gson.annotations.SerializedName
import com.homebox.lens.domain.model.MaintenanceEntry
// [CORE-LOGIC]
/**
* [CONTRACT]
* DTO для записи об обслуживании.
*/
data class MaintenanceEntryDto(
@SerializedName("id") val id: String,
@SerializedName("itemId") val itemId: String,
@SerializedName("title") val title: String,
@SerializedName("details") val details: String?,
@SerializedName("dueAt") val dueAt: String?,
@SerializedName("completedAt") val completedAt: String?,
@SerializedName("createdAt") val createdAt: String,
@SerializedName("updatedAt") val updatedAt: String
)
/**
* [CONTRACT]
* Маппер из MaintenanceEntryDto в доменную модель MaintenanceEntry.
*/
fun MaintenanceEntryDto.toDomain(): MaintenanceEntry {
return MaintenanceEntry(
id = this.id,
itemId = this.itemId,
title = this.title,
details = this.details,
dueAt = this.dueAt,
completedAt = this.completedAt,
createdAt = this.createdAt,
updatedAt = this.updatedAt
)
}

View File

@@ -0,0 +1,23 @@
// [PACKAGE] com.homebox.lens.data.api.dto
// [FILE] PaginationDto.kt
package com.homebox.lens.data.api.dto
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
// [CONTRACT]
/**
* [ENTITY: DataClass('PaginationResult')]
* [PURPOSE] DTO для пагинированных результатов от API.
*/
@JsonClass(generateAdapter = true)
data class PaginationResult<T>(
@Json(name = "items") val items: List<T>,
@Json(name = "page") val page: Int,
@Json(name = "pages") val pages: Int,
@Json(name = "total") val total: Int,
@Json(name = "pageSize") val pageSize: Int
)
// [END_FILE_PaginationDto.kt]

View File

@@ -0,0 +1,33 @@
// [PACKAGE] com.homebox.lens.data.api.dto
// [FILE] PaginationResultDto.kt
// [SEMANTICS] data_transfer_object, pagination
// [IMPORTS]
import com.google.gson.annotations.SerializedName
import com.homebox.lens.domain.model.PaginationResult
// [CORE-LOGIC]
/**
* [CONTRACT]
* DTO для постраничных результатов.
*/
data class PaginationResultDto<T>(
@SerializedName("items") val items: List<T>,
@SerializedName("page") val page: Int,
@SerializedName("pageSize") val pageSize: Int,
@SerializedName("total") val total: Int
)
/**
* [CONTRACT]
* Маппер из PaginationResultDto в доменную модель PaginationResult.
* @param transform Функция для преобразования каждого элемента из DTO в доменную модель.
*/
fun <T, R> PaginationResultDto<T>.toDomain(transform: (T) -> R): PaginationResult<R> {
return PaginationResult(
items = this.items.map(transform),
page = this.page,
pageSize = this.pageSize,
total = this.total
)
}

View File

@@ -0,0 +1,23 @@
// [PACKAGE] com.homebox.lens.data.api.dto
// [FILE] StatisticsDto.kt
package com.homebox.lens.data.api.dto
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import java.math.BigDecimal
// [CONTRACT]
/**
* [ENTITY: DataClass('GroupStatistics')]
* [PURPOSE] DTO для статистической информации.
*/
@JsonClass(generateAdapter = true)
data class GroupStatistics(
@Json(name = "totalValue") val totalValue: BigDecimal,
@Json(name = "totalItems") val totalItems: Int,
@Json(name = "locations") val locations: Int,
@Json(name = "labels") val labels: Int
)
// [END_FILE_StatisticsDto.kt]

View File

@@ -0,0 +1,26 @@
// [PACKAGE] com.homebox.lens.data.db
// [FILE] Converters.kt
package com.homebox.lens.data.db
import androidx.room.TypeConverter
import java.math.BigDecimal
// [CONTRACT]
/**
* [ENTITY: Class('Converters')]
* [PURPOSE] Предоставляет TypeConverters для Room для типов, не поддерживаемых по умолчанию.
*/
class Converters {
@TypeConverter
fun fromString(value: String?): BigDecimal? {
return value?.let { BigDecimal(it) }
}
@TypeConverter
fun bigDecimalToString(bigDecimal: BigDecimal?): String? {
return bigDecimal?.toPlainString()
}
}
// [END_FILE_Converters.kt]

View File

@@ -0,0 +1,41 @@
// [PACKAGE] com.homebox.lens.data.db
// [FILE] HomeboxDatabase.kt
package com.homebox.lens.data.db
import androidx.room.Database
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import com.homebox.lens.data.db.dao.ItemDao
import com.homebox.lens.data.db.dao.LabelDao
import com.homebox.lens.data.db.dao.LocationDao
import com.homebox.lens.data.db.entity.*
// [CONTRACT]
/**
* [ENTITY: RoomDatabase('HomeboxDatabase')]
* [PURPOSE] Основной класс для работы с локальной базой данных Room.
*/
@Database(
entities = [
ItemEntity::class,
LabelEntity::class,
LocationEntity::class,
ItemLabelCrossRef::class
],
version = 1,
exportSchema = false
)
@TypeConverters(Converters::class)
abstract class HomeboxDatabase : RoomDatabase() {
abstract fun itemDao(): ItemDao
abstract fun labelDao(): LabelDao
abstract fun locationDao(): LocationDao
companion object {
const val DATABASE_NAME = "homebox_lens_db"
}
}
// [END_FILE_HomeboxDatabase.kt]

View File

@@ -0,0 +1,40 @@
// [PACKAGE] com.homebox.lens.data.db.dao
// [FILE] ItemDao.kt
package com.homebox.lens.data.db.dao
import androidx.room.*
import com.homebox.lens.data.db.entity.ItemEntity
import com.homebox.lens.data.db.entity.ItemLabelCrossRef
import com.homebox.lens.data.db.entity.ItemWithLabels
// [CONTRACT]
/**
* [ENTITY: RoomDao('ItemDao')]
* [PURPOSE] Предоставляет методы для работы с 'items' в локальной БД.
*/
@Dao
interface ItemDao {
@Transaction
@Query("SELECT * FROM items")
suspend fun getItems(): List<ItemWithLabels>
@Transaction
@Query("SELECT * FROM items WHERE id = :itemId")
suspend fun getItem(itemId: String): ItemWithLabels?
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertItems(items: List<ItemEntity>)
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertItem(item: ItemEntity)
@Query("DELETE FROM items WHERE id = :itemId")
suspend fun deleteItem(itemId: String)
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertItemLabelCrossRefs(crossRefs: List<ItemLabelCrossRef>)
}
// [END_FILE_ItemDao.kt]

View File

@@ -0,0 +1,27 @@
// [PACKAGE] com.homebox.lens.data.db.dao
// [FILE] LabelDao.kt
package com.homebox.lens.data.db.dao
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import com.homebox.lens.data.db.entity.LabelEntity
// [CONTRACT]
/**
* [ENTITY: RoomDao('LabelDao')]
* [PURPOSE] Предоставляет методы для работы с 'labels' в локальной БД.
*/
@Dao
interface LabelDao {
@Query("SELECT * FROM labels")
suspend fun getLabels(): List<LabelEntity>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertLabels(labels: List<LabelEntity>)
}
// [END_FILE_LabelDao.kt]

View File

@@ -0,0 +1,27 @@
// [PACKAGE] com.homebox.lens.data.db.dao
// [FILE] LocationDao.kt
package com.homebox.lens.data.db.dao
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import com.homebox.lens.data.db.entity.LocationEntity
// [CONTRACT]
/**
* [ENTITY: RoomDao('LocationDao')]
* [PURPOSE] Предоставляет методы для работы с 'locations' в локальной БД.
*/
@Dao
interface LocationDao {
@Query("SELECT * FROM locations")
suspend fun getLocations(): List<LocationEntity>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertLocations(locations: List<LocationEntity>)
}
// [END_FILE_LocationDao.kt]

View File

@@ -0,0 +1,26 @@
// [PACKAGE] com.homebox.lens.data.db.entity
// [FILE] ItemEntity.kt
package com.homebox.lens.data.db.entity
import androidx.room.Entity
import androidx.room.PrimaryKey
import java.math.BigDecimal
// [CONTRACT]
/**
* [ENTITY: RoomEntity('ItemEntity')]
* [PURPOSE] Представляет собой строку в таблице 'items' в локальной БД.
*/
@Entity(tableName = "items")
data class ItemEntity(
@PrimaryKey val id: String,
val name: String,
val description: String?,
val image: String?,
val locationId: String?,
val value: BigDecimal?,
val createdAt: String?
)
// [END_FILE_ItemEntity.kt]

View File

@@ -0,0 +1,19 @@
// [PACKAGE] com.homebox.lens.data.db.entity
// [FILE] ItemLabelCrossRef.kt
package com.homebox.lens.data.db.entity
import androidx.room.Entity
// [CONTRACT]
/**
* [ENTITY: RoomEntity('ItemLabelCrossRef')]
* [PURPOSE] Таблица для связи "многие-ко-многим" между ItemEntity и LabelEntity.
*/
@Entity(primaryKeys = ["itemId", "labelId"])
data class ItemLabelCrossRef(
val itemId: String,
val labelId: String
)
// [END_FILE_ItemLabelCrossRef.kt]

View File

@@ -0,0 +1,29 @@
// [PACKAGE] com.homebox.lens.data.db.entity
// [FILE] ItemWithLabels.kt
package com.homebox.lens.data.db.entity
import androidx.room.Embedded
import androidx.room.Junction
import androidx.room.Relation
// [CONTRACT]
/**
* [ENTITY: Pojo('ItemWithLabels')]
* [PURPOSE] POJO для получения ItemEntity вместе со связанными LabelEntity.
*/
data class ItemWithLabels(
@Embedded val item: ItemEntity,
@Relation(
parentColumn = "id",
entityColumn = "id",
associateBy = Junction(
value = ItemLabelCrossRef::class,
parentColumn = "itemId",
entityColumn = "labelId"
)
)
val labels: List<LabelEntity>
)
// [END_FILE_ItemWithLabels.kt]

View File

@@ -0,0 +1,20 @@
// [PACKAGE] com.homebox.lens.data.db.entity
// [FILE] LabelEntity.kt
package com.homebox.lens.data.db.entity
import androidx.room.Entity
import androidx.room.PrimaryKey
// [CONTRACT]
/**
* [ENTITY: RoomEntity('LabelEntity')]
* [PURPOSE] Представляет собой строку в таблице 'labels' в локальной БД.
*/
@Entity(tableName = "labels")
data class LabelEntity(
@PrimaryKey val id: String,
val name: String
)
// [END_FILE_LabelEntity.kt]

View File

@@ -0,0 +1,20 @@
// [PACKAGE] com.homebox.lens.data.db.entity
// [FILE] LocationEntity.kt
package com.homebox.lens.data.db.entity
import androidx.room.Entity
import androidx.room.PrimaryKey
// [CONTRACT]
/**
* [ENTITY: RoomEntity('LocationEntity')]
* [PURPOSE] Представляет собой строку в таблице 'locations' в локальной БД.
*/
@Entity(tableName = "locations")
data class LocationEntity(
@PrimaryKey val id: String,
val name: String
)
// [END_FILE_LocationEntity.kt]

View File

@@ -0,0 +1,77 @@
// [PACKAGE] com.homebox.lens.data.di
// [FILE] ApiModule.kt
package com.homebox.lens.data.di
import com.homebox.lens.data.api.HomeboxApiService
import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory
import javax.inject.Singleton
// [CONTRACT]
/**
* [MODULE: DaggerHilt('ApiModule')]
* [PURPOSE] Предоставляет зависимости для работы с сетью (Retrofit, OkHttp, Moshi).
*/
@Module
@InstallIn(SingletonComponent::class)
object ApiModule {
// [HELPER]
private const val BASE_URL = "https://api.homebox.app/"
// [PROVIDER]
@Provides
@Singleton
fun provideOkHttpClient(): OkHttpClient {
// [ACTION] Create logging interceptor
val logging = HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BODY
}
// [ACTION] Build OkHttpClient
return OkHttpClient.Builder()
.addInterceptor(logging)
// [TODO] Add AuthInterceptor for Bearer token
.build()
}
// [PROVIDER]
@Provides
@Singleton
fun provideMoshi(): Moshi {
// [ACTION] Build Moshi with Kotlin adapter
return Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.build()
}
// [PROVIDER]
@Provides
@Singleton
fun provideRetrofit(okHttpClient: OkHttpClient, moshi: Moshi): Retrofit {
// [ACTION] Build Retrofit instance
return Retrofit.Builder()
.baseUrl(BASE_URL)
.client(okHttpClient)
.addConverterFactory(MoshiConverterFactory.create(moshi))
.build()
}
// [PROVIDER]
@Provides
@Singleton
fun provideHomeboxApiService(retrofit: Retrofit): HomeboxApiService {
// [ACTION] Create ApiService from Retrofit instance
return retrofit.create(HomeboxApiService::class.java)
}
}
// [END_FILE_ApiModule.kt]

View File

@@ -0,0 +1,50 @@
// [PACKAGE] com.homebox.lens.data.di
// [FILE] DatabaseModule.kt
package com.homebox.lens.data.di
import android.content.Context
import androidx.room.Room
import com.homebox.lens.data.db.HomeboxDatabase
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
// [CONTRACT]
/**
* [MODULE: DaggerHilt('DatabaseModule')]
* [PURPOSE] Предоставляет зависимости для работы с базой данных Room.
*/
@Module
@InstallIn(SingletonComponent::class)
object DatabaseModule {
// [PROVIDER]
@Provides
@Singleton
fun provideHomeboxDatabase(@ApplicationContext context: Context): HomeboxDatabase {
// [ACTION] Build Room database instance
return Room.databaseBuilder(
context,
HomeboxDatabase::class.java,
HomeboxDatabase.DATABASE_NAME
).build()
}
// [PROVIDER]
@Provides
fun provideItemDao(database: HomeboxDatabase) = database.itemDao()
// [PROVIDER]
@Provides
fun provideLabelDao(database: HomeboxDatabase) = database.labelDao()
// [PROVIDER]
@Provides
fun provideLocationDao(database: HomeboxDatabase) = database.locationDao()
}
// [END_FILE_DatabaseModule.kt]

View File

@@ -0,0 +1,31 @@
// [PACKAGE] com.homebox.lens.data.di
// [FILE] RepositoryModule.kt
package com.homebox.lens.data.di
import com.homebox.lens.data.repository.ItemRepositoryImpl
import com.homebox.lens.domain.repository.ItemRepository
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
// [CONTRACT]
/**
* [MODULE: DaggerHilt('RepositoryModule')]
* [PURPOSE] Предоставляет реализацию для интерфейса ItemRepository.
*/
@Module
@InstallIn(SingletonComponent::class)
abstract class RepositoryModule {
// [PROVIDER]
@Binds
@Singleton
abstract fun bindItemRepository(
itemRepositoryImpl: ItemRepositoryImpl
): ItemRepository
}
// [END_FILE_RepositoryModule.kt]

View File

@@ -0,0 +1,105 @@
// [PACKAGE] com.homebox.lens.data.repository
// [FILE] ItemRepositoryImpl.kt
// [SEMANTICS] data_repository, implementation, network
// [IMPORTS]
import com.homebox.lens.data.api.HomeboxApiService
import com.homebox.lens.data.api.dto.toDomain
import com.homebox.lens.data.api.dto.toDto
import com.homebox.lens.domain.model.*
import com.homebox.lens.domain.repository.ItemRepository
import javax.inject.Inject
// [CORE-LOGIC]
/**
* [CONTRACT]
* Реализация репозитория для работы с данными о вещах.
* @param apiService Сервис для взаимодействия с Homebox API.
*/
class ItemRepositoryImpl @Inject constructor(
private val apiService: HomeboxApiService
) : ItemRepository {
/**
* [CONTRACT] @see ItemRepository.createItem
*/
override suspend fun createItem(newItemData: ItemCreate): ItemSummary {
// [ACTION]
val itemDto = newItemData.toDto()
val resultDto = apiService.createItem(itemDto)
return resultDto.toDomain()
}
/**
* [CONTRACT] @see ItemRepository.getItemDetails
*/
override suspend fun getItemDetails(itemId: String): ItemOut {
// [ACTION]
val resultDto = apiService.getItem(itemId)
return resultDto.toDomain()
}
/**
* [CONTRACT] @see ItemRepository.updateItem
*/
override suspend fun updateItem(itemId: String, item: ItemUpdate): ItemOut {
// [ACTION]
val itemDto = item.toDto()
val resultDto = apiService.updateItem(itemId, itemDto)
return resultDto.toDomain()
}
/**
* [CONTRACT] @see ItemRepository.deleteItem
*/
override suspend fun deleteItem(itemId: String) {
// [ACTION]
apiService.deleteItem(itemId)
}
/**
* [CONTRACT] @see ItemRepository.syncInventory
*/
override suspend fun syncInventory(page: Int, pageSize: Int): PaginationResult<ItemSummary> {
// [ACTION]
val resultDto = apiService.getItems(page = page, pageSize = pageSize)
return resultDto.toDomain { it.toDomain() }
}
/**
* [CONTRACT] @see ItemRepository.getStatistics
*/
override suspend fun getStatistics(): GroupStatistics {
// [ACTION]
val resultDto = apiService.getStatistics()
return resultDto.toDomain()
}
/**
* [CONTRACT] @see ItemRepository.getAllLocations
*/
override suspend fun getAllLocations(): List<LocationOutCount> {
// [ACTION]
val resultDto = apiService.getLocations()
return resultDto.map { it.toDomain() }
}
/**
* [CONTRACT] @see ItemRepository.getAllLabels
*/
override suspend fun getAllLabels(): List<LabelOut> {
// [ACTION]
val resultDto = apiService.getLabels()
return resultDto.map { it.toDomain() }
}
/**
* [CONTRACT] @see ItemRepository.searchItems
*/
override suspend fun searchItems(query: String): PaginationResult<ItemSummary> {
// [ACTION]
val resultDto = apiService.getItems(query = query)
return resultDto.toDomain { it.toDomain() }
}
}
// [END_FILE_ItemRepositoryImpl.kt]