refactor: kcal/100g -> kcal/kg umbenennen und Mindestbestand entfernen

- ItemEntity, ItemDto: kcalPer100g -> kcalPerKg (kcal_per_kg),
  minStock-Spalte komplett entfernt
- CalculateSupplyRangeUseCase: Formel angepasst (/ 1000.0 * kcalPerKg)
- GetMinStockWarningsUseCase + MinStockWarning: gelöscht
- UI (ItemFormScreen, WarningsScreen, DashboardScreen): Mindestbestand-
  Felder und Warnungsabschnitte entfernt
- ViewModels, UiState, Repository: alle Referenzen bereinigt
- Server (Tables, InventoryRepository): Schema angepasst
- Room: fallbackToDestructiveMigration() hinzugefügt (keine Produktivdaten)
- Alle 434 Tests gruen
This commit is contained in:
Jens Reinemann 2026-05-16 14:19:10 +02:00
parent 395939a4ec
commit 8280a9daf9
37 changed files with 290 additions and 473 deletions

View file

@ -0,0 +1,125 @@
# Technology Candidates REST-Server für Geräte-Synchronisierung (Phase 2)
Date: 2026-05-14
Requirements file: requirements.md
---
## Candidate Table
| Name | Description | Use case | Age | Adoption | Last release | Update cadence | Req. coverage | Score |
| ----------------- | --------------------------------------------------- | -------------------------------------------- | ---------------- | --------------------------- | ------------- | -------------- | ------------------------ | ----- |
| Ktor | Kotlin-nativer async Web-Framework von JetBrains | Microservices, APIs, leichtgewichtige Server | ~8 Jahre (2018) | Moderate (JetBrains-backed) | 2025 (3.x) | Monatlich | Alle Must + alle Should | **9** |
| Spring Boot | Enterprise-Java/Kotlin-Framework, Industriestandard | Enterprise-Apps, REST-APIs, Microservices | ~12 Jahre (2014) | Widespread | 2025 (3.x) | Monatlich | Alle Must, teilw. Should | **6** |
| Node.js + Express | JavaScript-Runtime + minimalistisches Web-Framework | APIs, Prototyping, Full-Stack JS | ~15 Jahre (2010) | Widespread | 2025 (5.x) | Regelmäßig | Alle Must, wenig Should | **5** |
| Python + FastAPI | Modernes Python-Web-Framework mit Type Hints | Schnelle API-Entwicklung, ML-Backends | ~7 Jahre (2018) | Widespread | 2025 (0.115+) | Monatlich | Alle Must, wenig Should | **5** |
---
## Candidate Details
### 1. Ktor (JetBrains)
**Homepage:** https://ktor.io
**Repository:** https://github.com/ktorio/ktor
**Requirement coverage:**
- ✅ Must: REST-API (JSON), einfaches Deployment (Fat JAR / Docker), CRUD-Unterstützung, JSON via kotlinx.serialization nativ, aktiv gewartet (JetBrains), sehr geringer Ressourcenverbrauch
- ✅ Should: **Kotlin-nativ** gleiche Sprache wie Client, Datenmodelle direkt teilbar, kotlinx.serialization als First-Class-Citizen, Coroutines-basiert, kein Application-Server nötig (eingebetteter Netty/CIO), exzellente Dokumentation (ktor.io), einfache DB-Anbindung (Exposed ORM), einfaches Testing
- ✅ Nice-to-Have: WebSocket-Support eingebaut, Authentifizierungs-Plugins, Docker-ready (Fat JAR), OpenAPI-Plugin verfügbar, Hot-Reload via Auto-Reload-Feature, Type-safe Routing
**Stärken:**
- **Gleiche Sprache wie der Client** Datenmodelle (`@Serializable`-Klassen) können in einem Shared-Modul zwischen Server und Client geteilt werden
- Koroutinen-basiert konsistent mit dem Android-Client
- Sehr leichtgewichtig Start in < 1 Sekunde, wenig RAM
- JetBrains-maintained langfristige Pflege gesichert
- Modulares Plugin-System nur einbinden was man braucht
**Risiken / Schwächen:**
- Kleinere Community als Spring Boot weniger StackOverflow-Antworten
- Weniger Enterprise-Features out of the box (kein Spring-Ökosystem)
- Bei komplexen Anforderungen muss man mehr selbst bauen
---
### 2. Spring Boot
**Homepage:** https://spring.io/projects/spring-boot
**Repository:** https://github.com/spring-projects/spring-boot
**Requirement coverage:**
- ✅ Must: REST-API, Deployment, CRUD, JSON, aktiv gewartet, stabil
- ⚠️ Should: Kotlin-Support vorhanden aber nicht nativ (Java-First), kotlinx.serialization nicht Standard (Jackson wird bevorzugt), schwergewichtig (Tomcat eingebettet, hoher RAM-Verbrauch), Coroutines-Support nur via WebFlux
- ✅ Nice-to-Have: WebSocket, Spring Security, Docker, Swagger/OpenAPI, DevTools Hot-Reload
**Stärken:**
- Industriestandard mit riesiger Community und Ökosystem
- Extrem viele Tutorials, Bücher, StackOverflow-Antworten
- Battle-tested in Produktion bei Millionen von Projekten
- Spring Security für Authentication/Authorization
**Risiken / Schwächen:**
- **Overhead für ein kleines Projekt:** Hoher RAM-Verbrauch (200500 MB), langsame Startzeit, viel Boilerplate
- **Java-First:** Kotlin ist Second-Class-Citizen Spring-Idiome (Annotations, Bean-Wiring) fühlen sich in Kotlin weniger natürlich an
- **Keine geteilten Datenmodelle:** kotlinx.serialization wird nicht nativ unterstützt, Jackson-Annotationen nötig das Client-Datenmodell kann nicht 1:1 wiederverwendet werden
- Für 210 Nutzer massiv überdimensioniert
---
### 3. Node.js + Express
**Homepage:** https://expressjs.com
**Repository:** https://github.com/expressjs/express
**Requirement coverage:**
- ✅ Must: REST-API, einfaches Deployment, CRUD, JSON nativ, aktiv gewartet, geringer Verbrauch
- ❌ Should: **Andere Sprache (JavaScript/TypeScript)** keine Wiederverwendung von Kotlin-Datenmodellen, keine Coroutines, kein kotlinx.serialization, andere Toolchain (npm vs. Gradle)
- ⚠️ Nice-to-Have: WebSocket (via ws/socket.io), Passport.js für Auth, Docker einfach, OpenAPI-Generierung möglich
**Stärken:**
- Extrem leichtgewichtig und schnell aufzusetzen
- Riesige Community, npm-Ökosystem
- Sehr gute Performance für I/O-lastige Workloads
- Viele fertige Middleware-Pakete
**Risiken / Schwächen:**
- **Sprachwechsel:** Kotlin (Client) ↔ JavaScript (Server) kein Code-Sharing, doppelte Modell-Definition
- **Typsicherheit:** JavaScript bietet weniger Typsicherheit als Kotlin (TypeScript hilft, ist aber ein separates Ökosystem)
- **Dependency-Overhead:** npm-Ökosystem hat bekannte Supply-Chain-Risiken
- Separates Tooling und Build-System nötig
---
### 4. Python + FastAPI
**Homepage:** https://fastapi.tiangolo.com
**Repository:** https://github.com/tiangolo/fastapi
**Requirement coverage:**
- ✅ Must: REST-API, einfaches Deployment, CRUD, JSON, aktiv gewartet, geringer Verbrauch
- ❌ Should: **Andere Sprache (Python)** keine Wiederverwendung von Kotlin-Datenmodellen, kein kotlinx.serialization, andere Toolchain (pip vs. Gradle), kein Coroutines-Bezug
- ✅ Nice-to-Have: WebSocket-Support, OAuth2-Support, Docker einfach, **automatische OpenAPI/Swagger-Generierung** (Highlight!), Hot-Reload via uvicorn
**Stärken:**
- Extrem schnelle Entwicklung dank Auto-Dokumentation und Type Hints
- Automatische OpenAPI-Doku out of the box
- Async I/O via asyncio
- Gut für Prototyping
**Risiken / Schwächen:**
- **Sprachwechsel:** Kotlin ↔ Python komplett andere Toolchain, kein Code-Sharing
- **GIL:** Python Global Interpreter Lock kann bei CPU-intensiven Operationen limitieren (hier aber irrelevant)
- **Deployment:** Python-Umgebungen können fragiler sein als JVM-Fat-JARs (venv, pip-Abhängigkeiten)
- Weniger natürliche Integration mit dem Kotlin/Gradle-basierten Projekt

View file

