refactor: kcalPerKg -> kcalPerUnit (kcal pro Einheit)

- ItemEntity, ItemDto: kcalPerKg -> kcalPerUnit (kcal_per_unit)
- Room DB: version 4 -> 5, MIGRATION_4_5 hinzugefuegt
- CalculateSupplyRangeUseCase: Berechnung vereinfacht zu
  quantity * kcalPerUnit (keine Einheitenumrechnung mehr noetig,
  alle Einheiten unterstuetzt)
- ItemFormScreen: Label 'kcal / kg' -> 'kcal / Einheit'
- CsvExporter: Header 'kcal/kg' -> 'kcal/Einheit'
- OpenAiVisionService: Prompt-JSON-Feld angepasst
- Server Tables + InventoryRepository: kcal_per_unit
- Alle Tests aktualisiert und gruen
This commit is contained in:
Jens Reinemann 2026-05-17 11:29:39 +02:00
parent db2fc5dea1
commit 5e9c072b51
44 changed files with 528 additions and 172 deletions

View file

@ -0,0 +1,314 @@
{
"formatVersion": 1,
"database": {
"version": 5,
"identityHash": "94ca0ddef5eb5333c781b3f97eff9c85",
"entities": [
{
"tableName": "categories",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "locations",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "items",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `category_id` INTEGER NOT NULL, `quantity` REAL NOT NULL, `unit` TEXT NOT NULL, `unit_price` REAL NOT NULL, `kcal_per_unit` INTEGER, `expiry_date` TEXT, `location_id` INTEGER NOT NULL, `notes` TEXT NOT NULL, `last_updated` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`category_id`) REFERENCES `categories`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`location_id`) REFERENCES `locations`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "categoryId",
"columnName": "category_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "quantity",
"columnName": "quantity",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "unit",
"columnName": "unit",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "unitPrice",
"columnName": "unit_price",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "kcalPerUnit",
"columnName": "kcal_per_unit",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "expiryDate",
"columnName": "expiry_date",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "locationId",
"columnName": "location_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "notes",
"columnName": "notes",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "lastUpdated",
"columnName": "last_updated",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_items_category_id",
"unique": false,
"columnNames": [
"category_id"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_items_category_id` ON `${TABLE_NAME}` (`category_id`)"
},
{
"name": "index_items_location_id",
"unique": false,
"columnNames": [
"location_id"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_items_location_id` ON `${TABLE_NAME}` (`location_id`)"
}
],
"foreignKeys": [
{
"table": "categories",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"category_id"
],
"referencedColumns": [
"id"
]
},
{
"table": "locations",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"location_id"
],
"referencedColumns": [
"id"
]
}
]
},
{
"tableName": "settings",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` TEXT NOT NULL, PRIMARY KEY(`key`))",
"fields": [
{
"fieldPath": "key",
"columnName": "key",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "value",
"columnName": "value",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"key"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "pending_sync_ops",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `item_id` TEXT NOT NULL, `operation` TEXT NOT NULL, `payload` TEXT NOT NULL, `created_at` INTEGER NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "itemId",
"columnName": "item_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "operation",
"columnName": "operation",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "payload",
"columnName": "payload",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "createdAt",
"columnName": "created_at",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "messages",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `sender_id` TEXT NOT NULL, `sender_username` TEXT NOT NULL, `receiver_id` TEXT NOT NULL, `body` TEXT NOT NULL, `sent_at` INTEGER NOT NULL, `is_pending` INTEGER NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "senderId",
"columnName": "sender_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "senderUsername",
"columnName": "sender_username",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "receiverId",
"columnName": "receiver_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "body",
"columnName": "body",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "sentAt",
"columnName": "sent_at",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isPending",
"columnName": "is_pending",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '94ca0ddef5eb5333c781b3f97eff9c85')"
]
}
}

View file

@ -107,7 +107,7 @@ internal class KrisenvorratDatabaseMigrationTest {
context,
KrisenvorratDatabase::class.java,
dbName
).addMigrations(Migrations.MIGRATION_1_2, Migrations.MIGRATION_2_3, Migrations.MIGRATION_3_4).build()
).addMigrations(Migrations.MIGRATION_1_2, Migrations.MIGRATION_2_3, Migrations.MIGRATION_3_4, Migrations.MIGRATION_4_5).build()
private fun createV2Database() {
val dbFile = context.getDatabasePath(dbName).also { it.parentFile?.mkdirs() }
@ -150,13 +150,13 @@ internal class KrisenvorratDatabaseMigrationTest {
context,
KrisenvorratDatabase::class.java,
dbName
).addMigrations(Migrations.MIGRATION_2_3, Migrations.MIGRATION_3_4).build()
).addMigrations(Migrations.MIGRATION_2_3, Migrations.MIGRATION_3_4, Migrations.MIGRATION_4_5).build()
private fun openMigratedDbV4() = Room.databaseBuilder(
context,
KrisenvorratDatabase::class.java,
dbName
).addMigrations(Migrations.MIGRATION_3_4).build()
).addMigrations(Migrations.MIGRATION_3_4, Migrations.MIGRATION_4_5).build()
private fun createV3Database() {
val dbFile = context.getDatabasePath(dbName).also { it.parentFile?.mkdirs() }
@ -223,7 +223,7 @@ internal class KrisenvorratDatabaseMigrationTest {
val item = items[0]
assertEquals("item-uuid-1", item.id)
assertEquals("Apfel", item.name)
assertEquals(52, item.kcalPerKg)
assertEquals(52, item.kcalPerUnit)
assertEquals("Testnotiz", item.notes)
assertEquals(5.0, item.quantity, 0.0)
} finally {
@ -246,7 +246,7 @@ internal class KrisenvorratDatabaseMigrationTest {
assertFalse("min_stock darf nicht mehr existieren", columns.contains("min_stock"))
assertFalse("kcal_per_100g darf nicht mehr existieren", columns.contains("kcal_per_100g"))
assertTrue("kcal_per_kg muss existieren", columns.contains("kcal_per_kg"))
assertTrue("kcal_per_unit muss existieren", columns.contains("kcal_per_unit"))
assertTrue("id muss existieren", columns.contains("id"))
assertTrue("name muss existieren", columns.contains("name"))
assertTrue("last_updated muss existieren", columns.contains("last_updated"))
@ -285,7 +285,7 @@ internal class KrisenvorratDatabaseMigrationTest {
fun freshInstall_worksWithoutMigration() {
// Fresh-Install: Room.onCreate() läuft direkt, keine Migration nötig
val db = Room.inMemoryDatabaseBuilder(context, KrisenvorratDatabase::class.java)
.addMigrations(Migrations.MIGRATION_1_2, Migrations.MIGRATION_2_3, Migrations.MIGRATION_3_4)
.addMigrations(Migrations.MIGRATION_1_2, Migrations.MIGRATION_2_3, Migrations.MIGRATION_3_4, Migrations.MIGRATION_4_5)
.build()
try {
// Tabellen anlegen und Basis-Operationen prüfen
@ -397,7 +397,7 @@ internal class KrisenvorratDatabaseMigrationTest {
val item = items[0]
assertEquals("item-uuid-1", item.id)
assertEquals("Apfel", item.name)
assertEquals(52, item.kcalPerKg)
assertEquals(52, item.kcalPerUnit)
// Alle Tabellen existieren
val tables = mutableListOf<String>()

View file

@ -18,7 +18,7 @@ import de.krisenvorrat.app.data.db.entity.SettingsEntity
@Database(
entities = [CategoryEntity::class, LocationEntity::class, ItemEntity::class, SettingsEntity::class, PendingSyncOpEntity::class, MessageEntity::class],
version = 4,
version = 5,
exportSchema = true
)
@TypeConverters(LocalDateConverter::class)

View file

@ -112,4 +112,57 @@ internal object Migrations {
)
}
}
/**
* V4 V5:
* - `kcal_per_kg` umbenannt in `kcal_per_unit`
*
* SQLite unterstützt erst ab 3.25 (API 29) ALTER TABLE RENAME COLUMN.
* Da minSdk = 26, wird die Tabelle neu erstellt und die Daten kopiert.
*/
val MIGRATION_4_5 = object : Migration(4, 5) {
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL(
"""
CREATE TABLE `items_new` (
`id` TEXT NOT NULL,
`name` TEXT NOT NULL,
`category_id` INTEGER NOT NULL,
`quantity` REAL NOT NULL,
`unit` TEXT NOT NULL,
`unit_price` REAL NOT NULL,
`kcal_per_unit` INTEGER,
`expiry_date` TEXT,
`location_id` INTEGER NOT NULL,
`notes` TEXT NOT NULL,
`last_updated` INTEGER NOT NULL,
PRIMARY KEY(`id`),
FOREIGN KEY(`category_id`) REFERENCES `categories`(`id`)
ON UPDATE NO ACTION ON DELETE CASCADE,
FOREIGN KEY(`location_id`) REFERENCES `locations`(`id`)
ON UPDATE NO ACTION ON DELETE CASCADE
)
""".trimIndent()
)
db.execSQL(
"""
INSERT INTO `items_new`
(id, name, category_id, quantity, unit, unit_price,
kcal_per_unit, expiry_date, location_id, notes, last_updated)
SELECT
id, name, category_id, quantity, unit, unit_price,
kcal_per_kg, expiry_date, location_id, notes, last_updated
FROM `items`
""".trimIndent()
)
db.execSQL("DROP TABLE `items`")
db.execSQL("ALTER TABLE `items_new` RENAME TO `items`")
db.execSQL(
"CREATE INDEX IF NOT EXISTS `index_items_category_id` ON `items` (`category_id`)"
)
db.execSQL(
"CREATE INDEX IF NOT EXISTS `index_items_location_id` ON `items` (`location_id`)"
)
}
}
}

View file

@ -32,7 +32,7 @@ internal data class ItemEntity(
@ColumnInfo(name = "quantity") val quantity: Double,
@ColumnInfo(name = "unit") val unit: String,
@ColumnInfo(name = "unit_price") val unitPrice: Double,
@ColumnInfo(name = "kcal_per_kg") val kcalPerKg: Int?,
@ColumnInfo(name = "kcal_per_unit") val kcalPerUnit: Int?,
@ColumnInfo(name = "expiry_date") val expiryDate: LocalDate?,
@ColumnInfo(name = "location_id") val locationId: Int,
@ColumnInfo(name = "notes") val notes: String,

View file

@ -14,7 +14,7 @@ internal object CsvExporter {
private val HEADER = listOf(
"Name", "Kategorie", "Menge", "Einheit", "Stückpreis",
"kcal/kg", "MHD", "Lagerort", "Notizen"
"kcal/Einheit", "MHD", "Lagerort", "Notizen"
)
fun export(
@ -36,7 +36,7 @@ internal object CsvExporter {
formatQuantity(item.quantity),
escapeCsv(item.unit),
formatPrice(item.unitPrice),
item.kcalPerKg?.toString() ?: "",
item.kcalPerUnit?.toString() ?: "",
item.expiryDate?.format(DATE_FORMATTER) ?: "",
escapeCsv(locationMap[item.locationId] ?: ""),
escapeCsv(item.notes)

View file

@ -67,7 +67,7 @@ internal class ImportExportRepositoryImpl @Inject constructor(
quantity = item.quantity,
unit = item.unit,
unitPrice = item.unitPrice,
kcalPerKg = item.kcalPerKg,
kcalPerUnit = item.kcalPerUnit,
expiryDate = item.expiryDate?.toString(),
locationId = item.locationId,
notes = item.notes,
@ -112,7 +112,7 @@ internal class ImportExportRepositoryImpl @Inject constructor(
quantity = item.quantity,
unit = item.unit,
unitPrice = item.unitPrice,
kcalPerKg = item.kcalPerKg,
kcalPerUnit = item.kcalPerUnit,
expiryDate = item.expiryDate?.let { LocalDate.parse(it) },
locationId = item.locationId,
notes = item.notes,

View file

@ -92,7 +92,7 @@ internal class OpenAiVisionServiceImpl @Inject constructor(
"Erkenne alle Nahrungsmittel und Produkte in diesem Foto. " +
"Antworte ausschließlich im JSON-Format: " +
"{\"items\": [{\"name\": \"Produktname\", \"suggestedCategoryName\": \"Kategoriename\", " +
"\"unit\": \"Einheit\", \"kcalPerKg\": <Int oder null>, \"notes\": \"Zusatzinfos\"}]}. " +
"\"unit\": \"Einheit\", \"kcalPerUnit\": <Int oder null>, \"notes\": \"Zusatzinfos\"}]}. " +
"Verwende deutsche Produktnamen."
}
}

View file

@ -155,7 +155,7 @@ internal class ItemRepositoryImpl @Inject constructor(
quantity = quantity,
unit = unit,
unitPrice = unitPrice,
kcalPerKg = kcalPerKg,
kcalPerUnit = kcalPerUnit,
expiryDate = expiryDate?.toString(),
locationId = locationId,
notes = notes,

View file

@ -30,7 +30,7 @@ internal object DatabaseModule {
fun provideDatabase(@ApplicationContext context: Context): KrisenvorratDatabase =
Room.databaseBuilder(context, KrisenvorratDatabase::class.java, "krisenvorrat.db")
.addCallback(DefaultDataCallback)
.addMigrations(Migrations.MIGRATION_1_2, Migrations.MIGRATION_2_3, Migrations.MIGRATION_3_4)
.addMigrations(Migrations.MIGRATION_1_2, Migrations.MIGRATION_2_3, Migrations.MIGRATION_3_4, Migrations.MIGRATION_4_5)
.build()
private object DefaultDataCallback : RoomDatabase.Callback() {

View file

@ -7,6 +7,6 @@ internal data class ItemFormPrefill(
val name: String = "",
val suggestedCategoryName: String = "",
val unit: String = "",
val kcalPerKg: Int? = null,
val kcalPerUnit: Int? = null,
val notes: String = ""
)

View file

@ -19,20 +19,10 @@ internal class CalculateSupplyRangeUseCase @Inject constructor() {
if (dailyNeed <= 0) return 0.0
val totalKcal = items.sumOf { item ->
val kcalPerKg = item.kcalPerKg ?: return@sumOf 0.0
val grams = convertToGrams(item.quantity, item.unit) ?: return@sumOf 0.0
(grams / 1000.0) * kcalPerKg
val kcalPerUnit = item.kcalPerUnit ?: return@sumOf 0.0
item.quantity * kcalPerUnit
}
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
}
}
}

View file

@ -179,9 +179,9 @@ internal fun ItemFormScreen(
// kcal/kg
OutlinedTextField(
value = uiState.kcalPerKg,
onValueChange = viewModel::updateKcalPerKg,
label = { Text("kcal / kg") },
value = uiState.kcalPerUnit,
onValueChange = viewModel::updateKcalPerUnit,
label = { Text("kcal / Einheit") },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
singleLine = true,
modifier = Modifier.fillMaxWidth()

View file

@ -29,7 +29,7 @@ internal data class ItemFormUiState(
val quantity: String = "",
val unit: String = "",
val unitPrice: String = "",
val kcalPerKg: String = "",
val kcalPerUnit: String = "",
val expiryDate: LocalDate? = null,
val locationId: Int? = null,
val notes: String = "",
@ -115,7 +115,7 @@ internal class ItemFormViewModel @Inject constructor(
state.copy(
name = prefill.name,
unit = prefill.unit,
kcalPerKg = prefill.kcalPerKg?.toString() ?: "",
kcalPerUnit = prefill.kcalPerUnit?.toString() ?: "",
notes = prefill.notes
)
}
@ -142,7 +142,7 @@ internal class ItemFormViewModel @Inject constructor(
quantity = item.quantity.toBigDecimal().stripTrailingZeros().toPlainString(),
unit = item.unit,
unitPrice = item.unitPrice.toBigDecimal().stripTrailingZeros().toPlainString(),
kcalPerKg = item.kcalPerKg?.toString() ?: "",
kcalPerUnit = item.kcalPerUnit?.toString() ?: "",
expiryDate = item.expiryDate,
locationId = item.locationId,
notes = item.notes,
@ -178,8 +178,8 @@ internal class ItemFormViewModel @Inject constructor(
_uiState.update { it.copy(unitPrice = value) }
}
fun updateKcalPerKg(value: String) {
_uiState.update { it.copy(kcalPerKg = value) }
fun updateKcalPerUnit(value: String) {
_uiState.update { it.copy(kcalPerUnit = value) }
}
fun updateExpiryDate(value: LocalDate?) {
@ -211,7 +211,7 @@ internal class ItemFormViewModel @Inject constructor(
quantity = state.quantity.toDouble(),
unit = state.unit.trim(),
unitPrice = state.unitPrice.toDoubleOrNull() ?: 0.0,
kcalPerKg = state.kcalPerKg.toIntOrNull(),
kcalPerUnit = state.kcalPerUnit.toIntOrNull(),
expiryDate = state.expiryDate,
locationId = state.locationId!!,
notes = state.notes.trim(),

View file

@ -178,7 +178,7 @@ internal class ItemListViewModel @Inject constructor(
quantity = item.quantity,
unit = item.unit,
unitPrice = 0.0,
kcalPerKg = null,
kcalPerUnit = null,
expiryDate = item.expiryDate,
locationId = 0,
notes = "",

View file

@ -20,7 +20,7 @@ class CsvExporterTest {
quantity = 5.0,
unit = "Stk",
unitPrice = 1.50,
kcalPerKg = 800,
kcalPerUnit = 800,
expiryDate = LocalDate.of(2027, 3, 15),
locationId = 1
)
@ -33,7 +33,7 @@ class CsvExporterTest {
assertTrue(csv.startsWith("\uFEFF"))
val lines = csv.lines().filter { it.isNotBlank() }
assertEquals(2, lines.size)
assertEquals("\uFEFFName;Kategorie;Menge;Einheit;Stückpreis;kcal/kg;MHD;Lagerort;Notizen", lines[0])
assertEquals("\uFEFFName;Kategorie;Menge;Einheit;Stückpreis;kcal/Einheit;MHD;Lagerort;Notizen", lines[0])
assertEquals("Konserve;Lebensmittel;5;Stk;1,50;800;15.03.2027;Keller;", lines[1])
}

View file

@ -68,7 +68,7 @@ class ImportExportRepositoryImplTest {
@Test
fun test_importFromJson_withValidJson_upsertAllEntities() = runBlocking {
// 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,"kcalPerKg":null,"expiryDate":null,"locationId":1,"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,"kcalPerUnit":null,"expiryDate":null,"locationId":1,"notes":"","lastUpdated":0}],"settings":[{"key":"theme","value":"dark"}]}"""
val categoryDao = FakeCategoryDao()
val locationDao = FakeLocationDao()
val itemDao = FakeItemDao()
@ -249,7 +249,7 @@ class ImportExportRepositoryImplTest {
val itemDao = FakeItemDao()
itemDao.upsertAll(listOf(buildItemEntity("item1").copy(name = "Lokal", lastUpdated = 1000L)))
val repository = buildRepository(itemDao = itemDao)
val json = """{"version":1,"categories":[],"locations":[],"items":[{"id":"item1","name":"VomServer","categoryId":1,"quantity":2.0,"unit":"Stk","unitPrice":1.5,"kcalPerKg":null,"expiryDate":null,"locationId":1,"notes":"","lastUpdated":500}],"settings":[]}"""
val json = """{"version":1,"categories":[],"locations":[],"items":[{"id":"item1","name":"VomServer","categoryId":1,"quantity":2.0,"unit":"Stk","unitPrice":1.5,"kcalPerUnit":null,"expiryDate":null,"locationId":1,"notes":"","lastUpdated":500}],"settings":[]}"""
// When
repository.importFromJson(json)
@ -264,7 +264,7 @@ class ImportExportRepositoryImplTest {
val itemDao = FakeItemDao()
itemDao.upsertAll(listOf(buildItemEntity("item2").copy(name = "Lokal", lastUpdated = 100L)))
val repository = buildRepository(itemDao = itemDao)
val json = """{"version":1,"categories":[],"locations":[],"items":[{"id":"item2","name":"VomServer","categoryId":1,"quantity":2.0,"unit":"Stk","unitPrice":1.5,"kcalPerKg":null,"expiryDate":null,"locationId":1,"notes":"","lastUpdated":2000}],"settings":[]}"""
val json = """{"version":1,"categories":[],"locations":[],"items":[{"id":"item2","name":"VomServer","categoryId":1,"quantity":2.0,"unit":"Stk","unitPrice":1.5,"kcalPerUnit":null,"expiryDate":null,"locationId":1,"notes":"","lastUpdated":2000}],"settings":[]}"""
// When
repository.importFromJson(json)
@ -300,7 +300,7 @@ class ImportExportRepositoryImplTest {
// Then
assertTrue(csv.startsWith("\uFEFF"))
assertTrue(csv.contains("Name;Kategorie;Menge;Einheit;Stückpreis;kcal/kg;MHD;Lagerort;Notizen"))
assertTrue(csv.contains("Name;Kategorie;Menge;Einheit;Stückpreis;kcal/Einheit;MHD;Lagerort;Notizen"))
assertTrue(csv.contains("Konserve;Lebensmittel;5;Stk;1,50;;15.03.2027;Keller;"))
}
@ -360,7 +360,7 @@ class ImportExportRepositoryImplTest {
}
@Test
fun test_exportToCsv_withKcalPerKg_includesValue() = runBlocking {
fun test_exportToCsv_withkcalPerUnit_includesValue() = runBlocking {
// Given
val categoryDao = FakeCategoryDao()
val locationDao = FakeLocationDao()
@ -368,7 +368,7 @@ class ImportExportRepositoryImplTest {
categoryDao.upsertAll(listOf(CategoryEntity(id = 1, name = "Lebensmittel")))
locationDao.upsertAll(listOf(LocationEntity(id = 1, name = "Keller")))
itemDao.upsertAll(listOf(
buildItemEntity("item1").copy(name = "Reis", kcalPerKg = 3500)
buildItemEntity("item1").copy(name = "Reis", kcalPerUnit = 3500)
))
val repository = buildRepository(categoryDao, locationDao, itemDao)

View file

@ -44,7 +44,7 @@ class JsonRoundtripTest {
quantity = 10.0,
unit = "Stk",
unitPrice = 2.49,
kcalPerKg = 180,
kcalPerUnit = 180,
expiryDate = LocalDate.of(2027, 6, 15),
locationId = 1,
notes = "Ravioli",
@ -57,7 +57,7 @@ class JsonRoundtripTest {
quantity = 3.0,
unit = "Stk",
unitPrice = 0.99,
kcalPerKg = null,
kcalPerUnit = null,
expiryDate = null,
locationId = 2,
notes = "",
@ -119,7 +119,7 @@ class JsonRoundtripTest {
assertEquals(original.quantity, imported?.quantity)
assertEquals(original.unit, imported?.unit)
assertEquals(original.unitPrice, imported?.unitPrice)
assertEquals(original.kcalPerKg, imported?.kcalPerKg)
assertEquals(original.kcalPerUnit, imported?.kcalPerUnit)
assertEquals(original.expiryDate, imported?.expiryDate)
assertEquals(original.locationId, imported?.locationId)
assertEquals(original.notes, imported?.notes)
@ -151,7 +151,7 @@ class JsonRoundtripTest {
quantity = 1.0,
unit = "Stk",
unitPrice = 0.0,
kcalPerKg = null,
kcalPerUnit = null,
expiryDate = null,
locationId = 1,
notes = "",
@ -172,7 +172,7 @@ class JsonRoundtripTest {
// Then
assertTrue(result.isSuccess)
val imported = importItemDao.getItems().first()
assertEquals(null, imported.kcalPerKg)
assertEquals(null, imported.kcalPerUnit)
assertEquals(null, imported.expiryDate)
}

View file

@ -126,7 +126,7 @@ internal fun buildItemEntity(id: String = "item1") = ItemEntity(
quantity = 2.0,
unit = "Stk",
unitPrice = 1.5,
kcalPerKg = null,
kcalPerUnit = null,
expiryDate = null,
locationId = 1,
notes = "",

View file

@ -186,7 +186,7 @@ private fun buildItem(
quantity = 2.0,
unit = "Stk",
unitPrice = 1.5,
kcalPerKg = null,
kcalPerUnit = null,
expiryDate = null,
locationId = locationId,
notes = "",
@ -405,7 +405,7 @@ class ItemRepositoryImplTest {
id = "op1",
itemId = "drain1",
operation = "PATCH",
payload = """{"id":"drain1","name":"Konserve","categoryId":1,"quantity":2.0,"unit":"Stk","unitPrice":1.5,"kcalPerKg":null,"expiryDate":null,"locationId":1,"notes":"","lastUpdated":0}""",
payload = """{"id":"drain1","name":"Konserve","categoryId":1,"quantity":2.0,"unit":"Stk","unitPrice":1.5,"kcalPerUnit":null,"expiryDate":null,"locationId":1,"notes":"","lastUpdated":0}""",
createdAt = 1000L
)
)

View file

@ -48,7 +48,7 @@ class SyncServiceImplTest {
quantity = 5.0,
unit = "Dose",
unitPrice = 1.29,
kcalPerKg = 100,
kcalPerUnit = 100,
expiryDate = "2027-06-01",
locationId = 1,
notes = "",

View file

@ -9,10 +9,10 @@ class CalculateSupplyRangeUseCaseTest {
@Test
fun test_invoke_withKgItems_returnsCorrectDays() {
// Given 2 kg Reis à 3500 kcal/kg = 7000 kcal
// Given 2 Packungen à 3500 kcal/Packung = 7000 kcal
// 4000 kcal/Tag (2 × 2000) → 1.75 Tage
val items = listOf(
buildTestItem(id = "1", quantity = 2.0, unit = "kg", kcalPerKg = 3500)
buildTestItem(id = "1", quantity = 2.0, unit = "Packung", kcalPerUnit = 3500)
)
// When
@ -24,10 +24,10 @@ class CalculateSupplyRangeUseCaseTest {
@Test
fun test_invoke_withGramItems_returnsCorrectDays() {
// Given 500 g Nudeln à 3600 kcal/kg = 1800 kcal
// Given 5 Stk à 360 kcal/Stk = 1800 kcal
// 2000 kcal/Tag → 0.9 Tage
val items = listOf(
buildTestItem(id = "1", quantity = 500.0, unit = "g", kcalPerKg = 3600)
buildTestItem(id = "1", quantity = 5.0, unit = "Stk", kcalPerUnit = 360)
)
// When
@ -39,11 +39,11 @@ class CalculateSupplyRangeUseCaseTest {
@Test
fun test_invoke_withMultipleItems_sumsTotalKcal() {
// Given 1 kg Reis (3500 kcal/kg = 3500 kcal) + 500 g Nudeln (3600 kcal/kg = 1800 kcal) = 5300 kcal
// Given 1 Packung (3500 kcal/Packung) + 5 Stk (360 kcal/Stk = 1800 kcal) = 5300 kcal
// 4000 kcal/Tag → 1.325 Tage
val items = listOf(
buildTestItem(id = "1", quantity = 1.0, unit = "kg", kcalPerKg = 3500),
buildTestItem(id = "2", quantity = 500.0, unit = "g", kcalPerKg = 3600)
buildTestItem(id = "1", quantity = 1.0, unit = "Packung", kcalPerUnit = 3500),
buildTestItem(id = "2", quantity = 5.0, unit = "Stk", kcalPerUnit = 360)
)
// When
@ -64,13 +64,13 @@ class CalculateSupplyRangeUseCaseTest {
@Test
fun test_invoke_withNullKcal_skipsItem() {
// Given Item ohne kcalPerKg wird ignoriert
// Given Item ohne kcalPerUnit wird ignoriert
val items = listOf(
buildTestItem(id = "1", quantity = 1.0, unit = "kg", kcalPerKg = null),
buildTestItem(id = "2", quantity = 1.0, unit = "kg", kcalPerKg = 2000)
buildTestItem(id = "1", quantity = 1.0, unit = "Stk", kcalPerUnit = null),
buildTestItem(id = "2", quantity = 1.0, unit = "Stk", kcalPerUnit = 2000)
)
// When Nur 1 kg à 2000 kcal/kg = 2000 kcal, 2000 kcal/Tag = 1.0 Tage
// When Nur 1 Stk à 2000 kcal/Stk = 2000 kcal, 2000 kcal/Tag = 1.0 Tage
val result = useCase(items, totalDailyKcal = 2000)
// Then
@ -78,24 +78,24 @@ class CalculateSupplyRangeUseCaseTest {
}
@Test
fun test_invoke_withNonWeightUnit_skipsItem() {
// Given "Stk" ist keine Gewichtseinheit → wird ignoriert
fun test_invoke_withAnyUnit_isIncluded() {
// Given Jede Einheit wird gewertet, kein Unit-Filter mehr
val items = listOf(
buildTestItem(id = "1", quantity = 5.0, unit = "Stk", kcalPerKg = 2000)
buildTestItem(id = "1", quantity = 5.0, unit = "Stk", kcalPerUnit = 2000)
)
// When
// When 5 × 2000 = 10000 kcal / 4000 = 2.5 Tage
val result = useCase(items, totalDailyKcal = 4000)
// Then
assertEquals(0.0, result, 0.001)
assertEquals(2.5, result, 0.001)
}
@Test
fun test_invoke_withZeroTotalDailyKcal_returnsZero() {
// Given
val items = listOf(
buildTestItem(id = "1", quantity = 1.0, unit = "kg", kcalPerKg = 3500)
buildTestItem(id = "1", quantity = 1.0, unit = "Stk", kcalPerUnit = 3500)
)
// When
@ -107,10 +107,10 @@ class CalculateSupplyRangeUseCaseTest {
@Test
fun test_invoke_withDefaultParameters_uses4000KcalPerDay() {
// Given 4 kg Reis à 3500 kcal/kg = 14000 kcal
// Given 4 Packungen à 3500 kcal/Packung = 14000 kcal
// Default: 4000 kcal/Tag → 3.5 Tage
val items = listOf(
buildTestItem(id = "1", quantity = 4.0, unit = "kg", kcalPerKg = 3500)
buildTestItem(id = "1", quantity = 4.0, unit = "Packung", kcalPerUnit = 3500)
)
// When
@ -121,11 +121,11 @@ class CalculateSupplyRangeUseCaseTest {
}
@Test
fun test_invoke_withMgUnit_convertsCorrectly() {
// Given 500000 mg = 500 g à 2000 kcal/kg = 1000 kcal
fun test_invoke_withVariousUnits_allCounted() {
// Given Beliebige Einheit: 5 Stk à 200 kcal/Stk = 1000 kcal
// 1000 kcal/Tag → 1.0 Tag
val items = listOf(
buildTestItem(id = "1", quantity = 500000.0, unit = "mg", kcalPerKg = 2000)
buildTestItem(id = "1", quantity = 5.0, unit = "Stk", kcalPerUnit = 200)
)
// When
@ -136,19 +136,18 @@ class CalculateSupplyRangeUseCaseTest {
}
@Test
fun test_invoke_withMixedUnits_onlyCountsWeightBased() {
// Given 1 kg (3500 kcal/kg) + 5 Stk (ignored) + 2 L (ignored)
// Total: 3500 kcal, 2000 kcal/Tag → 1.75 Tage
fun test_invoke_withMixedUnits_allCounted() {
// Given kg, Stk, L werden alle berücksichtigt
val items = listOf(
buildTestItem(id = "1", quantity = 1.0, unit = "kg", kcalPerKg = 3500),
buildTestItem(id = "2", quantity = 5.0, unit = "Stk", kcalPerKg = 1000),
buildTestItem(id = "3", quantity = 2.0, unit = "L", kcalPerKg = 450)
buildTestItem(id = "1", quantity = 1.0, unit = "kg", kcalPerUnit = 3500),
buildTestItem(id = "2", quantity = 5.0, unit = "Stk", kcalPerUnit = 1000),
buildTestItem(id = "3", quantity = 2.0, unit = "L", kcalPerUnit = 450)
)
// When
// When 3500 + 5000 + 900 = 9400 kcal / 2000 = 4.7 Tage
val result = useCase(items, totalDailyKcal = 2000)
// Then
assertEquals(1.75, result, 0.001)
assertEquals(4.7, result, 0.001)
}
}

View file

@ -10,7 +10,7 @@ internal fun buildTestItem(
quantity: Double = 1.0,
unit: String = "Stk",
unitPrice: Double = 0.0,
kcalPerKg: Int? = null,
kcalPerUnit: Int? = null,
expiryDate: LocalDate? = null,
locationId: Int = 1
) = ItemEntity(
@ -20,7 +20,7 @@ internal fun buildTestItem(
quantity = quantity,
unit = unit,
unitPrice = unitPrice,
kcalPerKg = kcalPerKg,
kcalPerUnit = kcalPerUnit,
expiryDate = expiryDate,
locationId = locationId,
notes = "",

View file

@ -293,7 +293,7 @@ class CategoryListViewModelTest {
fakeItemRepository.addItem(
ItemEntity(
id = "i1", name = "Reis", categoryId = 1, quantity = 1.0,
unit = "kg", unitPrice = 0.0, kcalPerKg = null, expiryDate = null,
unit = "kg", unitPrice = 0.0, kcalPerUnit = null, expiryDate = null,
locationId = 1, notes = "", lastUpdated = 0L
)
)
@ -318,7 +318,7 @@ class CategoryListViewModelTest {
fakeItemRepository.addItem(
ItemEntity(
id = "i1", name = "Reis", categoryId = 1, quantity = 1.0,
unit = "kg", unitPrice = 0.0, kcalPerKg = null, expiryDate = null,
unit = "kg", unitPrice = 0.0, kcalPerUnit = null, expiryDate = null,
locationId = 1, notes = "", lastUpdated = 0L
)
)
@ -345,7 +345,7 @@ class CategoryListViewModelTest {
fakeItemRepository.addItem(
ItemEntity(
id = "i1", name = "Reis", categoryId = 1, quantity = 1.0,
unit = "kg", unitPrice = 0.0, kcalPerKg = null, expiryDate = null,
unit = "kg", unitPrice = 0.0, kcalPerUnit = null, expiryDate = null,
locationId = 1, notes = "", lastUpdated = 0L
)
)

View file

@ -149,7 +149,7 @@ class DashboardViewModelTest {
)
fakeItemRepository.emit(
listOf(
buildTestItem(id = "a", quantity = 1000.0, unit = "g", kcalPerKg = 2000)
buildTestItem(id = "a", quantity = 1.0, unit = "Stk", kcalPerUnit = 2000)
)
)
viewModel = createViewModel()
@ -172,7 +172,7 @@ class DashboardViewModelTest {
allZeroAgeGroups.toJson()
)
fakeItemRepository.emit(
listOf(buildTestItem(id = "a", quantity = 1000.0, unit = "g", kcalPerKg = 4000))
listOf(buildTestItem(id = "a", quantity = 1.0, unit = "Stk", kcalPerUnit = 4000))
)
viewModel = createViewModel()
@ -247,7 +247,7 @@ private fun buildTestItem(
quantity: Double = 1.0,
unit: String = "Stk",
unitPrice: Double = 0.0,
kcalPerKg: Int? = null,
kcalPerUnit: Int? = null,
expiryDate: LocalDate? = null,
locationId: Int = 1
) = ItemEntity(
@ -257,7 +257,7 @@ private fun buildTestItem(
quantity = quantity,
unit = unit,
unitPrice = unitPrice,
kcalPerKg = kcalPerKg,
kcalPerUnit = kcalPerUnit,
expiryDate = expiryDate,
locationId = locationId,
notes = "",

View file

@ -152,7 +152,7 @@ class ItemFormViewModelTest {
fakeItemRepository.addItem(
ItemEntity(
id = "prev-1", name = "Alt", categoryId = 1, quantity = 1.0,
unit = "kg", unitPrice = 0.0, kcalPerKg = null, expiryDate = null,
unit = "kg", unitPrice = 0.0, kcalPerUnit = null, expiryDate = null,
locationId = 2, notes = "", lastUpdated = 1000L
)
)
@ -177,7 +177,7 @@ class ItemFormViewModelTest {
fakeItemRepository.addItem(
ItemEntity(
id = "prev-1", name = "Alt", categoryId = 1, quantity = 1.0,
unit = "kg", unitPrice = 0.0, kcalPerKg = null, expiryDate = null,
unit = "kg", unitPrice = 0.0, kcalPerUnit = null, expiryDate = null,
locationId = 99, notes = "", lastUpdated = 1000L
)
)
@ -208,7 +208,7 @@ class ItemFormViewModelTest {
quantity = 5.0,
unit = "Stk",
unitPrice = 2.5,
kcalPerKg = 120,
kcalPerUnit = 120,
expiryDate = LocalDate.of(2026, 12, 31),
locationId = 2,
notes = "Bohnen",
@ -229,7 +229,7 @@ class ItemFormViewModelTest {
assertEquals("5", state.quantity)
assertEquals("Stk", state.unit)
assertEquals("2.5", state.unitPrice)
assertEquals("120", state.kcalPerKg)
assertEquals("120", state.kcalPerUnit)
assertEquals(LocalDate.of(2026, 12, 31), state.expiryDate)
assertEquals(2, state.locationId)
assertEquals("Bohnen", state.notes)
@ -465,7 +465,7 @@ class ItemFormViewModelTest {
quantity = 2.0,
unit = "Stk",
unitPrice = 0.0,
kcalPerKg = null,
kcalPerUnit = null,
expiryDate = null,
locationId = 1,
notes = "",

View file

@ -562,7 +562,7 @@ private fun buildItemEntity(
quantity = 2.0,
unit = "Stk",
unitPrice = 1.5,
kcalPerKg = null,
kcalPerUnit = null,
expiryDate = expiryDate,
locationId = locationId,
notes = notes,

View file

@ -292,7 +292,7 @@ class LocationListViewModelTest {
fakeItemRepository.addItem(
ItemEntity(
id = "i1", name = "Reis", categoryId = 1, quantity = 1.0,
unit = "kg", unitPrice = 0.0, kcalPerKg = null, expiryDate = null,
unit = "kg", unitPrice = 0.0, kcalPerUnit = null, expiryDate = null,
locationId = 1, notes = "", lastUpdated = 0L
)
)
@ -317,7 +317,7 @@ class LocationListViewModelTest {
fakeItemRepository.addItem(
ItemEntity(
id = "i1", name = "Reis", categoryId = 1, quantity = 1.0,
unit = "kg", unitPrice = 0.0, kcalPerKg = null, expiryDate = null,
unit = "kg", unitPrice = 0.0, kcalPerUnit = null, expiryDate = null,
locationId = 1, notes = "", lastUpdated = 0L
)
)
@ -344,7 +344,7 @@ class LocationListViewModelTest {
fakeItemRepository.addItem(
ItemEntity(
id = "i1", name = "Wasser", categoryId = 1, quantity = 1.0,
unit = "Flasche", unitPrice = 0.0, kcalPerKg = null, expiryDate = null,
unit = "Flasche", unitPrice = 0.0, kcalPerUnit = null, expiryDate = null,
locationId = 1, notes = "", lastUpdated = 0L
)
)

View file

@ -811,7 +811,7 @@ private class FakeSettingsRepository : SettingsRepository {
private class FakeImportExportRepository : ImportExportRepository {
var jsonResult = """{"version":1,"categories":[],"locations":[],"items":[],"settings":[]}"""
var markdownResult = "# Krisenvorrat Inventar\n"
var csvResult = "\uFEFFName;Kategorie;Menge;Einheit;Stückpreis;kcal/kg;MHD;Lagerort;Notizen\n"
var csvResult = "\uFEFFName;Kategorie;Menge;Einheit;Stückpreis;kcal/Einheit;MHD;Lagerort;Notizen\n"
var shouldThrow = false
var importShouldFail = false
var inventoryDto = InventoryDto(

View file

@ -124,7 +124,7 @@ private fun buildTestItem(
quantity: Double = 1.0,
unit: String = "Stk",
unitPrice: Double = 0.0,
kcalPerKg: Int? = null,
kcalPerUnit: Int? = null,
expiryDate: LocalDate? = null,
locationId: Int = 1
) = ItemEntity(
@ -134,7 +134,7 @@ private fun buildTestItem(
quantity = quantity,
unit = unit,
unitPrice = unitPrice,
kcalPerKg = kcalPerKg,
kcalPerUnit = kcalPerUnit,
expiryDate = expiryDate,
locationId = locationId,
notes = "",

View file

@ -167,7 +167,7 @@ $testInventory = @{
quantity = 12.0
unit = "Stueck"
unitPrice = 1.5
kcalPerKg = $null
kcalPerUnit = $null
expiryDate = "2027-12-31"
locationId = 1
notes = "Integrationstest"
@ -200,7 +200,7 @@ if ($aliceTokens) {
$patch = @{
id = $itemId; name = "Testkonserve"; categoryId = 1
quantity = 24.0; unit = "Stueck"; unitPrice = 1.5
kcalPerKg = $null; expiryDate = "2027-12-31"; locationId = 1
kcalPerUnit = $null; expiryDate = "2027-12-31"; locationId = 1
notes = "Nach PATCH"; lastUpdated = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds()
}
Invoke-Api -Method PATCH -Path "/api/inventory/items/$itemId" -Body $patch -Token $aliceTokens.accessToken | Out-Null
@ -527,16 +527,16 @@ if ($bobTokens) {
@{ id = 11; name = "Badezimmer" }
)
items = @(
@{ id = $dosenBrotId; name = "Dosenbrot"; categoryId = 10; quantity = 5.0; unit = "Stueck"; unitPrice = 2.0; kcalPerKg = $null; expiryDate = "2028-12-31"; locationId = 10; notes = ""; lastUpdated = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds() }
@{ id = $wasserId; name = "Mineralwasser"; categoryId = 11; quantity = 24.0; unit = "Stueck"; unitPrice = 0.5; kcalPerKg = $null; expiryDate = "2028-12-31"; locationId = 10; notes = ""; lastUpdated = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds() }
@{ id = $thunfischId; name = "Thunfisch"; categoryId = 10; quantity = 8.0; unit = "Dose"; unitPrice = 1.2; kcalPerKg = $null; expiryDate = "2028-06-30"; locationId = 10; notes = ""; lastUpdated = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds() }
@{ id = $nudelnId; name = "Nudeln"; categoryId = 10; quantity = 3.0; unit = "Packung"; unitPrice = 0.9; kcalPerKg = $null; expiryDate = "2029-01-01"; locationId = 10; notes = ""; lastUpdated = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds() }
@{ id = $toilettenPapierId; name = "Toilettenpapier"; categoryId = 12; quantity = 12.0; unit = "Rollen"; unitPrice = 0.3; kcalPerKg = $null; expiryDate = $null; locationId = 11; notes = ""; lastUpdated = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds() }
@{ id = $seifenId; name = "Seife"; categoryId = 12; quantity = 4.0; unit = "Stueck"; unitPrice = 1.5; kcalPerKg = $null; expiryDate = $null; locationId = 11; notes = ""; lastUpdated = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds() }
@{ id = $muesliId; name = "Muesliegel"; categoryId = 10; quantity = 20.0; unit = "Stueck"; unitPrice = 0.8; kcalPerKg = $null; expiryDate = "2028-03-31"; locationId = 10; notes = ""; lastUpdated = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds() }
@{ id = $apfelsaftId; name = "Apfelsaft"; categoryId = 11; quantity = 6.0; unit = "Liter"; unitPrice = 1.0; kcalPerKg = $null; expiryDate = "2028-09-30"; locationId = 10; notes = ""; lastUpdated = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds() }
@{ id = $kerzenId; name = "Kerzen"; categoryId = 10; quantity = 10.0; unit = "Stueck"; unitPrice = 1.5; kcalPerKg = $null; expiryDate = $null; locationId = 10; notes = ""; lastUpdated = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds() }
@{ id = $pflasterId; name = "Erste-Hilfe-Pflaster"; categoryId = 12; quantity = 2.0; unit = "Packung"; unitPrice = 3.5; kcalPerKg = $null; expiryDate = "2029-06-30"; locationId = 11; notes = ""; lastUpdated = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds() }
@{ id = $dosenBrotId; name = "Dosenbrot"; categoryId = 10; quantity = 5.0; unit = "Stueck"; unitPrice = 2.0; kcalPerUnit = $null; expiryDate = "2028-12-31"; locationId = 10; notes = ""; lastUpdated = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds() }
@{ id = $wasserId; name = "Mineralwasser"; categoryId = 11; quantity = 24.0; unit = "Stueck"; unitPrice = 0.5; kcalPerUnit = $null; expiryDate = "2028-12-31"; locationId = 10; notes = ""; lastUpdated = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds() }
@{ id = $thunfischId; name = "Thunfisch"; categoryId = 10; quantity = 8.0; unit = "Dose"; unitPrice = 1.2; kcalPerUnit = $null; expiryDate = "2028-06-30"; locationId = 10; notes = ""; lastUpdated = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds() }
@{ id = $nudelnId; name = "Nudeln"; categoryId = 10; quantity = 3.0; unit = "Packung"; unitPrice = 0.9; kcalPerUnit = $null; expiryDate = "2029-01-01"; locationId = 10; notes = ""; lastUpdated = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds() }
@{ id = $toilettenPapierId; name = "Toilettenpapier"; categoryId = 12; quantity = 12.0; unit = "Rollen"; unitPrice = 0.3; kcalPerUnit = $null; expiryDate = $null; locationId = 11; notes = ""; lastUpdated = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds() }
@{ id = $seifenId; name = "Seife"; categoryId = 12; quantity = 4.0; unit = "Stueck"; unitPrice = 1.5; kcalPerUnit = $null; expiryDate = $null; locationId = 11; notes = ""; lastUpdated = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds() }
@{ id = $muesliId; name = "Muesliegel"; categoryId = 10; quantity = 20.0; unit = "Stueck"; unitPrice = 0.8; kcalPerUnit = $null; expiryDate = "2028-03-31"; locationId = 10; notes = ""; lastUpdated = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds() }
@{ id = $apfelsaftId; name = "Apfelsaft"; categoryId = 11; quantity = 6.0; unit = "Liter"; unitPrice = 1.0; kcalPerUnit = $null; expiryDate = "2028-09-30"; locationId = 10; notes = ""; lastUpdated = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds() }
@{ id = $kerzenId; name = "Kerzen"; categoryId = 10; quantity = 10.0; unit = "Stueck"; unitPrice = 1.5; kcalPerUnit = $null; expiryDate = $null; locationId = 10; notes = ""; lastUpdated = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds() }
@{ id = $pflasterId; name = "Erste-Hilfe-Pflaster"; categoryId = 12; quantity = 2.0; unit = "Packung"; unitPrice = 3.5; kcalPerUnit = $null; expiryDate = "2029-06-30"; locationId = 11; notes = ""; lastUpdated = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds() }
)
settings = @()
}
@ -567,7 +567,7 @@ if ($bobTokens) {
$patchBody = @{
id = $dosenBrotId; name = "Dosenbrot"; categoryId = 10
quantity = 10.0; unit = "Stueck"; unitPrice = 2.0
kcalPerKg = $null; expiryDate = "2028-12-31"; locationId = 10
kcalPerUnit = $null; expiryDate = "2028-12-31"; locationId = 10
notes = ""; lastUpdated = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds()
}
Invoke-Api -Method PATCH -Path "/api/inventory/items/$dosenBrotId" -Body $patchBody -Token $bobTokens.accessToken | Out-Null
@ -589,7 +589,7 @@ if ($bobTokens) {
categories = $bob10Items.categories
locations = $bob10Items.locations
items = $bob10Items.items + @(
@{ id = $salzcrackerId; name = "Salzcracker"; categoryId = 10; quantity = 5.0; unit = "Packung"; unitPrice = 1.2; kcalPerKg = $null; expiryDate = "2028-06-30"; locationId = 10; notes = ""; lastUpdated = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds() }
@{ id = $salzcrackerId; name = "Salzcracker"; categoryId = 10; quantity = 5.0; unit = "Packung"; unitPrice = 1.2; kcalPerUnit = $null; expiryDate = "2028-06-30"; locationId = 10; notes = ""; lastUpdated = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds() }
)
settings = @()
}
@ -616,7 +616,7 @@ if ($bobTokens) {
$patchBody2 = @{
id = $dosenBrotId; name = "Dosenbrot"; categoryId = 10
quantity = 15.0; unit = "Stueck"; unitPrice = 2.0
kcalPerKg = $null; expiryDate = "2028-12-31"; locationId = 10
kcalPerUnit = $null; expiryDate = "2028-12-31"; locationId = 10
notes = ""; lastUpdated = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds()
}
Invoke-Api -Method PATCH -Path "/api/inventory/items/$dosenBrotId" -Body $patchBody2 -Token $bobTokens2.accessToken | Out-Null
@ -666,7 +666,7 @@ if ($bobTokens) {
$patchT1 = @{
id = $unknownId; name = "Unbekannt"; categoryId = 10
quantity = 1.0; unit = "Stueck"; unitPrice = 0.0
kcalPerKg = $null; expiryDate = $null; locationId = 10
kcalPerUnit = $null; expiryDate = $null; locationId = 10
notes = ""; lastUpdated = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds()
}
try {
@ -710,7 +710,7 @@ if ($bobTokens) {
else { Fail "T6 bob_invalidToken: Unerwarteter Fehler bei GET: $_" }
}
$t6PatchId = [System.Guid]::NewGuid().ToString()
$t6Body = @{ id = $t6PatchId; name = "X"; categoryId = 10; quantity = 1.0; unit = "x"; unitPrice = 0.0; kcalPerKg = $null; expiryDate = $null; locationId = 10; notes = ""; lastUpdated = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds() }
$t6Body = @{ id = $t6PatchId; name = "X"; categoryId = 10; quantity = 1.0; unit = "x"; unitPrice = 0.0; kcalPerUnit = $null; expiryDate = $null; locationId = 10; notes = ""; lastUpdated = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds() }
try {
Invoke-Api -Method PATCH -Path "/api/inventory/items/$t6PatchId" -Body $t6Body -Token $invalidToken | Out-Null
Fail "T6 bob_invalidToken: PATCH sollte 401 liefern"
@ -743,13 +743,13 @@ if ($bobTokens) {
$firstPut = @{
version = 1
categories = @(@{ id = 20; name = "KatA" }); locations = @(@{ id = 20; name = "OrtA" })
items = @(@{ id = $firstId; name = "ErstesPUT"; categoryId = 20; quantity = 1.0; unit = "Stueck"; unitPrice = 0.0; kcalPerKg = $null; expiryDate = $null; locationId = 20; notes = ""; lastUpdated = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds() })
items = @(@{ id = $firstId; name = "ErstesPUT"; categoryId = 20; quantity = 1.0; unit = "Stueck"; unitPrice = 0.0; kcalPerUnit = $null; expiryDate = $null; locationId = 20; notes = ""; lastUpdated = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds() })
settings = @()
}
$secondPut = @{
version = 1
categories = @(@{ id = 21; name = "KatB" }); locations = @(@{ id = 21; name = "OrtB" })
items = @(@{ id = $secondId; name = "ZweitesPUT"; categoryId = 21; quantity = 2.0; unit = "Stueck"; unitPrice = 0.0; kcalPerKg = $null; expiryDate = $null; locationId = 21; notes = ""; lastUpdated = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds() })
items = @(@{ id = $secondId; name = "ZweitesPUT"; categoryId = 21; quantity = 2.0; unit = "Stueck"; unitPrice = 0.0; kcalPerUnit = $null; expiryDate = $null; locationId = 21; notes = ""; lastUpdated = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds() })
settings = @()
}
Invoke-Api -Method PUT -Path "/api/inventory" -Body $firstPut -Token $bobTokens.accessToken | Out-Null
@ -772,7 +772,7 @@ if ($bobTokens) {
$t4ItemId = [System.Guid]::NewGuid().ToString()
$t4Inv = @{
version = 1; categories = @(@{ id = 30; name = "KatT4" }); locations = @(@{ id = 30; name = "OrtT4" })
items = @(@{ id = $t4ItemId; name = "T4-Item"; categoryId = 30; quantity = 1.0; unit = "Stueck"; unitPrice = 0.0; kcalPerKg = $null; expiryDate = $null; locationId = 30; notes = ""; lastUpdated = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds() })
items = @(@{ id = $t4ItemId; name = "T4-Item"; categoryId = 30; quantity = 1.0; unit = "Stueck"; unitPrice = 0.0; kcalPerUnit = $null; expiryDate = $null; locationId = 30; notes = ""; lastUpdated = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds() })
settings = @()
}
Invoke-Api -Method PUT -Path "/api/inventory" -Body $t4Inv -Token $bobTokens.accessToken | Out-Null
@ -780,7 +780,7 @@ if ($bobTokens) {
$t4WS = Open-WebSocket -token $bobTokens.accessToken
Start-Sleep -Milliseconds 800
$t4Patch = @{ id = $t4ItemId; name = "T4-Item"; categoryId = 30; quantity = 5.0; unit = "Stueck"; unitPrice = 0.0; kcalPerKg = $null; expiryDate = $null; locationId = 30; notes = ""; lastUpdated = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds() }
$t4Patch = @{ id = $t4ItemId; name = "T4-Item"; categoryId = 30; quantity = 5.0; unit = "Stueck"; unitPrice = 0.0; kcalPerUnit = $null; expiryDate = $null; locationId = 30; notes = ""; lastUpdated = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds() }
Invoke-Api -Method PATCH -Path "/api/inventory/items/$t4ItemId" -Body $t4Patch -Token $bobTokens.accessToken | Out-Null
$t4Events = Receive-WsMessages -ws $t4WS -waitSeconds 5
@ -804,7 +804,7 @@ if ($bobTokens) {
$t7ItemId = [System.Guid]::NewGuid().ToString()
$t7Inv = @{
version = 1; categories = @(@{ id = 40; name = "KatT7" }); locations = @(@{ id = 40; name = "OrtT7" })
items = @(@{ id = $t7ItemId; name = "T7-Item"; categoryId = 40; quantity = 1.0; unit = "Stueck"; unitPrice = 0.0; kcalPerKg = $null; expiryDate = $null; locationId = 40; notes = ""; lastUpdated = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds() })
items = @(@{ id = $t7ItemId; name = "T7-Item"; categoryId = 40; quantity = 1.0; unit = "Stueck"; unitPrice = 0.0; kcalPerUnit = $null; expiryDate = $null; locationId = 40; notes = ""; lastUpdated = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds() })
settings = @()
}
Invoke-Api -Method PUT -Path "/api/inventory" -Body $t7Inv -Token $bobTokens.accessToken | Out-Null
@ -814,7 +814,7 @@ if ($bobTokens) {
Start-Sleep -Milliseconds 1200
$bobTokens4 = Invoke-Api -Method POST -Path "/api/auth/login" -Body @{ username = $BobUser; password = $BobPassword }
$t7Patch = @{ id = $t7ItemId; name = "T7-Item"; categoryId = 40; quantity = 9.0; unit = "Stueck"; unitPrice = 0.0; kcalPerKg = $null; expiryDate = $null; locationId = 40; notes = ""; lastUpdated = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds() }
$t7Patch = @{ id = $t7ItemId; name = "T7-Item"; categoryId = 40; quantity = 9.0; unit = "Stueck"; unitPrice = 0.0; kcalPerUnit = $null; expiryDate = $null; locationId = 40; notes = ""; lastUpdated = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds() }
Invoke-Api -Method PATCH -Path "/api/inventory/items/$t7ItemId" -Body $t7Patch -Token $bobTokens4.accessToken | Out-Null
$t7ev1 = Receive-WsMessages -ws $t7ws1 -waitSeconds 5
@ -841,7 +841,7 @@ if ($bobTokens) {
$t8ItemId = [System.Guid]::NewGuid().ToString()
$t8Inv = @{
version = 1; categories = @(@{ id = 50; name = "KatT8" }); locations = @(@{ id = 50; name = "OrtT8" })
items = @(@{ id = $t8ItemId; name = "T8-Item"; categoryId = 50; quantity = 1.0; unit = "Stueck"; unitPrice = 0.0; kcalPerKg = $null; expiryDate = $null; locationId = 50; notes = ""; lastUpdated = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds() })
items = @(@{ id = $t8ItemId; name = "T8-Item"; categoryId = 50; quantity = 1.0; unit = "Stueck"; unitPrice = 0.0; kcalPerUnit = $null; expiryDate = $null; locationId = 50; notes = ""; lastUpdated = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds() })
settings = @()
}
Invoke-Api -Method PUT -Path "/api/inventory" -Body $t8Inv -Token $bobTokens.accessToken | Out-Null
@ -851,7 +851,7 @@ if ($bobTokens) {
Start-Sleep -Milliseconds 600
$bobTokens5 = Invoke-Api -Method POST -Path "/api/auth/login" -Body @{ username = $BobUser; password = $BobPassword }
$t8Patch = @{ id = $t8ItemId; name = "T8-Item"; categoryId = 50; quantity = 7.0; unit = "Stueck"; unitPrice = 0.0; kcalPerKg = $null; expiryDate = $null; locationId = 50; notes = ""; lastUpdated = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds() }
$t8Patch = @{ id = $t8ItemId; name = "T8-Item"; categoryId = 50; quantity = 7.0; unit = "Stueck"; unitPrice = 0.0; kcalPerUnit = $null; expiryDate = $null; locationId = 50; notes = ""; lastUpdated = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds() }
try {
Invoke-Api -Method PATCH -Path "/api/inventory/items/$t8ItemId" -Body $t8Patch -Token $bobTokens5.accessToken | Out-Null
Pass "T8 bob_patchAfterDisconnect: PATCH nach Bob-Disconnect liefert keinen Server-Fehler"

View file

@ -45,7 +45,7 @@ internal object Items : Table("items") {
val quantity = double("quantity")
val unit = varchar("unit", 50)
val unitPrice = double("unit_price")
val kcalPerKg = integer("kcal_per_kg").nullable()
val kcalPerUnit = integer("kcal_per_unit").nullable()
val expiryDate = varchar("expiry_date", 10).nullable()
val locationId = integer("location_id")
val notes = text("notes")

View file

@ -181,7 +181,7 @@ internal class InventoryRepository {
it[quantity] = item.quantity
it[unit] = item.unit
it[unitPrice] = item.unitPrice
it[kcalPerKg] = item.kcalPerKg
it[kcalPerUnit] = item.kcalPerUnit
it[expiryDate] = item.expiryDate
it[locationId] = item.locationId
it[notes] = item.notes
@ -220,7 +220,7 @@ internal class InventoryRepository {
quantity = it[Items.quantity],
unit = it[Items.unit],
unitPrice = it[Items.unitPrice],
kcalPerKg = it[Items.kcalPerKg],
kcalPerUnit = it[Items.kcalPerUnit],
expiryDate = it[Items.expiryDate],
locationId = it[Items.locationId],
notes = it[Items.notes],
@ -246,7 +246,7 @@ internal class InventoryRepository {
it[quantity] = item.quantity
it[unit] = item.unit
it[unitPrice] = item.unitPrice
it[kcalPerKg] = item.kcalPerKg
it[kcalPerUnit] = item.kcalPerUnit
it[expiryDate] = item.expiryDate
it[locationId] = item.locationId
it[notes] = item.notes
@ -257,7 +257,7 @@ internal class InventoryRepository {
}
fun patchItemPartial(inventoryId: String, itemId: String, fields: JsonObject): Boolean {
val updatableKeys = setOf("name", "categoryId", "quantity", "unit", "unitPrice", "kcalPerKg", "expiryDate", "locationId", "notes", "lastUpdated")
val updatableKeys = setOf("name", "categoryId", "quantity", "unit", "unitPrice", "kcalPerUnit", "expiryDate", "locationId", "notes", "lastUpdated")
return transaction {
val exists = Items.selectAll()
.where { (Items.id eq itemId) and (Items.inventoryId eq inventoryId) }
@ -275,7 +275,7 @@ internal class InventoryRepository {
if ("quantity" in fields) stmt[quantity] = fields["quantity"]!!.jsonPrimitive.doubleOrNull ?: 0.0
if ("unit" in fields) stmt[unit] = fields["unit"]!!.jsonPrimitive.content
if ("unitPrice" in fields) stmt[unitPrice] = fields["unitPrice"]!!.jsonPrimitive.doubleOrNull ?: 0.0
if ("kcalPerKg" in fields) stmt[kcalPerKg] = fields["kcalPerKg"]!!.jsonPrimitive.intOrNull
if ("kcalPerUnit" in fields) stmt[kcalPerUnit] = fields["kcalPerUnit"]!!.jsonPrimitive.intOrNull
if ("expiryDate" in fields) stmt[expiryDate] = fields["expiryDate"]!!.jsonPrimitive.contentOrNull
if ("locationId" in fields) stmt[locationId] = fields["locationId"]!!.jsonPrimitive.intOrNull ?: 0
if ("notes" in fields) stmt[notes] = fields["notes"]!!.jsonPrimitive.content
@ -297,7 +297,7 @@ internal class InventoryRepository {
quantity = it[Items.quantity],
unit = it[Items.unit],
unitPrice = it[Items.unitPrice],
kcalPerKg = it[Items.kcalPerKg],
kcalPerUnit = it[Items.kcalPerUnit],
expiryDate = it[Items.expiryDate],
locationId = it[Items.locationId],
notes = it[Items.notes],
@ -344,7 +344,7 @@ internal class InventoryRepository {
quantity = it[Items.quantity],
unit = it[Items.unit],
unitPrice = it[Items.unitPrice],
kcalPerKg = it[Items.kcalPerKg],
kcalPerUnit = it[Items.kcalPerUnit],
expiryDate = it[Items.expiryDate],
locationId = it[Items.locationId],
notes = it[Items.notes],

View file

@ -200,7 +200,7 @@ class ApplicationTest {
quantity = 5.0,
unit = "Stück",
unitPrice = 3.99,
kcalPerKg = 250,
kcalPerUnit = 250,
expiryDate = "2027-06-15",
locationId = 1,
notes = "Vollkornbrot in der Dose",

View file

@ -156,7 +156,7 @@ class DeltaSyncTest {
ItemDto(
id = "item-1", name = "Dosenbrot", categoryId = 1,
quantity = 5.0, unit = "Stück", unitPrice = 3.99,
kcalPerKg = 250, expiryDate = "2027-06-15", locationId = 1,
kcalPerUnit = 250, expiryDate = "2027-06-15", locationId = 1,
notes = "", lastUpdated = 3000L
)
),
@ -205,13 +205,13 @@ class DeltaSyncTest {
ItemDto(
id = "item-1", name = "Dosenbrot", categoryId = 1,
quantity = 5.0, unit = "Stück", unitPrice = 3.99,
kcalPerKg = 250, expiryDate = "2027-06-15", locationId = 1,
kcalPerUnit = 250, expiryDate = "2027-06-15", locationId = 1,
notes = "", lastUpdated = lastUpdated
),
ItemDto(
id = "item-2", name = "Mineralwasser", categoryId = 1,
quantity = 24.0, unit = "Liter", unitPrice = 0.49,
kcalPerKg = 0, expiryDate = "2028-01-01", locationId = 1,
kcalPerUnit = 0, expiryDate = "2028-01-01", locationId = 1,
notes = "", lastUpdated = lastUpdated
)
),

View file

@ -105,7 +105,7 @@ class EndToEndSyncTest {
quantity = 3.0,
unit = "Stück",
unitPrice = 1.50,
kcalPerKg = null,
kcalPerUnit = null,
expiryDate = null,
locationId = 10,
notes = "",
@ -176,7 +176,7 @@ class EndToEndSyncTest {
quantity = 10.0,
unit = "Stück",
unitPrice = 3.99,
kcalPerKg = 250,
kcalPerUnit = 250,
expiryDate = "2027-06-15",
locationId = 1,
notes = "Menge erhöht",
@ -246,7 +246,7 @@ class EndToEndSyncTest {
quantity = 5.0,
unit = "Stück",
unitPrice = 3.99,
kcalPerKg = 250,
kcalPerUnit = 250,
expiryDate = "2027-06-15",
locationId = 1,
notes = "Vollkornbrot in der Dose",
@ -259,7 +259,7 @@ class EndToEndSyncTest {
quantity = 24.0,
unit = "Liter",
unitPrice = 0.49,
kcalPerKg = 0,
kcalPerUnit = 0,
expiryDate = "2028-01-01",
locationId = 2,
notes = "Stilles Wasser",

View file

@ -49,7 +49,7 @@ class InputValidationTest {
quantity = 10.0,
unit = "L",
unitPrice = 0.5,
kcalPerKg = null,
kcalPerUnit = null,
expiryDate = "2027-12-31",
locationId = 1,
notes = "",

View file

@ -220,7 +220,7 @@ class InventoryManagementTest {
de.krisenvorrat.shared.model.ItemDto(
id = "item-1", name = "Water", categoryId = 1,
quantity = 10.0, unit = "L", unitPrice = 0.5,
kcalPerKg = null, expiryDate = null,
kcalPerUnit = null, expiryDate = null,
locationId = 1, notes = "", lastUpdated = 1000
)
),

View file

@ -286,7 +286,7 @@ class InventorySharingTest {
quantity = 1.0,
unit = "Stück",
unitPrice = 1.99,
kcalPerKg = null,
kcalPerUnit = null,
expiryDate = null,
locationId = 1,
notes = "",

View file

@ -95,10 +95,10 @@ class InventoryStatsTest {
),
items = listOf(
ItemDto(id = "i1", name = "Dosenbrot", categoryId = 1, quantity = 5.0, unit = "Stk",
unitPrice = 3.99, kcalPerKg = null, expiryDate = null, locationId = 1, notes = "",
unitPrice = 3.99, kcalPerUnit = null, expiryDate = null, locationId = 1, notes = "",
lastUpdated = System.currentTimeMillis()),
ItemDto(id = "i2", name = "Wasser", categoryId = 2, quantity = 10.0, unit = "L",
unitPrice = 0.5, kcalPerKg = null, expiryDate = null, locationId = 2, notes = "",
unitPrice = 0.5, kcalPerUnit = null, expiryDate = null, locationId = 2, notes = "",
lastUpdated = System.currentTimeMillis())
),
settings = emptyList()

View file

@ -42,7 +42,7 @@ class PatchItemTest {
quantity = 5.0,
unit = "Stück",
unitPrice = 3.99,
kcalPerKg = 250,
kcalPerUnit = 250,
expiryDate = "2027-06-15",
locationId = 1,
notes = "Vollkornbrot",
@ -83,7 +83,7 @@ class PatchItemTest {
// Unchanged fields remain
assertEquals("Stück", result.unit)
assertEquals(3.99, result.unitPrice, 0.001)
assertEquals(250, result.kcalPerKg)
assertEquals(250, result.kcalPerUnit)
assertEquals("2027-06-15", result.expiryDate)
assertEquals(1, result.locationId)
assertEquals("Vollkornbrot", result.notes)
@ -98,17 +98,17 @@ class PatchItemTest {
setBody(seedInventory())
}
// When only update kcalPerKg
// When only update kcalPerUnit
val response = client.patch("/api/inventory/items/item-patch-1") {
bearerAuth(token)
contentType(ContentType.Application.Json)
setBody("""{"kcalPerKg":3200}""")
setBody("""{"kcalPerUnit":3200}""")
}
// Then
assertEquals(HttpStatusCode.OK, response.status)
val result = json.decodeFromString<ItemDto>(response.bodyAsText())
assertEquals(3200, result.kcalPerKg)
assertEquals(3200, result.kcalPerUnit)
assertEquals("Dosenbrot", result.name)
assertEquals("item-patch-1", result.id)
}

View file

@ -51,7 +51,7 @@ class UserIsolationTest {
quantity = 5.0,
unit = "Stück",
unitPrice = 2.99,
kcalPerKg = null,
kcalPerUnit = null,
expiryDate = null,
locationId = 1,
notes = "",
@ -87,7 +87,7 @@ class UserIsolationTest {
ItemDto(
id = "item-a1", name = "Dosenbrot", categoryId = 1,
quantity = 5.0, unit = "Stück", unitPrice = 3.99,
kcalPerKg = null, expiryDate = null, locationId = 1, notes = "", lastUpdated = 100L
kcalPerUnit = null, expiryDate = null, locationId = 1, notes = "", lastUpdated = 100L
)
),
settings = emptyList()
@ -99,7 +99,7 @@ class UserIsolationTest {
ItemDto(
id = "item-b1", name = "Seife", categoryId = 1,
quantity = 3.0, unit = "Stück", unitPrice = 1.50,
kcalPerKg = null, expiryDate = null, locationId = 1, notes = "", lastUpdated = 200L
kcalPerUnit = null, expiryDate = null, locationId = 1, notes = "", lastUpdated = 200L
)
),
settings = emptyList()

View file

@ -86,7 +86,7 @@ class InventoryRepositoryTest {
assertEquals(5.0, item.quantity, 0.001)
assertEquals("Stück", item.unit)
assertEquals(3.99, item.unitPrice, 0.001)
assertEquals(250, item.kcalPerKg)
assertEquals(250, item.kcalPerUnit)
assertEquals("2027-06-15", item.expiryDate)
assertEquals(1, item.locationId)
assertEquals("Vollkornbrot in der Dose", item.notes)
@ -137,7 +137,7 @@ class InventoryRepositoryTest {
quantity = 10.0,
unit = "Stück",
unitPrice = 1.50,
kcalPerKg = null,
kcalPerUnit = null,
expiryDate = null,
locationId = 1,
notes = "",
@ -153,7 +153,7 @@ class InventoryRepositoryTest {
// Then
val item = result.items[0]
assertNull(item.kcalPerKg)
assertNull(item.kcalPerUnit)
assertNull(item.expiryDate)
}
@ -174,7 +174,7 @@ class InventoryRepositoryTest {
quantity = 5.0,
unit = "Stück",
unitPrice = 3.99,
kcalPerKg = 250,
kcalPerUnit = 250,
expiryDate = "2027-06-15",
locationId = 1,
notes = "Vollkornbrot in der Dose",
@ -225,11 +225,11 @@ class InventoryRepositoryTest {
locations = listOf(LocationDto(id = 1, name = "Test")),
items = listOf(
ItemDto(id = "old", name = "Old", categoryId = 1, quantity = 1.0, unit = "Stk", unitPrice = 0.0,
kcalPerKg = null, expiryDate = null, locationId = 1, notes = "", lastUpdated = oldTimestamp),
kcalPerUnit = null, expiryDate = null, locationId = 1, notes = "", lastUpdated = oldTimestamp),
ItemDto(id = "recent1", name = "Recent1", categoryId = 1, quantity = 1.0, unit = "Stk", unitPrice = 0.0,
kcalPerKg = null, expiryDate = null, locationId = 1, notes = "", lastUpdated = recentTimestamp),
kcalPerUnit = null, expiryDate = null, locationId = 1, notes = "", lastUpdated = recentTimestamp),
ItemDto(id = "recent2", name = "Recent2", categoryId = 1, quantity = 1.0, unit = "Stk", unitPrice = 0.0,
kcalPerKg = null, expiryDate = null, locationId = 1, notes = "", lastUpdated = now)
kcalPerUnit = null, expiryDate = null, locationId = 1, notes = "", lastUpdated = now)
),
settings = emptyList()
)
@ -252,10 +252,10 @@ class InventoryRepositoryTest {
locations = listOf(LocationDto(id = 1, name = "Test")),
items = listOf(
ItemDto(id = "old", name = "Old", categoryId = 1, quantity = 1.0, unit = "Stk",
unitPrice = 0.0, kcalPerKg = null, expiryDate = null, locationId = 1,
unitPrice = 0.0, kcalPerUnit = null, expiryDate = null, locationId = 1,
notes = "", lastUpdated = 1000L),
ItemDto(id = "new", name = "New", categoryId = 1, quantity = 1.0, unit = "Stk",
unitPrice = 0.0, kcalPerKg = null, expiryDate = null, locationId = 1,
unitPrice = 0.0, kcalPerUnit = null, expiryDate = null, locationId = 1,
notes = "", lastUpdated = 3000L)
),
settings = emptyList()
@ -319,7 +319,7 @@ class InventoryRepositoryTest {
items = listOf(
ItemDto(
id = "inv-item-1", name = "Dosenbrot", categoryId = 1, quantity = 3.0,
unit = "Stk", unitPrice = 1.5, kcalPerKg = null, expiryDate = null,
unit = "Stk", unitPrice = 1.5, kcalPerUnit = null, expiryDate = null,
locationId = 1, notes = "", lastUpdated = 1715000000L
)
),

View file

@ -10,7 +10,7 @@ data class ItemDto(
val quantity: Double,
val unit: String,
val unitPrice: Double,
val kcalPerKg: Int?,
val kcalPerUnit: Int?,
val expiryDate: String?,
val locationId: Int,
val notes: String,