feat(domain): add dashboard calculation use cases and tests
domain/model/: - CategorySummary: item count + total value per category - ExpiryWarning + ExpiryUrgency: expiry date warnings (urgent ≤6mo, warning ≤12mo) - MinStockWarning: items below minimum stock with deficit domain/usecase/: - CalculateTotalValueUseCase: sum of quantity × unitPrice - CalculateCategorySummaryUseCase: per-category item count and value - CalculateSupplyRangeUseCase: kcal-based supply range in days (weight units g/kg/mg only, defaults 2 persons × 2000 kcal/day) - GetExpiryWarningsUseCase: items expiring within 6/12 months - GetMinStockWarningsUseCase: items where quantity < minStock All use cases are pure functions with @Inject constructor() for Hilt. 39 unit tests covering all calculations including edge cases. Closes #29
This commit is contained in:
parent
10d19f0321
commit
4cc7a781d2
15 changed files with 851 additions and 0 deletions
|
|
@ -0,0 +1,8 @@
|
|||
package de.krisenvorrat.app.domain.model
|
||||
|
||||
internal data class CategorySummary(
|
||||
val categoryId: Int,
|
||||
val categoryName: String,
|
||||
val itemCount: Int,
|
||||
val totalValue: Double
|
||||
)
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package de.krisenvorrat.app.domain.model
|
||||
|
||||
import de.krisenvorrat.app.data.db.entity.ItemEntity
|
||||
|
||||
internal enum class ExpiryUrgency {
|
||||
URGENT,
|
||||
WARNING
|
||||
}
|
||||
|
||||
internal data class ExpiryWarning(
|
||||
val item: ItemEntity,
|
||||
val daysUntilExpiry: Long,
|
||||
val urgency: ExpiryUrgency
|
||||
)
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
package de.krisenvorrat.app.domain.model
|
||||
|
||||
import de.krisenvorrat.app.data.db.entity.ItemEntity
|
||||
|
||||
internal data class MinStockWarning(
|
||||
val item: ItemEntity,
|
||||
val deficit: Double
|
||||
)
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
package de.krisenvorrat.app.domain.usecase
|
||||
|
||||
import de.krisenvorrat.app.data.db.entity.CategoryEntity
|
||||
import de.krisenvorrat.app.data.db.entity.ItemEntity
|
||||
import de.krisenvorrat.app.domain.model.CategorySummary
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class CalculateCategorySummaryUseCase @Inject constructor() {
|
||||
|
||||
operator fun invoke(
|
||||
items: List<ItemEntity>,
|
||||
categories: List<CategoryEntity>
|
||||
): List<CategorySummary> {
|
||||
val categoryMap = categories.associate { it.id to it.name }
|
||||
return items
|
||||
.groupBy { it.categoryId }
|
||||
.map { (categoryId, categoryItems) ->
|
||||
CategorySummary(
|
||||
categoryId = categoryId,
|
||||
categoryName = categoryMap[categoryId] ?: "Unbekannt",
|
||||
itemCount = categoryItems.size,
|
||||
totalValue = categoryItems.sumOf { it.quantity * it.unitPrice }
|
||||
)
|
||||
}
|
||||
.sortedBy { it.categoryName }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
package de.krisenvorrat.app.domain.usecase
|
||||
|
||||
import de.krisenvorrat.app.data.db.entity.ItemEntity
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class CalculateSupplyRangeUseCase @Inject constructor() {
|
||||
|
||||
companion object {
|
||||
const val DEFAULT_HOUSEHOLD_SIZE = 2
|
||||
const val DEFAULT_DAILY_KCAL_PER_PERSON = 2000
|
||||
}
|
||||
|
||||
operator fun invoke(
|
||||
items: List<ItemEntity>,
|
||||
householdSize: Int = DEFAULT_HOUSEHOLD_SIZE,
|
||||
dailyKcalPerPerson: Int = DEFAULT_DAILY_KCAL_PER_PERSON
|
||||
): Double {
|
||||
val dailyNeed = householdSize * dailyKcalPerPerson
|
||||
if (dailyNeed <= 0) return 0.0
|
||||
|
||||
val totalKcal = items.sumOf { item ->
|
||||
val kcalPer100g = item.kcalPer100g ?: return@sumOf 0.0
|
||||
val grams = convertToGrams(item.quantity, item.unit) ?: return@sumOf 0.0
|
||||
(grams / 100.0) * kcalPer100g
|
||||
}
|
||||
|
||||
return totalKcal / dailyNeed
|
||||
}
|
||||
|
||||
private fun convertToGrams(quantity: Double, unit: String): Double? {
|
||||
return when (unit.lowercase().trim()) {
|
||||
"g" -> quantity
|
||||
"kg" -> quantity * 1000.0
|
||||
"mg" -> quantity / 1000.0
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package de.krisenvorrat.app.domain.usecase
|
||||
|
||||
import de.krisenvorrat.app.data.db.entity.ItemEntity
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class CalculateTotalValueUseCase @Inject constructor() {
|
||||
|
||||
operator fun invoke(items: List<ItemEntity>): Double {
|
||||
return items.sumOf { it.quantity * it.unitPrice }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
package de.krisenvorrat.app.domain.usecase
|
||||
|
||||
import de.krisenvorrat.app.data.db.entity.ItemEntity
|
||||
import de.krisenvorrat.app.domain.model.ExpiryUrgency
|
||||
import de.krisenvorrat.app.domain.model.ExpiryWarning
|
||||
import java.time.LocalDate
|
||||
import java.time.temporal.ChronoUnit
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class GetExpiryWarningsUseCase @Inject constructor() {
|
||||
|
||||
companion object {
|
||||
const val URGENT_MONTHS = 6L
|
||||
const val WARNING_MONTHS = 12L
|
||||
}
|
||||
|
||||
operator fun invoke(
|
||||
items: List<ItemEntity>,
|
||||
referenceDate: LocalDate = LocalDate.now()
|
||||
): List<ExpiryWarning> {
|
||||
val urgentCutoff = referenceDate.plusMonths(URGENT_MONTHS)
|
||||
val warningCutoff = referenceDate.plusMonths(WARNING_MONTHS)
|
||||
|
||||
return items
|
||||
.mapNotNull { item ->
|
||||
val expiryDate = item.expiryDate ?: return@mapNotNull null
|
||||
if (expiryDate.isAfter(warningCutoff)) return@mapNotNull null
|
||||
|
||||
val daysUntil = ChronoUnit.DAYS.between(referenceDate, expiryDate)
|
||||
val urgency = if (!expiryDate.isAfter(urgentCutoff)) {
|
||||
ExpiryUrgency.URGENT
|
||||
} else {
|
||||
ExpiryUrgency.WARNING
|
||||
}
|
||||
ExpiryWarning(
|
||||
item = item,
|
||||
daysUntilExpiry = daysUntil,
|
||||
urgency = urgency
|
||||
)
|
||||
}
|
||||
.sortedBy { it.daysUntilExpiry }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package de.krisenvorrat.app.domain.usecase
|
||||
|
||||
import de.krisenvorrat.app.data.db.entity.ItemEntity
|
||||
import de.krisenvorrat.app.domain.model.MinStockWarning
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class GetMinStockWarningsUseCase @Inject constructor() {
|
||||
|
||||
operator fun invoke(items: List<ItemEntity>): List<MinStockWarning> {
|
||||
return items
|
||||
.filter { it.quantity < it.minStock }
|
||||
.map { item ->
|
||||
MinStockWarning(
|
||||
item = item,
|
||||
deficit = item.minStock - item.quantity
|
||||
)
|
||||
}
|
||||
.sortedByDescending { it.deficit }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,107 @@
|
|||
package de.krisenvorrat.app.domain.usecase
|
||||
|
||||
import de.krisenvorrat.app.data.db.entity.CategoryEntity
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
class CalculateCategorySummaryUseCaseTest {
|
||||
|
||||
private val useCase = CalculateCategorySummaryUseCase()
|
||||
|
||||
@Test
|
||||
fun test_invoke_withItemsInMultipleCategories_returnsCorrectSummaries() {
|
||||
// Given
|
||||
val categories = listOf(
|
||||
CategoryEntity(id = 1, name = "Konserven"),
|
||||
CategoryEntity(id = 2, name = "Getränke")
|
||||
)
|
||||
val items = listOf(
|
||||
buildTestItem(id = "1", categoryId = 1, quantity = 2.0, unitPrice = 3.00),
|
||||
buildTestItem(id = "2", categoryId = 1, quantity = 1.0, unitPrice = 5.00),
|
||||
buildTestItem(id = "3", categoryId = 2, quantity = 6.0, unitPrice = 0.50)
|
||||
)
|
||||
|
||||
// When
|
||||
val result = useCase(items, categories)
|
||||
|
||||
// Then
|
||||
assertEquals(2, result.size)
|
||||
val getränke = result.first { it.categoryId == 2 }
|
||||
assertEquals("Getränke", getränke.categoryName)
|
||||
assertEquals(1, getränke.itemCount)
|
||||
assertEquals(3.00, getränke.totalValue, 0.001)
|
||||
|
||||
val konserven = result.first { it.categoryId == 1 }
|
||||
assertEquals("Konserven", konserven.categoryName)
|
||||
assertEquals(2, konserven.itemCount)
|
||||
assertEquals(11.00, konserven.totalValue, 0.001)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_invoke_withEmptyItemList_returnsEmptyList() {
|
||||
// Given
|
||||
val categories = listOf(CategoryEntity(id = 1, name = "Konserven"))
|
||||
|
||||
// When
|
||||
val result = useCase(emptyList(), categories)
|
||||
|
||||
// Then
|
||||
assertTrue(result.isEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_invoke_withUnknownCategory_usesUnbekanntAsName() {
|
||||
// Given
|
||||
val categories = emptyList<CategoryEntity>()
|
||||
val items = listOf(
|
||||
buildTestItem(id = "1", categoryId = 99, quantity = 1.0, unitPrice = 2.00)
|
||||
)
|
||||
|
||||
// When
|
||||
val result = useCase(items, categories)
|
||||
|
||||
// Then
|
||||
assertEquals(1, result.size)
|
||||
assertEquals("Unbekannt", result[0].categoryName)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_invoke_withSingleCategory_returnsOneSummary() {
|
||||
// Given
|
||||
val categories = listOf(CategoryEntity(id = 1, name = "Reis"))
|
||||
val items = listOf(
|
||||
buildTestItem(id = "1", categoryId = 1, quantity = 2.0, unitPrice = 1.50),
|
||||
buildTestItem(id = "2", categoryId = 1, quantity = 3.0, unitPrice = 1.50)
|
||||
)
|
||||
|
||||
// When
|
||||
val result = useCase(items, categories)
|
||||
|
||||
// Then
|
||||
assertEquals(1, result.size)
|
||||
assertEquals(2, result[0].itemCount)
|
||||
assertEquals(7.50, result[0].totalValue, 0.001)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_invoke_resultIsSortedByCategoryName() {
|
||||
// Given
|
||||
val categories = listOf(
|
||||
CategoryEntity(id = 1, name = "Zucker"),
|
||||
CategoryEntity(id = 2, name = "Bohnen"),
|
||||
CategoryEntity(id = 3, name = "Mehl")
|
||||
)
|
||||
val items = listOf(
|
||||
buildTestItem(id = "1", categoryId = 1, quantity = 1.0, unitPrice = 1.0),
|
||||
buildTestItem(id = "2", categoryId = 2, quantity = 1.0, unitPrice = 1.0),
|
||||
buildTestItem(id = "3", categoryId = 3, quantity = 1.0, unitPrice = 1.0)
|
||||
)
|
||||
|
||||
// When
|
||||
val result = useCase(items, categories)
|
||||
|
||||
// Then
|
||||
assertEquals(listOf("Bohnen", "Mehl", "Zucker"), result.map { it.categoryName })
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,154 @@
|
|||
package de.krisenvorrat.app.domain.usecase
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
|
||||
class CalculateSupplyRangeUseCaseTest {
|
||||
|
||||
private val useCase = CalculateSupplyRangeUseCase()
|
||||
|
||||
@Test
|
||||
fun test_invoke_withKgItems_returnsCorrectDays() {
|
||||
// Given – 2 kg Reis à 350 kcal/100g = 7000 kcal
|
||||
// 2 Personen × 2000 kcal/Tag = 4000 kcal/Tag → 1.75 Tage
|
||||
val items = listOf(
|
||||
buildTestItem(id = "1", quantity = 2.0, unit = "kg", kcalPer100g = 350)
|
||||
)
|
||||
|
||||
// When
|
||||
val result = useCase(items, householdSize = 2, dailyKcalPerPerson = 2000)
|
||||
|
||||
// Then
|
||||
assertEquals(1.75, result, 0.001)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_invoke_withGramItems_returnsCorrectDays() {
|
||||
// Given – 500 g Nudeln à 360 kcal/100g = 1800 kcal
|
||||
// 1 Person × 2000 kcal/Tag → 0.9 Tage
|
||||
val items = listOf(
|
||||
buildTestItem(id = "1", quantity = 500.0, unit = "g", kcalPer100g = 360)
|
||||
)
|
||||
|
||||
// When
|
||||
val result = useCase(items, householdSize = 1, dailyKcalPerPerson = 2000)
|
||||
|
||||
// Then
|
||||
assertEquals(0.9, result, 0.001)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_invoke_withMultipleItems_sumsTotalKcal() {
|
||||
// Given – 1 kg Reis (350 kcal/100g = 3500 kcal) + 500 g Nudeln (360 kcal/100g = 1800 kcal) = 5300 kcal
|
||||
// 2 Personen × 2000 kcal/Tag = 4000 kcal/Tag → 1.325 Tage
|
||||
val items = listOf(
|
||||
buildTestItem(id = "1", quantity = 1.0, unit = "kg", kcalPer100g = 350),
|
||||
buildTestItem(id = "2", quantity = 500.0, unit = "g", kcalPer100g = 360)
|
||||
)
|
||||
|
||||
// When
|
||||
val result = useCase(items, householdSize = 2, dailyKcalPerPerson = 2000)
|
||||
|
||||
// Then
|
||||
assertEquals(1.325, result, 0.001)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_invoke_withEmptyList_returnsZero() {
|
||||
// Given / When
|
||||
val result = useCase(emptyList(), householdSize = 2, dailyKcalPerPerson = 2000)
|
||||
|
||||
// Then
|
||||
assertEquals(0.0, result, 0.001)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_invoke_withNullKcal_skipsItem() {
|
||||
// Given – Item ohne kcalPer100g wird ignoriert
|
||||
val items = listOf(
|
||||
buildTestItem(id = "1", quantity = 1.0, unit = "kg", kcalPer100g = null),
|
||||
buildTestItem(id = "2", quantity = 1.0, unit = "kg", kcalPer100g = 200)
|
||||
)
|
||||
|
||||
// When – Nur 1 kg à 200 kcal/100g = 2000 kcal, 1 Person × 2000 = 1.0 Tage
|
||||
val result = useCase(items, householdSize = 1, dailyKcalPerPerson = 2000)
|
||||
|
||||
// Then
|
||||
assertEquals(1.0, result, 0.001)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_invoke_withNonWeightUnit_skipsItem() {
|
||||
// Given – "Stk" ist keine Gewichtseinheit → wird ignoriert
|
||||
val items = listOf(
|
||||
buildTestItem(id = "1", quantity = 5.0, unit = "Stk", kcalPer100g = 200)
|
||||
)
|
||||
|
||||
// When
|
||||
val result = useCase(items, householdSize = 2, dailyKcalPerPerson = 2000)
|
||||
|
||||
// Then
|
||||
assertEquals(0.0, result, 0.001)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_invoke_withZeroHouseholdSize_returnsZero() {
|
||||
// Given
|
||||
val items = listOf(
|
||||
buildTestItem(id = "1", quantity = 1.0, unit = "kg", kcalPer100g = 350)
|
||||
)
|
||||
|
||||
// When
|
||||
val result = useCase(items, householdSize = 0, dailyKcalPerPerson = 2000)
|
||||
|
||||
// Then
|
||||
assertEquals(0.0, result, 0.001)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_invoke_withDefaultParameters_uses2PersonsAnd2000Kcal() {
|
||||
// Given – 4 kg Reis à 350 kcal/100g = 14000 kcal
|
||||
// Default: 2 Personen × 2000 kcal = 4000 kcal/Tag → 3.5 Tage
|
||||
val items = listOf(
|
||||
buildTestItem(id = "1", quantity = 4.0, unit = "kg", kcalPer100g = 350)
|
||||
)
|
||||
|
||||
// When
|
||||
val result = useCase(items)
|
||||
|
||||
// Then
|
||||
assertEquals(3.5, result, 0.001)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_invoke_withMgUnit_convertsCorrectly() {
|
||||
// Given – 500000 mg = 500 g à 200 kcal/100g = 1000 kcal
|
||||
// 1 Person × 1000 kcal/Tag → 1.0 Tag
|
||||
val items = listOf(
|
||||
buildTestItem(id = "1", quantity = 500000.0, unit = "mg", kcalPer100g = 200)
|
||||
)
|
||||
|
||||
// When
|
||||
val result = useCase(items, householdSize = 1, dailyKcalPerPerson = 1000)
|
||||
|
||||
// Then
|
||||
assertEquals(1.0, result, 0.001)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_invoke_withMixedUnits_onlyCountsWeightBased() {
|
||||
// Given – 1 kg (350 kcal/100g) + 5 Stk (ignored) + 2 L (ignored)
|
||||
// Total: 3500 kcal, 1 Person × 2000 → 1.75 Tage
|
||||
val items = listOf(
|
||||
buildTestItem(id = "1", quantity = 1.0, unit = "kg", kcalPer100g = 350),
|
||||
buildTestItem(id = "2", quantity = 5.0, unit = "Stk", kcalPer100g = 100),
|
||||
buildTestItem(id = "3", quantity = 2.0, unit = "L", kcalPer100g = 45)
|
||||
)
|
||||
|
||||
// When
|
||||
val result = useCase(items, householdSize = 1, dailyKcalPerPerson = 2000)
|
||||
|
||||
// Then
|
||||
assertEquals(1.75, result, 0.001)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
package de.krisenvorrat.app.domain.usecase
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
|
||||
class CalculateTotalValueUseCaseTest {
|
||||
|
||||
private val useCase = CalculateTotalValueUseCase()
|
||||
|
||||
@Test
|
||||
fun test_invoke_withMultipleItems_returnsSumOfQuantityTimesPrice() {
|
||||
// Given
|
||||
val items = listOf(
|
||||
buildTestItem(id = "1", quantity = 3.0, unitPrice = 2.50),
|
||||
buildTestItem(id = "2", quantity = 1.0, unitPrice = 4.00),
|
||||
buildTestItem(id = "3", quantity = 5.0, unitPrice = 1.20)
|
||||
)
|
||||
|
||||
// When
|
||||
val result = useCase(items)
|
||||
|
||||
// Then
|
||||
assertEquals(17.50, result, 0.001)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_invoke_withEmptyList_returnsZero() {
|
||||
// Given
|
||||
val items = emptyList<Nothing>()
|
||||
|
||||
// When
|
||||
val result = useCase(items)
|
||||
|
||||
// Then
|
||||
assertEquals(0.0, result, 0.001)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_invoke_withZeroPrice_returnsZero() {
|
||||
// Given
|
||||
val items = listOf(
|
||||
buildTestItem(id = "1", quantity = 10.0, unitPrice = 0.0)
|
||||
)
|
||||
|
||||
// When
|
||||
val result = useCase(items)
|
||||
|
||||
// Then
|
||||
assertEquals(0.0, result, 0.001)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_invoke_withZeroQuantity_returnsZero() {
|
||||
// Given
|
||||
val items = listOf(
|
||||
buildTestItem(id = "1", quantity = 0.0, unitPrice = 5.0)
|
||||
)
|
||||
|
||||
// When
|
||||
val result = useCase(items)
|
||||
|
||||
// Then
|
||||
assertEquals(0.0, result, 0.001)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_invoke_withSingleItem_returnsCorrectValue() {
|
||||
// Given
|
||||
val items = listOf(
|
||||
buildTestItem(id = "1", quantity = 2.5, unitPrice = 3.00)
|
||||
)
|
||||
|
||||
// When
|
||||
val result = useCase(items)
|
||||
|
||||
// Then
|
||||
assertEquals(7.50, result, 0.001)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,184 @@
|
|||
package de.krisenvorrat.app.domain.usecase
|
||||
|
||||
import de.krisenvorrat.app.domain.model.ExpiryUrgency
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
import java.time.LocalDate
|
||||
|
||||
class GetExpiryWarningsUseCaseTest {
|
||||
|
||||
private val useCase = GetExpiryWarningsUseCase()
|
||||
private val today = LocalDate.of(2026, 5, 14)
|
||||
|
||||
@Test
|
||||
fun test_invoke_withItemExpiringIn3Months_returnsUrgentWarning() {
|
||||
// Given
|
||||
val items = listOf(
|
||||
buildTestItem(id = "1", expiryDate = today.plusMonths(3))
|
||||
)
|
||||
|
||||
// When
|
||||
val result = useCase(items, referenceDate = today)
|
||||
|
||||
// Then
|
||||
assertEquals(1, result.size)
|
||||
assertEquals(ExpiryUrgency.URGENT, result[0].urgency)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_invoke_withItemExpiringIn9Months_returnsWarning() {
|
||||
// Given
|
||||
val items = listOf(
|
||||
buildTestItem(id = "1", expiryDate = today.plusMonths(9))
|
||||
)
|
||||
|
||||
// When
|
||||
val result = useCase(items, referenceDate = today)
|
||||
|
||||
// Then
|
||||
assertEquals(1, result.size)
|
||||
assertEquals(ExpiryUrgency.WARNING, result[0].urgency)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_invoke_withItemExpiringIn18Months_returnsNoWarning() {
|
||||
// Given
|
||||
val items = listOf(
|
||||
buildTestItem(id = "1", expiryDate = today.plusMonths(18))
|
||||
)
|
||||
|
||||
// When
|
||||
val result = useCase(items, referenceDate = today)
|
||||
|
||||
// Then
|
||||
assertTrue(result.isEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_invoke_withAlreadyExpiredItem_returnsUrgentWithNegativeDays() {
|
||||
// Given
|
||||
val items = listOf(
|
||||
buildTestItem(id = "1", expiryDate = today.minusDays(10))
|
||||
)
|
||||
|
||||
// When
|
||||
val result = useCase(items, referenceDate = today)
|
||||
|
||||
// Then
|
||||
assertEquals(1, result.size)
|
||||
assertEquals(ExpiryUrgency.URGENT, result[0].urgency)
|
||||
assertEquals(-10L, result[0].daysUntilExpiry)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_invoke_withNoExpiryDate_skipsItem() {
|
||||
// Given
|
||||
val items = listOf(
|
||||
buildTestItem(id = "1", expiryDate = null)
|
||||
)
|
||||
|
||||
// When
|
||||
val result = useCase(items, referenceDate = today)
|
||||
|
||||
// Then
|
||||
assertTrue(result.isEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_invoke_withEmptyList_returnsEmptyList() {
|
||||
// Given / When
|
||||
val result = useCase(emptyList(), referenceDate = today)
|
||||
|
||||
// Then
|
||||
assertTrue(result.isEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_invoke_withMixedItems_groupsByUrgency() {
|
||||
// Given
|
||||
val items = listOf(
|
||||
buildTestItem(id = "1", name = "Bald", expiryDate = today.plusMonths(2)),
|
||||
buildTestItem(id = "2", name = "Mittel", expiryDate = today.plusMonths(8)),
|
||||
buildTestItem(id = "3", name = "Weit", expiryDate = today.plusMonths(18)),
|
||||
buildTestItem(id = "4", name = "Ohne", expiryDate = null)
|
||||
)
|
||||
|
||||
// When
|
||||
val result = useCase(items, referenceDate = today)
|
||||
|
||||
// Then
|
||||
assertEquals(2, result.size)
|
||||
val urgent = result.filter { it.urgency == ExpiryUrgency.URGENT }
|
||||
val warning = result.filter { it.urgency == ExpiryUrgency.WARNING }
|
||||
assertEquals(1, urgent.size)
|
||||
assertEquals("Bald", urgent[0].item.name)
|
||||
assertEquals(1, warning.size)
|
||||
assertEquals("Mittel", warning[0].item.name)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_invoke_resultIsSortedByDaysUntilExpiry() {
|
||||
// Given
|
||||
val items = listOf(
|
||||
buildTestItem(id = "1", expiryDate = today.plusMonths(10)),
|
||||
buildTestItem(id = "2", expiryDate = today.plusDays(5)),
|
||||
buildTestItem(id = "3", expiryDate = today.plusMonths(4))
|
||||
)
|
||||
|
||||
// When
|
||||
val result = useCase(items, referenceDate = today)
|
||||
|
||||
// Then
|
||||
assertEquals(3, result.size)
|
||||
assertEquals("2", result[0].item.id)
|
||||
assertEquals("3", result[1].item.id)
|
||||
assertEquals("1", result[2].item.id)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_invoke_withItemExpiringExactlyAt6Months_isUrgent() {
|
||||
// Given
|
||||
val items = listOf(
|
||||
buildTestItem(id = "1", expiryDate = today.plusMonths(6))
|
||||
)
|
||||
|
||||
// When
|
||||
val result = useCase(items, referenceDate = today)
|
||||
|
||||
// Then
|
||||
assertEquals(1, result.size)
|
||||
assertEquals(ExpiryUrgency.URGENT, result[0].urgency)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_invoke_withItemExpiringExactlyAt12Months_isWarning() {
|
||||
// Given
|
||||
val items = listOf(
|
||||
buildTestItem(id = "1", expiryDate = today.plusMonths(12))
|
||||
)
|
||||
|
||||
// When
|
||||
val result = useCase(items, referenceDate = today)
|
||||
|
||||
// Then
|
||||
assertEquals(1, result.size)
|
||||
assertEquals(ExpiryUrgency.WARNING, result[0].urgency)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_invoke_withItemExpiringToday_isUrgentWithZeroDays() {
|
||||
// Given
|
||||
val items = listOf(
|
||||
buildTestItem(id = "1", expiryDate = today)
|
||||
)
|
||||
|
||||
// When
|
||||
val result = useCase(items, referenceDate = today)
|
||||
|
||||
// Then
|
||||
assertEquals(1, result.size)
|
||||
assertEquals(ExpiryUrgency.URGENT, result[0].urgency)
|
||||
assertEquals(0L, result[0].daysUntilExpiry)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,128 @@
|
|||
package de.krisenvorrat.app.domain.usecase
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
class GetMinStockWarningsUseCaseTest {
|
||||
|
||||
private val useCase = GetMinStockWarningsUseCase()
|
||||
|
||||
@Test
|
||||
fun test_invoke_withItemBelowMinStock_returnsWarning() {
|
||||
// Given
|
||||
val items = listOf(
|
||||
buildTestItem(id = "1", quantity = 2.0, minStock = 5.0)
|
||||
)
|
||||
|
||||
// When
|
||||
val result = useCase(items)
|
||||
|
||||
// Then
|
||||
assertEquals(1, result.size)
|
||||
assertEquals(3.0, result[0].deficit, 0.001)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_invoke_withItemAtMinStock_returnsNoWarning() {
|
||||
// Given
|
||||
val items = listOf(
|
||||
buildTestItem(id = "1", quantity = 5.0, minStock = 5.0)
|
||||
)
|
||||
|
||||
// When
|
||||
val result = useCase(items)
|
||||
|
||||
// Then
|
||||
assertTrue(result.isEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_invoke_withItemAboveMinStock_returnsNoWarning() {
|
||||
// Given
|
||||
val items = listOf(
|
||||
buildTestItem(id = "1", quantity = 10.0, minStock = 5.0)
|
||||
)
|
||||
|
||||
// When
|
||||
val result = useCase(items)
|
||||
|
||||
// Then
|
||||
assertTrue(result.isEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_invoke_withEmptyList_returnsEmptyList() {
|
||||
// Given / When
|
||||
val result = useCase(emptyList())
|
||||
|
||||
// Then
|
||||
assertTrue(result.isEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_invoke_withZeroMinStock_returnsNoWarning() {
|
||||
// Given
|
||||
val items = listOf(
|
||||
buildTestItem(id = "1", quantity = 0.0, minStock = 0.0)
|
||||
)
|
||||
|
||||
// When
|
||||
val result = useCase(items)
|
||||
|
||||
// Then
|
||||
assertTrue(result.isEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_invoke_withZeroQuantity_returnsWarning() {
|
||||
// Given
|
||||
val items = listOf(
|
||||
buildTestItem(id = "1", quantity = 0.0, minStock = 3.0)
|
||||
)
|
||||
|
||||
// When
|
||||
val result = useCase(items)
|
||||
|
||||
// Then
|
||||
assertEquals(1, result.size)
|
||||
assertEquals(3.0, result[0].deficit, 0.001)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_invoke_resultIsSortedByDeficitDescending() {
|
||||
// Given
|
||||
val items = listOf(
|
||||
buildTestItem(id = "1", quantity = 3.0, minStock = 5.0), // deficit 2
|
||||
buildTestItem(id = "2", quantity = 1.0, minStock = 10.0), // deficit 9
|
||||
buildTestItem(id = "3", quantity = 4.0, minStock = 7.0) // deficit 3
|
||||
)
|
||||
|
||||
// When
|
||||
val result = useCase(items)
|
||||
|
||||
// Then
|
||||
assertEquals(3, result.size)
|
||||
assertEquals("2", result[0].item.id) // deficit 9
|
||||
assertEquals("3", result[1].item.id) // deficit 3
|
||||
assertEquals("1", result[2].item.id) // deficit 2
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_invoke_withMixedItems_onlyReturnsBelowMinStock() {
|
||||
// Given
|
||||
val items = listOf(
|
||||
buildTestItem(id = "1", quantity = 2.0, minStock = 5.0), // below
|
||||
buildTestItem(id = "2", quantity = 10.0, minStock = 3.0), // above
|
||||
buildTestItem(id = "3", quantity = 5.0, minStock = 5.0), // equal
|
||||
buildTestItem(id = "4", quantity = 0.0, minStock = 1.0) // below
|
||||
)
|
||||
|
||||
// When
|
||||
val result = useCase(items)
|
||||
|
||||
// Then
|
||||
assertEquals(2, result.size)
|
||||
assertTrue(result.all { it.item.id in listOf("1", "4") })
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
package de.krisenvorrat.app.domain.usecase
|
||||
|
||||
import de.krisenvorrat.app.data.db.entity.ItemEntity
|
||||
import java.time.LocalDate
|
||||
|
||||
internal fun buildTestItem(
|
||||
id: String = "id1",
|
||||
name: String = "Konserve",
|
||||
categoryId: Int = 1,
|
||||
quantity: Double = 1.0,
|
||||
unit: String = "Stk",
|
||||
unitPrice: Double = 0.0,
|
||||
kcalPer100g: Int? = null,
|
||||
expiryDate: LocalDate? = null,
|
||||
locationId: Int = 1,
|
||||
minStock: Double = 0.0
|
||||
) = ItemEntity(
|
||||
id = id,
|
||||
name = name,
|
||||
categoryId = categoryId,
|
||||
quantity = quantity,
|
||||
unit = unit,
|
||||
unitPrice = unitPrice,
|
||||
kcalPer100g = kcalPer100g,
|
||||
expiryDate = expiryDate,
|
||||
locationId = locationId,
|
||||
minStock = minStock,
|
||||
notes = "",
|
||||
lastUpdated = 0L
|
||||
)
|
||||
Loading…
Reference in a new issue