@ -0,0 +1,60 @@
# Technology Requirements REST-Server für Geräte-Synchronisierung (Phase 2)
Date: 2026-05-14
Issue: #10
Author: Tech-Decision Workflow
---
## Kontext
Die Krisenvorrat-App (Android/Kotlin) speichert in v1.0 alle Daten lokal (Room/SQLite). In Phase 2 soll ein REST-Server hinzukommen, der die Synchronisierung und das Sharing des Inventars zwischen mehreren Geräten ermöglicht.
**Einsatzszenario:**
- Privater Gebrauch, kein kommerzieller Betrieb
- Wenige Geräte (210), keine hohe Last
- Während Entwicklung: Server lokal auf Laptop, Testgerät im LAN
- Später: kleiner Server im Internet (VPS / Homeserver)
- Datenformat: JSON (kotlinx.serialization, bereits im Client definiert)
**Vom Issue genannte Optionen:** Ktor, Spring Boot, Node.js/Express, Python/FastAPI
---
## Must-Have (Eliminatoren)
- REST-API bereitstellen (JSON Request/Response)
- Einfaches Deployment auf einem kleinen Linux-VPS oder Homeserver
- Unterstützung für grundlegende CRUD-Operationen (Inventar-Items, Kategorien, Lagerorte)
- JSON als primäres Datenformat (kompatibel mit dem bestehenden Client-Datenmodell)
- Aktiv gewartet, stabile Release-Versionen verfügbar
- Geringe Betriebskosten (wenig RAM/CPU-Verbrauch für wenige Nutzer)
## Should-Have (gewichtete Pluspunkte)
- **Kotlin-Kompatibilität:** Gleiche Sprache wie der Android-Client (Wiederverwendung von Datenmodellen, kotlinx.serialization)
- Einfache Einrichtung und geringer Boilerplate
- Integrierte Unterstützung für Coroutines / async I/O
- Leichtgewichtig (kein Application-Server wie Tomcat nötig)
- Gute Dokumentation und Lernressourcen
- Einfache Datenbankanbindung (SQLite oder PostgreSQL)
- Einfaches Testen (Unit- und Integrationstests)
## Nice-to-Have (Bonus)
- WebSocket-Unterstützung (für Echtzeit-Sync in einer späteren Phase)
- Eingebaute Authentifizierung / Sicherheitsmechanismen
- Docker-Image oder einfache Containerisierung
- OpenAPI / Swagger-Generierung
- Hot-Reload / schnelle Entwicklungszyklen
- Type-safe Routing
## Constraints
- **Zielplattform Server:** Linux (VPS / Homeserver), optional auch Windows/macOS für lokale Entwicklung
- **Client-Sprache:** Kotlin (Android) geteilte Modelle sind ein großer Vorteil
- **Serialisierung:** kotlinx.serialization (bereits im Client im Einsatz)
- **Lizenz:** Open Source (MIT, Apache 2.0 o.ä.)
- **Skalierung:** Nicht relevant max. 10 gleichzeitige Nutzer
- **Budget:** Kein Budget für kommerzielle Lizenzen oder teure Hosting-Lösungen

View file

@ -32,10 +32,9 @@ internal data class ItemEntity(
@ColumnInfo(name = "quantity") val quantity: Double, @ColumnInfo(name = "quantity") val quantity: Double,
@ColumnInfo(name = "unit") val unit: String, @ColumnInfo(name = "unit") val unit: String,
@ColumnInfo(name = "unit_price") val unitPrice: Double, @ColumnInfo(name = "unit_price") val unitPrice: Double,
@ColumnInfo(name = "kcal_per_100g") val kcalPer100g: Int?, @ColumnInfo(name = "kcal_per_kg") val kcalPerKg: Int?,
@ColumnInfo(name = "expiry_date") val expiryDate: LocalDate?, @ColumnInfo(name = "expiry_date") val expiryDate: LocalDate?,
@ColumnInfo(name = "location_id") val locationId: Int, @ColumnInfo(name = "location_id") val locationId: Int,
@ColumnInfo(name = "min_stock") val minStock: Double,
@ColumnInfo(name = "notes") val notes: String, @ColumnInfo(name = "notes") val notes: String,
@ColumnInfo(name = "last_updated") val lastUpdated: Long @ColumnInfo(name = "last_updated") val lastUpdated: Long
) )

View file

@ -64,10 +64,9 @@ internal class ImportExportRepositoryImpl @Inject constructor(
quantity = item.quantity, quantity = item.quantity,
unit = item.unit, unit = item.unit,
unitPrice = item.unitPrice, unitPrice = item.unitPrice,
kcalPer100g = item.kcalPer100g, kcalPerKg = item.kcalPerKg,
expiryDate = item.expiryDate?.toString(), expiryDate = item.expiryDate?.toString(),
locationId = item.locationId, locationId = item.locationId,
minStock = item.minStock,
notes = item.notes, notes = item.notes,
lastUpdated = item.lastUpdated lastUpdated = item.lastUpdated
) )
@ -102,10 +101,9 @@ internal class ImportExportRepositoryImpl @Inject constructor(
quantity = item.quantity, quantity = item.quantity,
unit = item.unit, unit = item.unit,
unitPrice = item.unitPrice, unitPrice = item.unitPrice,
kcalPer100g = item.kcalPer100g, kcalPerKg = item.kcalPerKg,
expiryDate = item.expiryDate?.let { LocalDate.parse(it) }, expiryDate = item.expiryDate?.let { LocalDate.parse(it) },
locationId = item.locationId, locationId = item.locationId,
minStock = item.minStock,
notes = item.notes, notes = item.notes,
lastUpdated = item.lastUpdated lastUpdated = item.lastUpdated
) )

View file

@ -27,6 +27,7 @@ internal object DatabaseModule {
fun provideDatabase(@ApplicationContext context: Context): KrisenvorratDatabase = fun provideDatabase(@ApplicationContext context: Context): KrisenvorratDatabase =
Room.databaseBuilder(context, KrisenvorratDatabase::class.java, "krisenvorrat.db") Room.databaseBuilder(context, KrisenvorratDatabase::class.java, "krisenvorrat.db")
.addCallback(DefaultDataCallback) .addCallback(DefaultDataCallback)
.fallbackToDestructiveMigration()
.build() .build()
private object DefaultDataCallback : RoomDatabase.Callback() { private object DefaultDataCallback : RoomDatabase.Callback() {

View file

@ -1,8 +0,0 @@
package de.krisenvorrat.app.domain.model
import de.krisenvorrat.app.data.db.entity.ItemEntity
internal data class MinStockWarning(
val item: ItemEntity,
val deficit: Double
)

View file

@ -19,9 +19,9 @@ internal class CalculateSupplyRangeUseCase @Inject constructor() {
if (dailyNeed <= 0) return 0.0 if (dailyNeed <= 0) return 0.0
val totalKcal = items.sumOf { item -> val totalKcal = items.sumOf { item ->
val kcalPer100g = item.kcalPer100g ?: return@sumOf 0.0 val kcalPerKg = item.kcalPerKg ?: return@sumOf 0.0
val grams = convertToGrams(item.quantity, item.unit) ?: return@sumOf 0.0 val grams = convertToGrams(item.quantity, item.unit) ?: return@sumOf 0.0
(grams / 100.0) * kcalPer100g (grams / 1000.0) * kcalPerKg
} }
return totalKcal / dailyNeed return totalKcal / dailyNeed

View file

@ -1,20 +0,0 @@
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 }
}
}

View file

@ -60,11 +60,10 @@ internal fun DashboardScreen(
item { SupplyRangeCard(supplyRangeDays = uiState.supplyRangeDays) } item { SupplyRangeCard(supplyRangeDays = uiState.supplyRangeDays) }
if (uiState.hasExpiryWarnings || uiState.hasMinStockWarnings) { if (uiState.hasExpiryWarnings) {
item { item {
WarningsSummaryCard( WarningsSummaryCard(
expiryCount = uiState.expiryWarnings.size, expiryCount = uiState.expiryWarnings.size
minStockCount = uiState.minStockWarnings.size
) )
} }
} }
@ -176,8 +175,8 @@ private fun SupplyRangeCard(supplyRangeDays: Double) {
} }
@Composable @Composable
private fun WarningsSummaryCard(expiryCount: Int, minStockCount: Int) { private fun WarningsSummaryCard(expiryCount: Int) {
val totalCount = expiryCount + minStockCount val totalCount = expiryCount
Card( Card(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors( colors = CardDefaults.cardColors(
@ -198,13 +197,6 @@ private fun WarningsSummaryCard(expiryCount: Int, minStockCount: Int) {
color = MaterialTheme.colorScheme.onErrorContainer color = MaterialTheme.colorScheme.onErrorContainer
) )
} }
if (minStockCount > 0) {
Text(
text = "$minStockCount Mindestbestand-Warnungen",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onErrorContainer
)
}
} }
} }
} }

View file

