diff --git a/app/src/main/java/de/krisenvorrat/app/domain/.gitkeep b/app/src/main/java/de/krisenvorrat/app/domain/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/app/src/main/java/de/krisenvorrat/app/domain/model/CategorySummary.kt b/app/src/main/java/de/krisenvorrat/app/domain/model/CategorySummary.kt new file mode 100644 index 0000000..b6935a4 --- /dev/null +++ b/app/src/main/java/de/krisenvorrat/app/domain/model/CategorySummary.kt @@ -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 +) diff --git a/app/src/main/java/de/krisenvorrat/app/domain/model/ExpiryWarning.kt b/app/src/main/java/de/krisenvorrat/app/domain/model/ExpiryWarning.kt new file mode 100644 index 0000000..e34482c --- /dev/null +++ b/app/src/main/java/de/krisenvorrat/app/domain/model/ExpiryWarning.kt @@ -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 +) diff --git a/app/src/main/java/de/krisenvorrat/app/domain/model/MinStockWarning.kt b/app/src/main/java/de/krisenvorrat/app/domain/model/MinStockWarning.kt new file mode 100644 index 0000000..b71f1f0 --- /dev/null +++ b/app/src/main/java/de/krisenvorrat/app/domain/model/MinStockWarning.kt @@ -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 +) diff --git a/app/src/main/java/de/krisenvorrat/app/domain/usecase/CalculateCategorySummaryUseCase.kt b/app/src/main/java/de/krisenvorrat/app/domain/usecase/CalculateCategorySummaryUseCase.kt new file mode 100644 index 0000000..8fb119a --- /dev/null +++ b/app/src/main/java/de/krisenvorrat/app/domain/usecase/CalculateCategorySummaryUseCase.kt @@ -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, + categories: List + ): List { + 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 } + } +} diff --git a/app/src/main/java/de/krisenvorrat/app/domain/usecase/CalculateSupplyRangeUseCase.kt b/app/src/main/java/de/krisenvorrat/app/domain/usecase/CalculateSupplyRangeUseCase.kt new file mode 100644 index 0000000..53e2694 --- /dev/null +++ b/app/src/main/java/de/krisenvorrat/app/domain/usecase/CalculateSupplyRangeUseCase.kt @@ -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, + 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 + } + } +} diff --git a/app/src/main/java/de/krisenvorrat/app/domain/usecase/CalculateTotalValueUseCase.kt b/app/src/main/java/de/krisenvorrat/app/domain/usecase/CalculateTotalValueUseCase.kt new file mode 100644 index 0000000..83a8324 --- /dev/null +++ b/app/src/main/java/de/krisenvorrat/app/domain/usecase/CalculateTotalValueUseCase.kt @@ -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): Double { + return items.sumOf { it.quantity * it.unitPrice } + } +} diff --git a/app/src/main/java/de/krisenvorrat/app/domain/usecase/GetExpiryWarningsUseCase.kt b/app/src/main/java/de/krisenvorrat/app/domain/usecase/GetExpiryWarningsUseCase.kt new file mode 100644 index 0000000..96a0466 --- /dev/null +++ b/app/src/main/java/de/krisenvorrat/app/domain/usecase/GetExpiryWarningsUseCase.kt @@ -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, + referenceDate: LocalDate = LocalDate.now() + ): List { + 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 } + } +} diff --git a/app/src/main/java/de/krisenvorrat/app/domain/usecase/GetMinStockWarningsUseCase.kt b/app/src/main/java/de/krisenvorrat/app/domain/usecase/GetMinStockWarningsUseCase.kt new file mode 100644 index 0000000..fa86447 --- /dev/null +++ b/app/src/main/java/de/krisenvorrat/app/domain/usecase/GetMinStockWarningsUseCase.kt @@ -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): List { + return items + .filter { it.quantity < it.minStock } + .map { item -> + MinStockWarning( + item = item, + deficit = item.minStock - item.quantity + ) + } + .sortedByDescending { it.deficit } + } +} diff --git a/app/src/test/java/de/krisenvorrat/app/domain/usecase/CalculateCategorySummaryUseCaseTest.kt b/app/src/test/java/de/krisenvorrat/app/domain/usecase/CalculateCategorySummaryUseCaseTest.kt new file mode 100644 index 0000000..e4aa030 --- /dev/null +++ b/app/src/test/java/de/krisenvorrat/app/domain/usecase/CalculateCategorySummaryUseCaseTest.kt @@ -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() + 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 }) + } +} diff --git a/app/src/test/java/de/krisenvorrat/app/domain/usecase/CalculateSupplyRangeUseCaseTest.kt b/app/src/test/java/de/krisenvorrat/app/domain/usecase/CalculateSupplyRangeUseCaseTest.kt new file mode 100644 index 0000000..aa62ab3 --- /dev/null +++ b/app/src/test/java/de/krisenvorrat/app/domain/usecase/CalculateSupplyRangeUseCaseTest.kt @@ -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) + } +} diff --git a/app/src/test/java/de/krisenvorrat/app/domain/usecase/CalculateTotalValueUseCaseTest.kt b/app/src/test/java/de/krisenvorrat/app/domain/usecase/CalculateTotalValueUseCaseTest.kt new file mode 100644 index 0000000..1dadfe5 --- /dev/null +++ b/app/src/test/java/de/krisenvorrat/app/domain/usecase/CalculateTotalValueUseCaseTest.kt @@ -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() + + // 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) + } +} diff --git a/app/src/test/java/de/krisenvorrat/app/domain/usecase/GetExpiryWarningsUseCaseTest.kt b/app/src/test/java/de/krisenvorrat/app/domain/usecase/GetExpiryWarningsUseCaseTest.kt new file mode 100644 index 0000000..0a26dce --- /dev/null +++ b/app/src/test/java/de/krisenvorrat/app/domain/usecase/GetExpiryWarningsUseCaseTest.kt @@ -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) + } +} diff --git a/app/src/test/java/de/krisenvorrat/app/domain/usecase/GetMinStockWarningsUseCaseTest.kt b/app/src/test/java/de/krisenvorrat/app/domain/usecase/GetMinStockWarningsUseCaseTest.kt new file mode 100644 index 0000000..eead21c --- /dev/null +++ b/app/src/test/java/de/krisenvorrat/app/domain/usecase/GetMinStockWarningsUseCaseTest.kt @@ -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") }) + } +} diff --git a/app/src/test/java/de/krisenvorrat/app/domain/usecase/TestHelpers.kt b/app/src/test/java/de/krisenvorrat/app/domain/usecase/TestHelpers.kt new file mode 100644 index 0000000..0c9c849 --- /dev/null +++ b/app/src/test/java/de/krisenvorrat/app/domain/usecase/TestHelpers.kt @@ -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 +)