@ -2,14 +2,12 @@ package de.krisenvorrat.app.ui.dashboard
import de.krisenvorrat.app.domain.model.CategorySummary import de.krisenvorrat.app.domain.model.CategorySummary
import de.krisenvorrat.app.domain.model.ExpiryWarning import de.krisenvorrat.app.domain.model.ExpiryWarning
import de.krisenvorrat.app.domain.model.MinStockWarning
internal data class DashboardUiState( internal data class DashboardUiState(
val categorySummaries: List<CategorySummary> = emptyList(), val categorySummaries: List<CategorySummary> = emptyList(),
val totalValue: Double = 0.0, val totalValue: Double = 0.0,
val supplyRangeDays: Double = 0.0, val supplyRangeDays: Double = 0.0,
val expiryWarnings: List<ExpiryWarning> = emptyList(), val expiryWarnings: List<ExpiryWarning> = emptyList(),
val minStockWarnings: List<MinStockWarning> = emptyList(),
val isLoading: Boolean = true val isLoading: Boolean = true
) { ) {
val totalItemCount: Int val totalItemCount: Int
@ -17,7 +15,4 @@ internal data class DashboardUiState(
val hasExpiryWarnings: Boolean val hasExpiryWarnings: Boolean
get() = expiryWarnings.isNotEmpty() get() = expiryWarnings.isNotEmpty()
val hasMinStockWarnings: Boolean
get() = minStockWarnings.isNotEmpty()
} }

View file

@ -12,7 +12,6 @@ import de.krisenvorrat.app.domain.usecase.CalculateCategorySummaryUseCase
import de.krisenvorrat.app.domain.usecase.CalculateSupplyRangeUseCase import de.krisenvorrat.app.domain.usecase.CalculateSupplyRangeUseCase
import de.krisenvorrat.app.domain.usecase.CalculateTotalValueUseCase import de.krisenvorrat.app.domain.usecase.CalculateTotalValueUseCase
import de.krisenvorrat.app.domain.usecase.GetExpiryWarningsUseCase import de.krisenvorrat.app.domain.usecase.GetExpiryWarningsUseCase
import de.krisenvorrat.app.domain.usecase.GetMinStockWarningsUseCase
import de.krisenvorrat.app.domain.repository.CategoryRepository import de.krisenvorrat.app.domain.repository.CategoryRepository
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
@ -30,8 +29,7 @@ internal class DashboardViewModel @Inject constructor(
private val calculateCategorySummary: CalculateCategorySummaryUseCase, private val calculateCategorySummary: CalculateCategorySummaryUseCase,
private val calculateTotalValue: CalculateTotalValueUseCase, private val calculateTotalValue: CalculateTotalValueUseCase,
private val calculateSupplyRange: CalculateSupplyRangeUseCase, private val calculateSupplyRange: CalculateSupplyRangeUseCase,
private val getExpiryWarnings: GetExpiryWarningsUseCase, private val getExpiryWarnings: GetExpiryWarningsUseCase
private val getMinStockWarnings: GetMinStockWarningsUseCase
) : ViewModel() { ) : ViewModel() {
private val _uiState = MutableStateFlow(DashboardUiState()) private val _uiState = MutableStateFlow(DashboardUiState())
@ -54,7 +52,6 @@ internal class DashboardViewModel @Inject constructor(
totalValue = calculateTotalValue(items), totalValue = calculateTotalValue(items),
supplyRangeDays = calculateSupplyRange(items, totalDailyKcal), supplyRangeDays = calculateSupplyRange(items, totalDailyKcal),
expiryWarnings = getExpiryWarnings(items), expiryWarnings = getExpiryWarnings(items),
minStockWarnings = getMinStockWarnings(items),
isLoading = false isLoading = false
) )
}.collect { state -> }.collect { state ->

View file

@ -177,11 +177,11 @@ internal fun ItemFormScreen(
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
// kcal/100g // kcal/kg
OutlinedTextField( OutlinedTextField(
value = uiState.kcalPer100g, value = uiState.kcalPerKg,
onValueChange = viewModel::updateKcalPer100g, onValueChange = viewModel::updateKcalPerKg,
label = { Text("kcal / 100g") }, label = { Text("kcal / kg") },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
singleLine = true, singleLine = true,
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
@ -189,18 +189,6 @@ internal fun ItemFormScreen(
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
// Mindestbestand
OutlinedTextField(
value = uiState.minStock,
onValueChange = viewModel::updateMinStock,
label = { Text("Mindestbestand") },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal),
singleLine = true,
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(8.dp))
// Notizen // Notizen
OutlinedTextField( OutlinedTextField(
value = uiState.notes, value = uiState.notes,

View file

@ -27,10 +27,9 @@ internal data class ItemFormUiState(
val quantity: String = "", val quantity: String = "",
val unit: String = "", val unit: String = "",
val unitPrice: String = "", val unitPrice: String = "",
val kcalPer100g: String = "", val kcalPerKg: String = "",
val expiryDate: LocalDate? = null, val expiryDate: LocalDate? = null,
val locationId: Int? = null, val locationId: Int? = null,
val minStock: String = "",
val notes: String = "", val notes: String = "",
val categories: List<CategoryEntity> = emptyList(), val categories: List<CategoryEntity> = emptyList(),
val locations: List<LocationEntity> = emptyList(), val locations: List<LocationEntity> = emptyList(),
@ -109,10 +108,9 @@ internal class ItemFormViewModel @Inject constructor(
quantity = item.quantity.toBigDecimal().stripTrailingZeros().toPlainString(), quantity = item.quantity.toBigDecimal().stripTrailingZeros().toPlainString(),
unit = item.unit, unit = item.unit,
unitPrice = item.unitPrice.toBigDecimal().stripTrailingZeros().toPlainString(), unitPrice = item.unitPrice.toBigDecimal().stripTrailingZeros().toPlainString(),
kcalPer100g = item.kcalPer100g?.toString() ?: "", kcalPerKg = item.kcalPerKg?.toString() ?: "",
expiryDate = item.expiryDate, expiryDate = item.expiryDate,
locationId = item.locationId, locationId = item.locationId,
minStock = item.minStock.toBigDecimal().stripTrailingZeros().toPlainString(),
notes = item.notes, notes = item.notes,
isLoading = false isLoading = false
) )
@ -146,8 +144,8 @@ internal class ItemFormViewModel @Inject constructor(
_uiState.update { it.copy(unitPrice = value) } _uiState.update { it.copy(unitPrice = value) }
} }
fun updateKcalPer100g(value: String) { fun updateKcalPerKg(value: String) {
_uiState.update { it.copy(kcalPer100g = value) } _uiState.update { it.copy(kcalPerKg = value) }
} }
fun updateExpiryDate(value: LocalDate?) { fun updateExpiryDate(value: LocalDate?) {
@ -158,10 +156,6 @@ internal class ItemFormViewModel @Inject constructor(
_uiState.update { it.copy(locationId = value, validationErrors = it.validationErrors - "locationId") } _uiState.update { it.copy(locationId = value, validationErrors = it.validationErrors - "locationId") }
} }
fun updateMinStock(value: String) {
_uiState.update { it.copy(minStock = value) }
}
fun updateNotes(value: String) { fun updateNotes(value: String) {
_uiState.update { it.copy(notes = value) } _uiState.update { it.copy(notes = value) }
} }
@ -183,10 +177,9 @@ internal class ItemFormViewModel @Inject constructor(
quantity = state.quantity.toDouble(), quantity = state.quantity.toDouble(),
unit = state.unit.trim(), unit = state.unit.trim(),
unitPrice = state.unitPrice.toDoubleOrNull() ?: 0.0, unitPrice = state.unitPrice.toDoubleOrNull() ?: 0.0,
kcalPer100g = state.kcalPer100g.toIntOrNull(), kcalPerKg = state.kcalPerKg.toIntOrNull(),
expiryDate = state.expiryDate, expiryDate = state.expiryDate,
locationId = state.locationId!!, locationId = state.locationId!!,
minStock = state.minStock.toDoubleOrNull() ?: 0.0,
notes = state.notes.trim(), notes = state.notes.trim(),
lastUpdated = System.currentTimeMillis() lastUpdated = System.currentTimeMillis()
) )

View file

@ -82,10 +82,9 @@ internal class ItemListViewModel @Inject constructor(
quantity = item.quantity, quantity = item.quantity,
unit = item.unit, unit = item.unit,
unitPrice = 0.0, unitPrice = 0.0,
kcalPer100g = null, kcalPerKg = null,
expiryDate = item.expiryDate, expiryDate = item.expiryDate,
locationId = 0, locationId = 0,
minStock = 0.0,
notes = "", notes = "",
lastUpdated = 0L lastUpdated = 0L
) )

View file

@ -29,7 +29,6 @@ import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import de.krisenvorrat.app.domain.model.ExpiryUrgency import de.krisenvorrat.app.domain.model.ExpiryUrgency
import de.krisenvorrat.app.domain.model.ExpiryWarning import de.krisenvorrat.app.domain.model.ExpiryWarning
import de.krisenvorrat.app.domain.model.MinStockWarning
import java.util.Locale import java.util.Locale
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@ -84,19 +83,6 @@ internal fun WarningsScreen(
} }
} }
if (uiState.hasMinStockWarnings) {
item {
Text(
text = "Mindestbestand unterschritten (${uiState.minStockWarnings.size})",
style = MaterialTheme.typography.titleMedium,
modifier = Modifier.padding(top = 4.dp)
)
}
items(uiState.minStockWarnings) { warning ->
MinStockWarningItem(warning)
}
}
item { Spacer(modifier = Modifier.height(8.dp)) } item { Spacer(modifier = Modifier.height(8.dp)) }
} }
} }
@ -145,32 +131,4 @@ private fun ExpiryWarningItem(warning: ExpiryWarning) {
} }
} }
@Composable
private fun MinStockWarningItem(warning: MinStockWarning) {
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.errorContainer
)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = warning.item.name,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onErrorContainer
)
Text(
text = "fehlen: ${String.format(Locale.GERMANY, "%.1f", warning.deficit)} ${warning.item.unit}",
style = MaterialTheme.typography.bodyMedium,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.error
)
}
}
}

View file

@ -1,22 +1,17 @@
package de.krisenvorrat.app.ui.warnings package de.krisenvorrat.app.ui.warnings
import de.krisenvorrat.app.domain.model.ExpiryWarning import de.krisenvorrat.app.domain.model.ExpiryWarning
import de.krisenvorrat.app.domain.model.MinStockWarning
internal data class WarningsUiState( internal data class WarningsUiState(
val expiryWarnings: List<ExpiryWarning> = emptyList(), val expiryWarnings: List<ExpiryWarning> = emptyList(),
val minStockWarnings: List<MinStockWarning> = emptyList(),
val isLoading: Boolean = true val isLoading: Boolean = true
) { ) {
val hasExpiryWarnings: Boolean val hasExpiryWarnings: Boolean
get() = expiryWarnings.isNotEmpty() get() = expiryWarnings.isNotEmpty()
val hasMinStockWarnings: Boolean
get() = minStockWarnings.isNotEmpty()
val totalWarningCount: Int val totalWarningCount: Int
get() = expiryWarnings.size + minStockWarnings.size get() = expiryWarnings.size
val hasWarnings: Boolean val hasWarnings: Boolean
get() = hasExpiryWarnings || hasMinStockWarnings get() = hasExpiryWarnings
} }

View file

@ -5,7 +5,6 @@ import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import de.krisenvorrat.app.domain.repository.ItemRepository import de.krisenvorrat.app.domain.repository.ItemRepository
import de.krisenvorrat.app.domain.usecase.GetExpiryWarningsUseCase import de.krisenvorrat.app.domain.usecase.GetExpiryWarningsUseCase
import de.krisenvorrat.app.domain.usecase.GetMinStockWarningsUseCase
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
@ -16,8 +15,7 @@ import javax.inject.Inject
@HiltViewModel @HiltViewModel
internal class WarningsViewModel @Inject constructor( internal class WarningsViewModel @Inject constructor(
private val itemRepository: ItemRepository, private val itemRepository: ItemRepository,
private val getExpiryWarnings: GetExpiryWarningsUseCase, private val getExpiryWarnings: GetExpiryWarningsUseCase
private val getMinStockWarnings: GetMinStockWarningsUseCase
) : ViewModel() { ) : ViewModel() {
private val _uiState = MutableStateFlow(WarningsUiState()) private val _uiState = MutableStateFlow(WarningsUiState())
@ -29,7 +27,6 @@ internal class WarningsViewModel @Inject constructor(
_uiState.update { _uiState.update {
WarningsUiState( WarningsUiState(
expiryWarnings = getExpiryWarnings(items), expiryWarnings = getExpiryWarnings(items),
minStockWarnings = getMinStockWarnings(items),
isLoading = false isLoading = false
) )
} }

View file

@ -68,7 +68,7 @@ class ImportExportRepositoryImplTest {
@Test @Test
fun test_importFromJson_withValidJson_upsertAllEntities() = runBlocking { fun test_importFromJson_withValidJson_upsertAllEntities() = runBlocking {
// Given // Given
val validJson = """{"version":1,"categories":[{"id":1,"name":"Lebensmittel"}],"locations":[{"id":1,"name":"Keller"}],"items":[{"id":"item1","name":"Konserve","categoryId":1,"quantity":2.0,"unit":"Stk","unitPrice":1.5,"kcalPer100g":null,"expiryDate":null,"locationId":1,"minStock":1.0,"notes":"","lastUpdated":0}],"settings":[{"key":"theme","value":"dark"}]}""" val validJson = """{"version":1,"categories":[{"id":1,"name":"Lebensmittel"}],"locations":[{"id":1,"name":"Keller"}],"items":[{"id":"item1","name":"Konserve","categoryId":1,"quantity":2.0,"unit":"Stk","unitPrice":1.5,"kcalPerKg":null,"expiryDate":null,"locationId":1,"notes":"","lastUpdated":0}],"settings":[{"key":"theme","value":"dark"}]}"""
val categoryDao = FakeCategoryDao() val categoryDao = FakeCategoryDao()
val locationDao = FakeLocationDao() val locationDao = FakeLocationDao()
val itemDao = FakeItemDao() val itemDao = FakeItemDao()

View file

@ -44,10 +44,9 @@ class JsonRoundtripTest {
quantity = 10.0, quantity = 10.0,
unit = "Stk", unit = "Stk",
unitPrice = 2.49, unitPrice = 2.49,
kcalPer100g = 180, kcalPerKg = 180,
expiryDate = LocalDate.of(2027, 6, 15), expiryDate = LocalDate.of(2027, 6, 15),
locationId = 1, locationId = 1,
minStock = 5.0,
notes = "Ravioli", notes = "Ravioli",
lastUpdated = 1700000000L lastUpdated = 1700000000L
), ),
@ -58,10 +57,9 @@ class JsonRoundtripTest {
quantity = 3.0, quantity = 3.0,
unit = "Stk", unit = "Stk",
unitPrice = 0.99, unitPrice = 0.99,
kcalPer100g = null, kcalPerKg = null,
expiryDate = null, expiryDate = null,
locationId = 2, locationId = 2,
minStock = 1.0,
notes = "", notes = "",
lastUpdated = 1700000001L lastUpdated = 1700000001L
) )
@ -121,10 +119,9 @@ class JsonRoundtripTest {
assertEquals(original.quantity, imported?.quantity) assertEquals(original.quantity, imported?.quantity)
assertEquals(original.unit, imported?.unit) assertEquals(original.unit, imported?.unit)
assertEquals(original.unitPrice, imported?.unitPrice) assertEquals(original.unitPrice, imported?.unitPrice)
assertEquals(original.kcalPer100g, imported?.kcalPer100g) assertEquals(original.kcalPerKg, imported?.kcalPerKg)
assertEquals(original.expiryDate, imported?.expiryDate) assertEquals(original.expiryDate, imported?.expiryDate)
assertEquals(original.locationId, imported?.locationId) assertEquals(original.locationId, imported?.locationId)
assertEquals(original.minStock, imported?.minStock)
assertEquals(original.notes, imported?.notes) assertEquals(original.notes, imported?.notes)
assertEquals(original.lastUpdated, imported?.lastUpdated) assertEquals(original.lastUpdated, imported?.lastUpdated)
} }
@ -154,10 +151,9 @@ class JsonRoundtripTest {
quantity = 1.0, quantity = 1.0,
unit = "Stk", unit = "Stk",
unitPrice = 0.0, unitPrice = 0.0,
kcalPer100g = null, kcalPerKg = null,
expiryDate = null, expiryDate = null,
locationId = 1, locationId = 1,
minStock = 0.0,
notes = "", notes = "",
lastUpdated = 0L lastUpdated = 0L
) )
@ -176,7 +172,7 @@ class JsonRoundtripTest {
// Then // Then
assertTrue(result.isSuccess) assertTrue(result.isSuccess)
val imported = importItemDao.getItems().first() val imported = importItemDao.getItems().first()
assertEquals(null, imported.kcalPer100g) assertEquals(null, imported.kcalPerKg)
assertEquals(null, imported.expiryDate) assertEquals(null, imported.expiryDate)
} }

View file

@ -121,10 +121,9 @@ internal fun buildItemEntity(id: String = "item1") = ItemEntity(
quantity = 2.0, quantity = 2.0,
unit = "Stk", unit = "Stk",
unitPrice = 1.5, unitPrice = 1.5,
kcalPer100g = null, kcalPerKg = null,
expiryDate = null, expiryDate = null,
locationId = 1, locationId = 1,
minStock = 1.0,
notes = "", notes = "",
lastUpdated = 0L lastUpdated = 0L
) )

View file

@ -92,10 +92,9 @@ private fun buildItem(
quantity = 2.0, quantity = 2.0,
unit = "Stk", unit = "Stk",
unitPrice = 1.5, unitPrice = 1.5,
kcalPer100g = null, kcalPerKg = null,
expiryDate = null, expiryDate = null,
locationId = locationId, locationId = locationId,
minStock = 1.0,
notes = "", notes = "",
lastUpdated = 0L lastUpdated = 0L
) )

View file

@ -47,10 +47,9 @@ class SyncServiceImplTest {
quantity = 5.0, quantity = 5.0,
unit = "Dose", unit = "Dose",
unitPrice = 1.29, unitPrice = 1.29,
kcalPer100g = 100, kcalPerKg = 100,
expiryDate = "2027-06-01", expiryDate = "2027-06-01",
locationId = 1, locationId = 1,
minStock = 2.0,
notes = "", notes = "",
lastUpdated = System.currentTimeMillis() lastUpdated = System.currentTimeMillis()
) )

View file

@ -9,10 +9,10 @@ class CalculateSupplyRangeUseCaseTest {
@Test @Test
fun test_invoke_withKgItems_returnsCorrectDays() { fun test_invoke_withKgItems_returnsCorrectDays() {
// Given 2 kg Reis à 350 kcal/100g = 7000 kcal // Given 2 kg Reis à 3500 kcal/kg = 7000 kcal
// 4000 kcal/Tag (2 × 2000) → 1.75 Tage // 4000 kcal/Tag (2 × 2000) → 1.75 Tage
val items = listOf( val items = listOf(
buildTestItem(id = "1", quantity = 2.0, unit = "kg", kcalPer100g = 350) buildTestItem(id = "1", quantity = 2.0, unit = "kg", kcalPerKg = 3500)
) )
// When // When
@ -24,10 +24,10 @@ class CalculateSupplyRangeUseCaseTest {
@Test @Test
fun test_invoke_withGramItems_returnsCorrectDays() { fun test_invoke_withGramItems_returnsCorrectDays() {
// Given 500 g Nudeln à 360 kcal/100g = 1800 kcal // Given 500 g Nudeln à 3600 kcal/kg = 1800 kcal
// 2000 kcal/Tag → 0.9 Tage // 2000 kcal/Tag → 0.9 Tage
val items = listOf( val items = listOf(
buildTestItem(id = "1", quantity = 500.0, unit = "g", kcalPer100g = 360) buildTestItem(id = "1", quantity = 500.0, unit = "g", kcalPerKg = 3600)
) )
// When // When
@ -39,11 +39,11 @@ class CalculateSupplyRangeUseCaseTest {
@Test @Test
fun test_invoke_withMultipleItems_sumsTotalKcal() { fun test_invoke_withMultipleItems_sumsTotalKcal() {
// Given 1 kg Reis (350 kcal/100g = 3500 kcal) + 500 g Nudeln (360 kcal/100g = 1800 kcal) = 5300 kcal // Given 1 kg Reis (3500 kcal/kg = 3500 kcal) + 500 g Nudeln (3600 kcal/kg = 1800 kcal) = 5300 kcal
// 4000 kcal/Tag → 1.325 Tage // 4000 kcal/Tag → 1.325 Tage
val items = listOf( val items = listOf(
buildTestItem(id = "1", quantity = 1.0, unit = "kg", kcalPer100g = 350), buildTestItem(id = "1", quantity = 1.0, unit = "kg", kcalPerKg = 3500),
buildTestItem(id = "2", quantity = 500.0, unit = "g", kcalPer100g = 360) buildTestItem(id = "2", quantity = 500.0, unit = "g", kcalPerKg = 3600)
) )
// When // When
@ -64,13 +64,13 @@ class CalculateSupplyRangeUseCaseTest {
@Test @Test
fun test_invoke_withNullKcal_skipsItem() { fun test_invoke_withNullKcal_skipsItem() {
// Given Item ohne kcalPer100g wird ignoriert // Given Item ohne kcalPerKg wird ignoriert
val items = listOf( val items = listOf(
buildTestItem(id = "1", quantity = 1.0, unit = "kg", kcalPer100g = null), buildTestItem(id = "1", quantity = 1.0, unit = "kg", kcalPerKg = null),
buildTestItem(id = "2", quantity = 1.0, unit = "kg", kcalPer100g = 200) buildTestItem(id = "2", quantity = 1.0, unit = "kg", kcalPerKg = 2000)
) )
// When Nur 1 kg à 200 kcal/100g = 2000 kcal, 2000 kcal/Tag = 1.0 Tage // When Nur 1 kg à 2000 kcal/kg = 2000 kcal, 2000 kcal/Tag = 1.0 Tage
val result = useCase(items, totalDailyKcal = 2000) val result = useCase(items, totalDailyKcal = 2000)
// Then // Then
@ -81,7 +81,7 @@ class CalculateSupplyRangeUseCaseTest {
fun test_invoke_withNonWeightUnit_skipsItem() { fun test_invoke_withNonWeightUnit_skipsItem() {
// Given "Stk" ist keine Gewichtseinheit → wird ignoriert // Given "Stk" ist keine Gewichtseinheit → wird ignoriert
val items = listOf( val items = listOf(
buildTestItem(id = "1", quantity = 5.0, unit = "Stk", kcalPer100g = 200) buildTestItem(id = "1", quantity = 5.0, unit = "Stk", kcalPerKg = 2000)
) )
// When // When
@ -95,7 +95,7 @@ class CalculateSupplyRangeUseCaseTest {
fun test_invoke_withZeroTotalDailyKcal_returnsZero() { fun test_invoke_withZeroTotalDailyKcal_returnsZero() {
// Given // Given
val items = listOf( val items = listOf(
buildTestItem(id = "1", quantity = 1.0, unit = "kg", kcalPer100g = 350) buildTestItem(id = "1", quantity = 1.0, unit = "kg", kcalPerKg = 3500)
) )
// When // When
@ -107,10 +107,10 @@ class CalculateSupplyRangeUseCaseTest {
@Test @Test
fun test_invoke_withDefaultParameters_uses4000KcalPerDay() { fun test_invoke_withDefaultParameters_uses4000KcalPerDay() {
// Given 4 kg Reis à 350 kcal/100g = 14000 kcal // Given 4 kg Reis à 3500 kcal/kg = 14000 kcal
// Default: 4000 kcal/Tag → 3.5 Tage // Default: 4000 kcal/Tag → 3.5 Tage
val items = listOf( val items = listOf(
buildTestItem(id = "1", quantity = 4.0, unit = "kg", kcalPer100g = 350) buildTestItem(id = "1", quantity = 4.0, unit = "kg", kcalPerKg = 3500)
) )
// When // When
@ -122,10 +122,10 @@ class CalculateSupplyRangeUseCaseTest {
@Test @Test
fun test_invoke_withMgUnit_convertsCorrectly() { fun test_invoke_withMgUnit_convertsCorrectly() {
// Given 500000 mg = 500 g à 200 kcal/100g = 1000 kcal // Given 500000 mg = 500 g à 2000 kcal/kg = 1000 kcal
// 1000 kcal/Tag → 1.0 Tag // 1000 kcal/Tag → 1.0 Tag
val items = listOf( val items = listOf(
buildTestItem(id = "1", quantity = 500000.0, unit = "mg", kcalPer100g = 200) buildTestItem(id = "1", quantity = 500000.0, unit = "mg", kcalPerKg = 2000)
) )
// When // When
@ -137,12 +137,12 @@ class CalculateSupplyRangeUseCaseTest {
@Test @Test
fun test_invoke_withMixedUnits_onlyCountsWeightBased() { fun test_invoke_withMixedUnits_onlyCountsWeightBased() {
// Given 1 kg (350 kcal/100g) + 5 Stk (ignored) + 2 L (ignored) // Given 1 kg (3500 kcal/kg) + 5 Stk (ignored) + 2 L (ignored)
// Total: 3500 kcal, 2000 kcal/Tag → 1.75 Tage // Total: 3500 kcal, 2000 kcal/Tag → 1.75 Tage
val items = listOf( val items = listOf(
buildTestItem(id = "1", quantity = 1.0, unit = "kg", kcalPer100g = 350), buildTestItem(id = "1", quantity = 1.0, unit = "kg", kcalPerKg = 3500),
buildTestItem(id = "2", quantity = 5.0, unit = "Stk", kcalPer100g = 100), buildTestItem(id = "2", quantity = 5.0, unit = "Stk", kcalPerKg = 1000),
buildTestItem(id = "3", quantity = 2.0, unit = "L", kcalPer100g = 45) buildTestItem(id = "3", quantity = 2.0, unit = "L", kcalPerKg = 450)
) )
// When // When

View file

@ -1,128 +0,0 @@
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") })
}
}

View file

@ -10,10 +10,9 @@ internal fun buildTestItem(
quantity: Double = 1.0, quantity: Double = 1.0,
unit: String = "Stk", unit: String = "Stk",
unitPrice: Double = 0.0, unitPrice: Double = 0.0,
kcalPer100g: Int? = null, kcalPerKg: Int? = null,
expiryDate: LocalDate? = null, expiryDate: LocalDate? = null,
locationId: Int = 1, locationId: Int = 1
minStock: Double = 0.0
) = ItemEntity( ) = ItemEntity(
id = id, id = id,
name = name, name = name,
@ -21,10 +20,9 @@ internal fun buildTestItem(
quantity = quantity, quantity = quantity,
unit = unit, unit = unit,
unitPrice = unitPrice, unitPrice = unitPrice,
kcalPer100g = kcalPer100g, kcalPerKg = kcalPerKg,
expiryDate = expiryDate, expiryDate = expiryDate,
locationId = locationId, locationId = locationId,
minStock = minStock,
notes = "", notes = "",
lastUpdated = 0L lastUpdated = 0L
) )

View file

@ -293,8 +293,8 @@ class CategoryListViewModelTest {
fakeItemRepository.addItem( fakeItemRepository.addItem(
ItemEntity( ItemEntity(
id = "i1", name = "Reis", categoryId = 1, quantity = 1.0, id = "i1", name = "Reis", categoryId = 1, quantity = 1.0,
unit = "kg", unitPrice = 0.0, kcalPer100g = null, expiryDate = null, unit = "kg", unitPrice = 0.0, kcalPerKg = null, expiryDate = null,
locationId = 1, minStock = 0.0, notes = "", lastUpdated = 0L locationId = 1, notes = "", lastUpdated = 0L
) )
) )
val category = CategoryEntity(id = 1, name = "Lebensmittel") val category = CategoryEntity(id = 1, name = "Lebensmittel")
@ -318,8 +318,8 @@ class CategoryListViewModelTest {
fakeItemRepository.addItem( fakeItemRepository.addItem(
ItemEntity( ItemEntity(
id = "i1", name = "Reis", categoryId = 1, quantity = 1.0, id = "i1", name = "Reis", categoryId = 1, quantity = 1.0,
unit = "kg", unitPrice = 0.0, kcalPer100g = null, expiryDate = null, unit = "kg", unitPrice = 0.0, kcalPerKg = null, expiryDate = null,
locationId = 1, minStock = 0.0, notes = "", lastUpdated = 0L locationId = 1, notes = "", lastUpdated = 0L
) )
) )
val category = CategoryEntity(id = 1, name = "Lebensmittel") val category = CategoryEntity(id = 1, name = "Lebensmittel")
@ -345,8 +345,8 @@ class CategoryListViewModelTest {
fakeItemRepository.addItem( fakeItemRepository.addItem(
ItemEntity( ItemEntity(
id = "i1", name = "Reis", categoryId = 1, quantity = 1.0, id = "i1", name = "Reis", categoryId = 1, quantity = 1.0,
unit = "kg", unitPrice = 0.0, kcalPer100g = null, expiryDate = null, unit = "kg", unitPrice = 0.0, kcalPerKg = null, expiryDate = null,
locationId = 1, minStock = 0.0, notes = "", lastUpdated = 0L locationId = 1, notes = "", lastUpdated = 0L
) )
) )
viewModel.showDeleteDialog(CategoryEntity(id = 1, name = "Lebensmittel")) viewModel.showDeleteDialog(CategoryEntity(id = 1, name = "Lebensmittel"))

View file

@ -14,7 +14,6 @@ import de.krisenvorrat.app.domain.usecase.CalculateCategorySummaryUseCase
import de.krisenvorrat.app.domain.usecase.CalculateSupplyRangeUseCase import de.krisenvorrat.app.domain.usecase.CalculateSupplyRangeUseCase
import de.krisenvorrat.app.domain.usecase.CalculateTotalValueUseCase import de.krisenvorrat.app.domain.usecase.CalculateTotalValueUseCase
import de.krisenvorrat.app.domain.usecase.GetExpiryWarningsUseCase import de.krisenvorrat.app.domain.usecase.GetExpiryWarningsUseCase
import de.krisenvorrat.app.domain.usecase.GetMinStockWarningsUseCase
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@ -61,8 +60,7 @@ class DashboardViewModelTest {
calculateCategorySummary = CalculateCategorySummaryUseCase(), calculateCategorySummary = CalculateCategorySummaryUseCase(),
calculateTotalValue = CalculateTotalValueUseCase(), calculateTotalValue = CalculateTotalValueUseCase(),
calculateSupplyRange = CalculateSupplyRangeUseCase(), calculateSupplyRange = CalculateSupplyRangeUseCase(),
getExpiryWarnings = GetExpiryWarningsUseCase(), getExpiryWarnings = GetExpiryWarningsUseCase()
getMinStockWarnings = GetMinStockWarningsUseCase()
) )
@Test @Test
@ -80,7 +78,6 @@ class DashboardViewModelTest {
assertEquals(0.0, state.totalValue, 0.001) assertEquals(0.0, state.totalValue, 0.001)
assertEquals(0.0, state.supplyRangeDays, 0.001) assertEquals(0.0, state.supplyRangeDays, 0.001)
assertFalse(state.hasExpiryWarnings) assertFalse(state.hasExpiryWarnings)
assertFalse(state.hasMinStockWarnings)
assertEquals(0, state.totalItemCount) assertEquals(0, state.totalItemCount)
} }
@ -151,7 +148,7 @@ class DashboardViewModelTest {
) )
fakeItemRepository.emit( fakeItemRepository.emit(
listOf( listOf(
buildTestItem(id = "a", quantity = 1000.0, unit = "g", kcalPer100g = 200) buildTestItem(id = "a", quantity = 1000.0, unit = "g", kcalPerKg = 2000)
) )
) )
viewModel = createViewModel() viewModel = createViewModel()
@ -174,7 +171,7 @@ class DashboardViewModelTest {
allZeroAgeGroups.toJson() allZeroAgeGroups.toJson()
) )
fakeItemRepository.emit( fakeItemRepository.emit(
listOf(buildTestItem(id = "a", quantity = 1000.0, unit = "g", kcalPer100g = 400)) listOf(buildTestItem(id = "a", quantity = 1000.0, unit = "g", kcalPerKg = 4000))
) )
viewModel = createViewModel() viewModel = createViewModel()
@ -203,39 +200,6 @@ class DashboardViewModelTest {
assertEquals(1, state.expiryWarnings.size) assertEquals(1, state.expiryWarnings.size)
} }
@Test
fun test_init_withMinStockViolation_minStockWarningsArePresent() = runTest(testDispatcher) {
// Given item with quantity 1 but minStock 5
fakeItemRepository.emit(
listOf(buildTestItem(id = "a", quantity = 1.0, minStock = 5.0))
)
viewModel = createViewModel()
// When
advanceUntilIdle()
// Then
val state = viewModel.uiState.value
assertTrue(state.hasMinStockWarnings)
assertEquals(1, state.minStockWarnings.size)
assertEquals(4.0, state.minStockWarnings.first().deficit, 0.001)
}
@Test
fun test_init_withSufficientStock_noMinStockWarnings() = runTest(testDispatcher) {
// Given item with quantity above minStock
fakeItemRepository.emit(
listOf(buildTestItem(id = "a", quantity = 10.0, minStock = 5.0))
)
viewModel = createViewModel()
// When
advanceUntilIdle()
// Then
assertFalse(viewModel.uiState.value.hasMinStockWarnings)
}
@Test @Test
fun test_init_withFarExpiryDate_noExpiryWarnings() = runTest(testDispatcher) { fun test_init_withFarExpiryDate_noExpiryWarnings() = runTest(testDispatcher) {
// Given item expiring in 2 years (beyond WARNING_MONTHS=12) // Given item expiring in 2 years (beyond WARNING_MONTHS=12)
@ -282,10 +246,9 @@ private fun buildTestItem(
quantity: Double = 1.0, quantity: Double = 1.0,
unit: String = "Stk", unit: String = "Stk",
unitPrice: Double = 0.0, unitPrice: Double = 0.0,
kcalPer100g: Int? = null, kcalPerKg: Int? = null,
expiryDate: LocalDate? = null, expiryDate: LocalDate? = null,
locationId: Int = 1, locationId: Int = 1
minStock: Double = 0.0
) = ItemEntity( ) = ItemEntity(
id = id, id = id,
name = name, name = name,
@ -293,10 +256,9 @@ private fun buildTestItem(
quantity = quantity, quantity = quantity,
unit = unit, unit = unit,
unitPrice = unitPrice, unitPrice = unitPrice,
kcalPer100g = kcalPer100g, kcalPerKg = kcalPerKg,
expiryDate = expiryDate, expiryDate = expiryDate,
locationId = locationId, locationId = locationId,
minStock = minStock,
notes = "", notes = "",
lastUpdated = 0L lastUpdated = 0L
) )

View file

@ -149,8 +149,8 @@ class ItemFormViewModelTest {
fakeItemRepository.addItem( fakeItemRepository.addItem(
ItemEntity( ItemEntity(
id = "prev-1", name = "Alt", categoryId = 1, quantity = 1.0, id = "prev-1", name = "Alt", categoryId = 1, quantity = 1.0,
unit = "kg", unitPrice = 0.0, kcalPer100g = null, expiryDate = null, unit = "kg", unitPrice = 0.0, kcalPerKg = null, expiryDate = null,
locationId = 2, minStock = 0.0, notes = "", lastUpdated = 1000L locationId = 2, notes = "", lastUpdated = 1000L
) )
) )
fakeLocationRepository.emit( fakeLocationRepository.emit(
@ -174,8 +174,8 @@ class ItemFormViewModelTest {
fakeItemRepository.addItem( fakeItemRepository.addItem(
ItemEntity( ItemEntity(
id = "prev-1", name = "Alt", categoryId = 1, quantity = 1.0, id = "prev-1", name = "Alt", categoryId = 1, quantity = 1.0,
unit = "kg", unitPrice = 0.0, kcalPer100g = null, expiryDate = null, unit = "kg", unitPrice = 0.0, kcalPerKg = null, expiryDate = null,
locationId = 99, minStock = 0.0, notes = "", lastUpdated = 1000L locationId = 99, notes = "", lastUpdated = 1000L
) )
) )
fakeLocationRepository.emit( fakeLocationRepository.emit(
@ -205,10 +205,9 @@ class ItemFormViewModelTest {
quantity = 5.0, quantity = 5.0,
unit = "Stk", unit = "Stk",
unitPrice = 2.5, unitPrice = 2.5,
kcalPer100g = 120, kcalPerKg = 120,
expiryDate = LocalDate.of(2026, 12, 31), expiryDate = LocalDate.of(2026, 12, 31),
locationId = 2, locationId = 2,
minStock = 2.0,
notes = "Bohnen", notes = "Bohnen",
lastUpdated = 1000L lastUpdated = 1000L
) )
@ -227,10 +226,9 @@ class ItemFormViewModelTest {
assertEquals("5", state.quantity) assertEquals("5", state.quantity)
assertEquals("Stk", state.unit) assertEquals("Stk", state.unit)
assertEquals("2.5", state.unitPrice) assertEquals("2.5", state.unitPrice)
assertEquals("120", state.kcalPer100g) assertEquals("120", state.kcalPerKg)
assertEquals(LocalDate.of(2026, 12, 31), state.expiryDate) assertEquals(LocalDate.of(2026, 12, 31), state.expiryDate)
assertEquals(2, state.locationId) assertEquals(2, state.locationId)
assertEquals("2", state.minStock)
assertEquals("Bohnen", state.notes) assertEquals("Bohnen", state.notes)
} }
@ -463,10 +461,9 @@ class ItemFormViewModelTest {
quantity = 2.0, quantity = 2.0,
unit = "Stk", unit = "Stk",
unitPrice = 0.0, unitPrice = 0.0,
kcalPer100g = null, kcalPerKg = null,
expiryDate = null, expiryDate = null,
locationId = 1, locationId = 1,
minStock = 0.0,
notes = "", notes = "",
lastUpdated = 0L lastUpdated = 0L
) )

View file

@ -230,10 +230,9 @@ private fun buildItemEntity(
quantity = 2.0, quantity = 2.0,
unit = "Stk", unit = "Stk",
unitPrice = 1.5, unitPrice = 1.5,
kcalPer100g = null, kcalPerKg = null,
expiryDate = null, expiryDate = null,
locationId = locationId, locationId = locationId,
minStock = 1.0,
notes = "", notes = "",
lastUpdated = 0L lastUpdated = 0L
) )

View file

@ -292,8 +292,8 @@ class LocationListViewModelTest {
fakeItemRepository.addItem( fakeItemRepository.addItem(
ItemEntity( ItemEntity(
id = "i1", name = "Reis", categoryId = 1, quantity = 1.0, id = "i1", name = "Reis", categoryId = 1, quantity = 1.0,
unit = "kg", unitPrice = 0.0, kcalPer100g = null, expiryDate = null, unit = "kg", unitPrice = 0.0, kcalPerKg = null, expiryDate = null,
locationId = 1, minStock = 0.0, notes = "", lastUpdated = 0L locationId = 1, notes = "", lastUpdated = 0L
) )
) )
val location = LocationEntity(id = 1, name = "Keller") val location = LocationEntity(id = 1, name = "Keller")
@ -317,8 +317,8 @@ class LocationListViewModelTest {
fakeItemRepository.addItem( fakeItemRepository.addItem(
ItemEntity( ItemEntity(
id = "i1", name = "Reis", categoryId = 1, quantity = 1.0, id = "i1", name = "Reis", categoryId = 1, quantity = 1.0,
unit = "kg", unitPrice = 0.0, kcalPer100g = null, expiryDate = null, unit = "kg", unitPrice = 0.0, kcalPerKg = null, expiryDate = null,
locationId = 1, minStock = 0.0, notes = "", lastUpdated = 0L locationId = 1, notes = "", lastUpdated = 0L
) )
) )
val location = LocationEntity(id = 1, name = "Keller") val location = LocationEntity(id = 1, name = "Keller")
@ -344,8 +344,8 @@ class LocationListViewModelTest {
fakeItemRepository.addItem( fakeItemRepository.addItem(
ItemEntity( ItemEntity(
id = "i1", name = "Wasser", categoryId = 1, quantity = 1.0, id = "i1", name = "Wasser", categoryId = 1, quantity = 1.0,
unit = "Flasche", unitPrice = 0.0, kcalPer100g = null, expiryDate = null, unit = "Flasche", unitPrice = 0.0, kcalPerKg = null, expiryDate = null,
locationId = 1, minStock = 0.0, notes = "", lastUpdated = 0L locationId = 1, notes = "", lastUpdated = 0L
) )
) )
viewModel.showDeleteDialog(LocationEntity(id = 1, name = "Keller")) viewModel.showDeleteDialog(LocationEntity(id = 1, name = "Keller"))

View file

@ -3,7 +3,6 @@ package de.krisenvorrat.app.ui.warnings
import de.krisenvorrat.app.data.db.entity.ItemEntity import de.krisenvorrat.app.data.db.entity.ItemEntity
import de.krisenvorrat.app.domain.repository.ItemRepository import de.krisenvorrat.app.domain.repository.ItemRepository
import de.krisenvorrat.app.domain.usecase.GetExpiryWarningsUseCase import de.krisenvorrat.app.domain.usecase.GetExpiryWarningsUseCase
import de.krisenvorrat.app.domain.usecase.GetMinStockWarningsUseCase
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@ -41,8 +40,7 @@ class WarningsViewModelTest {
private fun createViewModel() = WarningsViewModel( private fun createViewModel() = WarningsViewModel(
itemRepository = fakeItemRepository, itemRepository = fakeItemRepository,
getExpiryWarnings = GetExpiryWarningsUseCase(), getExpiryWarnings = GetExpiryWarningsUseCase()
getMinStockWarnings = GetMinStockWarningsUseCase()
) )
@Test @Test
@ -57,7 +55,6 @@ class WarningsViewModelTest {
val state = viewModel.uiState.value val state = viewModel.uiState.value
assertFalse(state.isLoading) assertFalse(state.isLoading)
assertFalse(state.hasExpiryWarnings) assertFalse(state.hasExpiryWarnings)
assertFalse(state.hasMinStockWarnings)
assertFalse(state.hasWarnings) assertFalse(state.hasWarnings)
assertEquals(0, state.totalWarningCount) assertEquals(0, state.totalWarningCount)
} }
@ -82,48 +79,6 @@ class WarningsViewModelTest {
assertEquals(1, state.totalWarningCount) assertEquals(1, state.totalWarningCount)
} }
@Test
fun test_init_withMinStockViolation_minStockWarningsArePresent() = runTest(testDispatcher) {
// Given item with quantity 1 but minStock 5
fakeItemRepository.emit(
listOf(buildTestItem(id = "a", quantity = 1.0, minStock = 5.0))
)
viewModel = createViewModel()
// When
advanceUntilIdle()
// Then
val state = viewModel.uiState.value
assertTrue(state.hasMinStockWarnings)
assertEquals(1, state.minStockWarnings.size)
assertEquals(4.0, state.minStockWarnings.first().deficit, 0.001)
assertTrue(state.hasWarnings)
assertEquals(1, state.totalWarningCount)
}
@Test
fun test_init_withBothWarnings_totalCountIsCombined() = runTest(testDispatcher) {
// Given one expiring item and one below min stock
val expiryDate = LocalDate.now().plusMonths(3)
fakeItemRepository.emit(
listOf(
buildTestItem(id = "a", expiryDate = expiryDate),
buildTestItem(id = "b", quantity = 1.0, minStock = 5.0)
)
)
viewModel = createViewModel()
// When
advanceUntilIdle()
// Then
val state = viewModel.uiState.value
assertTrue(state.hasExpiryWarnings)
assertTrue(state.hasMinStockWarnings)
assertEquals(2, state.totalWarningCount)
}
@Test @Test
fun test_init_withFarExpiryDate_noExpiryWarnings() = runTest(testDispatcher) { fun test_init_withFarExpiryDate_noExpiryWarnings() = runTest(testDispatcher) {
// Given item expiring in 2 years (beyond WARNING_MONTHS=12) // Given item expiring in 2 years (beyond WARNING_MONTHS=12)
@ -140,21 +95,6 @@ class WarningsViewModelTest {
assertFalse(viewModel.uiState.value.hasExpiryWarnings) assertFalse(viewModel.uiState.value.hasExpiryWarnings)
} }
@Test
fun test_init_withSufficientStock_noMinStockWarnings() = runTest(testDispatcher) {
// Given item with quantity above minStock
fakeItemRepository.emit(
listOf(buildTestItem(id = "a", quantity = 10.0, minStock = 5.0))
)
viewModel = createViewModel()
// When
advanceUntilIdle()
// Then
assertFalse(viewModel.uiState.value.hasMinStockWarnings)
}
@Test @Test
fun test_reactiveUpdate_whenItemsChange_stateUpdates() = runTest(testDispatcher) { fun test_reactiveUpdate_whenItemsChange_stateUpdates() = runTest(testDispatcher) {
// Given start with no warnings // Given start with no warnings
@ -162,14 +102,15 @@ class WarningsViewModelTest {
advanceUntilIdle() advanceUntilIdle()
assertFalse(viewModel.uiState.value.hasWarnings) assertFalse(viewModel.uiState.value.hasWarnings)
// When an item below min stock appears // When an expiring item appears
val expiryDate = LocalDate.now().plusMonths(3)
fakeItemRepository.emit( fakeItemRepository.emit(
listOf(buildTestItem(id = "a", quantity = 1.0, minStock = 5.0)) listOf(buildTestItem(id = "a", expiryDate = expiryDate))
) )
advanceUntilIdle() advanceUntilIdle()
// Then // Then
assertTrue(viewModel.uiState.value.hasMinStockWarnings) assertTrue(viewModel.uiState.value.hasExpiryWarnings)
assertEquals(1, viewModel.uiState.value.totalWarningCount) assertEquals(1, viewModel.uiState.value.totalWarningCount)
} }
} }
@ -183,10 +124,9 @@ private fun buildTestItem(
quantity: Double = 1.0, quantity: Double = 1.0,
unit: String = "Stk", unit: String = "Stk",
unitPrice: Double = 0.0, unitPrice: Double = 0.0,
kcalPer100g: Int? = null, kcalPerKg: Int? = null,
expiryDate: LocalDate? = null, expiryDate: LocalDate? = null,
locationId: Int = 1, locationId: Int = 1
minStock: Double = 0.0
) = ItemEntity( ) = ItemEntity(
id = id, id = id,
name = name, name = name,
@ -194,10 +134,9 @@ private fun buildTestItem(
quantity = quantity, quantity = quantity,
unit = unit, unit = unit,
unitPrice = unitPrice, unitPrice = unitPrice,
kcalPer100g = kcalPer100g, kcalPerKg = kcalPerKg,
expiryDate = expiryDate, expiryDate = expiryDate,
locationId = locationId, locationId = locationId,
minStock = minStock,
notes = "", notes = "",
lastUpdated = 0L lastUpdated = 0L
) )

View file

@ -23,10 +23,9 @@ internal object Items : Table("items") {
val quantity = double("quantity") val quantity = double("quantity")
val unit = varchar("unit", 50) val unit = varchar("unit", 50)
val unitPrice = double("unit_price") val unitPrice = double("unit_price")
val kcalPer100g = integer("kcal_per_100g").nullable() val kcalPerKg = integer("kcal_per_kg").nullable()
val expiryDate = varchar("expiry_date", 10).nullable() val expiryDate = varchar("expiry_date", 10).nullable()
val locationId = integer("location_id").references(Locations.id) val locationId = integer("location_id").references(Locations.id)
val minStock = double("min_stock")
val notes = text("notes") val notes = text("notes")
val lastUpdated = long("last_updated") val lastUpdated = long("last_updated")

View file

@ -45,10 +45,9 @@ internal class InventoryRepository {
it[quantity] = item.quantity it[quantity] = item.quantity
it[unit] = item.unit it[unit] = item.unit
it[unitPrice] = item.unitPrice it[unitPrice] = item.unitPrice
it[kcalPer100g] = item.kcalPer100g it[kcalPerKg] = item.kcalPerKg
it[expiryDate] = item.expiryDate it[expiryDate] = item.expiryDate
it[locationId] = item.locationId it[locationId] = item.locationId
it[minStock] = item.minStock
it[notes] = item.notes it[notes] = item.notes
it[lastUpdated] = item.lastUpdated it[lastUpdated] = item.lastUpdated
} }
@ -87,10 +86,9 @@ internal class InventoryRepository {
quantity = it[Items.quantity], quantity = it[Items.quantity],
unit = it[Items.unit], unit = it[Items.unit],
unitPrice = it[Items.unitPrice], unitPrice = it[Items.unitPrice],
kcalPer100g = it[Items.kcalPer100g], kcalPerKg = it[Items.kcalPerKg],
expiryDate = it[Items.expiryDate], expiryDate = it[Items.expiryDate],
locationId = it[Items.locationId], locationId = it[Items.locationId],
minStock = it[Items.minStock],
notes = it[Items.notes], notes = it[Items.notes],
lastUpdated = it[Items.lastUpdated] lastUpdated = it[Items.lastUpdated]
) )

View file

@ -197,10 +197,9 @@ class ApplicationTest {
quantity = 5.0, quantity = 5.0,
unit = "Stück", unit = "Stück",
unitPrice = 3.99, unitPrice = 3.99,
kcalPer100g = 250, kcalPerKg = 250,
expiryDate = "2027-06-15", expiryDate = "2027-06-15",
locationId = 1, locationId = 1,
minStock = 2.0,
notes = "Vollkornbrot in der Dose", notes = "Vollkornbrot in der Dose",
lastUpdated = 1715000000L lastUpdated = 1715000000L
) )

View file

@ -102,10 +102,9 @@ class EndToEndSyncTest {
quantity = 3.0, quantity = 3.0,
unit = "Stück", unit = "Stück",
unitPrice = 1.50, unitPrice = 1.50,
kcalPer100g = null, kcalPerKg = null,
expiryDate = null, expiryDate = null,
locationId = 10, locationId = 10,
minStock = 1.0,
notes = "", notes = "",
lastUpdated = 1715100000L lastUpdated = 1715100000L
) )
@ -174,10 +173,9 @@ class EndToEndSyncTest {
quantity = 10.0, quantity = 10.0,
unit = "Stück", unit = "Stück",
unitPrice = 3.99, unitPrice = 3.99,
kcalPer100g = 250, kcalPerKg = 250,
expiryDate = "2027-06-15", expiryDate = "2027-06-15",
locationId = 1, locationId = 1,
minStock = 2.0,
notes = "Menge erhöht", notes = "Menge erhöht",
lastUpdated = 1715200000L lastUpdated = 1715200000L
) )
@ -245,10 +243,9 @@ class EndToEndSyncTest {
quantity = 5.0, quantity = 5.0,
unit = "Stück", unit = "Stück",
unitPrice = 3.99, unitPrice = 3.99,
kcalPer100g = 250, kcalPerKg = 250,
expiryDate = "2027-06-15", expiryDate = "2027-06-15",
locationId = 1, locationId = 1,
minStock = 2.0,
notes = "Vollkornbrot in der Dose", notes = "Vollkornbrot in der Dose",
lastUpdated = 1715000000L lastUpdated = 1715000000L
), ),
@ -259,10 +256,9 @@ class EndToEndSyncTest {
quantity = 24.0, quantity = 24.0,
unit = "Liter", unit = "Liter",
unitPrice = 0.49, unitPrice = 0.49,
kcalPer100g = 0, kcalPerKg = 0,
expiryDate = "2028-01-01", expiryDate = "2028-01-01",
locationId = 2, locationId = 2,
minStock = 12.0,
notes = "Stilles Wasser", notes = "Stilles Wasser",
lastUpdated = 1715000000L lastUpdated = 1715000000L
) )

View file

@ -66,10 +66,9 @@ class InventoryRepositoryTest {
assertEquals(5.0, item.quantity, 0.001) assertEquals(5.0, item.quantity, 0.001)
assertEquals("Stück", item.unit) assertEquals("Stück", item.unit)
assertEquals(3.99, item.unitPrice, 0.001) assertEquals(3.99, item.unitPrice, 0.001)
assertEquals(250, item.kcalPer100g) assertEquals(250, item.kcalPerKg)
assertEquals("2027-06-15", item.expiryDate) assertEquals("2027-06-15", item.expiryDate)
assertEquals(1, item.locationId) assertEquals(1, item.locationId)
assertEquals(2.0, item.minStock, 0.001)
assertEquals("Vollkornbrot in der Dose", item.notes) assertEquals("Vollkornbrot in der Dose", item.notes)
assertEquals(1715000000L, item.lastUpdated) assertEquals(1715000000L, item.lastUpdated)
@ -118,10 +117,9 @@ class InventoryRepositoryTest {
quantity = 10.0, quantity = 10.0,
unit = "Stück", unit = "Stück",
unitPrice = 1.50, unitPrice = 1.50,
kcalPer100g = null, kcalPerKg = null,
expiryDate = null, expiryDate = null,
locationId = 1, locationId = 1,
minStock = 5.0,
notes = "", notes = "",
lastUpdated = 1715000000L lastUpdated = 1715000000L
) )
@ -135,7 +133,7 @@ class InventoryRepositoryTest {
// Then // Then
val item = result.items[0] val item = result.items[0]
assertNull(item.kcalPer100g) assertNull(item.kcalPerKg)
assertNull(item.expiryDate) assertNull(item.expiryDate)
} }
@ -156,10 +154,9 @@ class InventoryRepositoryTest {
quantity = 5.0, quantity = 5.0,
unit = "Stück", unit = "Stück",
unitPrice = 3.99, unitPrice = 3.99,
kcalPer100g = 250, kcalPerKg = 250,
expiryDate = "2027-06-15", expiryDate = "2027-06-15",
locationId = 1, locationId = 1,
minStock = 2.0,
notes = "Vollkornbrot in der Dose", notes = "Vollkornbrot in der Dose",
lastUpdated = 1715000000L lastUpdated = 1715000000L
) )

View file

@ -10,10 +10,9 @@ data class ItemDto(
val quantity: Double, val quantity: Double,
val unit: String, val unit: String,
val unitPrice: Double, val unitPrice: Double,
val kcalPer100g: Int?, val kcalPerKg: Int?,
val expiryDate: String?, val expiryDate: String?,
val locationId: Int, val locationId: Int,
val minStock: Double,
val notes: String, val notes: String,
val lastUpdated: Long val lastUpdated: Long
) )