From ade48fa5f68eb9d066389bf7586a9c220ae8e7d8 Mon Sep 17 00:00:00 2001
From: Janne Mareike Koschinski <mail@justjanne.de>
Date: Mon, 13 Nov 2023 01:12:23 +0100
Subject: [PATCH] wip: progress

---
 .../chaosdorf/mete/{ => model}/BarcodeId.kt   |   6 +-
 .../de/chaosdorf/mete/model/BarcodeModel.kt   |  30 +++++
 .../de/chaosdorf/mete/{ => model}/DrinkId.kt  |   6 +-
 .../DrinkModel.kt}                            |  25 ++--
 .../kotlin/de/chaosdorf/mete/model/MeteApi.kt |  42 ++++++
 .../mete/{v1 => model}/MeteApiFactory.kt      |   6 +-
 .../mete/model/PurchaseSummaryModel.kt        |  35 +++++
 .../de/chaosdorf/mete/{ => model}/PwaIcon.kt  |   2 +-
 .../chaosdorf/mete/{ => model}/PwaManifest.kt |   2 +-
 .../TransactionId.kt}                         |   6 +-
 .../TransactionModel.kt}                      |  21 ++-
 .../de/chaosdorf/mete/{ => model}/UserId.kt   |   6 +-
 .../de/chaosdorf/mete/model/UserModel.kt      |  37 +++++
 .../mete/util/BigDecimalSerializer.kt         |  44 ++++++
 .../chaosdorf/mete/util/Code204Interceptor.kt |  41 ++++++
 .../de/chaosdorf/mete/v1/BarcodeModelV1.kt    |  16 ++-
 .../de/chaosdorf/mete/v1/DrinkModelV1.kt      |  30 +++--
 .../kotlin/de/chaosdorf/mete/v1/MeteApiV1.kt  |  61 ++++++---
 .../de/chaosdorf/mete/v1/MeteApiV1Factory.kt  |  13 +-
 .../chaosdorf/mete/v1/TransactionModelV1.kt   |  47 +++++++
 .../mete/v1/TransactionSummaryModelV1.kt      |  46 +++++++
 .../de/chaosdorf/mete/v1/UserModelV1.kt       |  31 +++--
 app/build.gradle.kts                          |   1 +
 .../chaosdorf/meteroid/di/DatabaseModule.kt   |  11 +-
 .../meteroid/sample/SampleDrinkProvider.kt    |  55 +++-----
 .../meteroid/storage/AccountPreferences.kt    |   2 +-
 .../storage/AccountPreferencesImpl.kt         |   2 +-
 .../meteroid/sync/DrinkSyncHandler.kt         |   9 +-
 .../meteroid/sync/PurchaseSyncHandler.kt      |  35 ++---
 .../meteroid/sync/UserSyncHandler.kt          |   9 +-
 .../de/chaosdorf/meteroid/ui/AppRouter.kt     |  44 ++++--
 .../de/chaosdorf/meteroid/ui/AppViewModel.kt  |  16 +--
 .../de/chaosdorf/meteroid/ui/PriceBadge.kt    |  62 +++++++++
 .../de/chaosdorf/meteroid/ui/SyncManager.kt   |  75 +++++++++++
 .../meteroid/ui/drinks/DrinkListFilterChip.kt |  60 +++++++++
 .../meteroid/ui/drinks/DrinkListScreen.kt     |  34 +++--
 .../meteroid/ui/drinks/DrinkListViewModel.kt  |  52 +++++--
 .../chaosdorf/meteroid/ui/drinks/DrinkTile.kt |  62 +++++----
 .../meteroid/ui/money/MoneyListScreen.kt      |   2 +-
 .../meteroid/ui/money/MoneyListViewModel.kt   |  31 +++--
 .../chaosdorf/meteroid/ui/money/MoneyTile.kt  |  53 ++++----
 .../meteroid/ui/navigation/MeteroidTopBar.kt  | 127 +++++++++---------
 .../meteroid/ui/servers/AddServerViewModel.kt |   5 +-
 .../PurchaseListItem.kt                       |  45 +++----
 .../PurchaseListScreen.kt                     |  25 ++--
 .../PurchaseViewModel.kt                      |  44 +++---
 .../TransactionInfo.kt}                       |   8 +-
 .../meteroid/ui/users/UserListScreen.kt       |  35 ++---
 .../meteroid/ui/users/UserListViewModel.kt    |   7 +-
 .../meteroid/util/PwaManifestExtension.kt     |   4 +-
 .../de/chaosdorf/meteroid/util/Update.kt      |  35 +++++
 .../1.json                                    |  86 +++---------
 .../de/chaosdorf/meteroid/MeteroidDatabase.kt |  16 ++-
 .../de/chaosdorf/meteroid/model/Drink.kt      |  32 ++---
 .../model/{Purchase.kt => Transaction.kt}     |  61 +++++----
 .../de/chaosdorf/meteroid/model/User.kt       |  16 +--
 .../meteroid/util/BigDecimalTypeConverter.kt  |  36 +++++
 57 files changed, 1196 insertions(+), 554 deletions(-)
 rename api/src/main/kotlin/de/chaosdorf/mete/{ => model}/BarcodeId.kt (91%)
 create mode 100644 api/src/main/kotlin/de/chaosdorf/mete/model/BarcodeModel.kt
 rename api/src/main/kotlin/de/chaosdorf/mete/{ => model}/DrinkId.kt (91%)
 rename api/src/main/kotlin/de/chaosdorf/mete/{v1/AuditResponseV1.kt => model/DrinkModel.kt} (75%)
 create mode 100644 api/src/main/kotlin/de/chaosdorf/mete/model/MeteApi.kt
 rename api/src/main/kotlin/de/chaosdorf/mete/{v1 => model}/MeteApiFactory.kt (91%)
 create mode 100644 api/src/main/kotlin/de/chaosdorf/mete/model/PurchaseSummaryModel.kt
 rename api/src/main/kotlin/de/chaosdorf/mete/{ => model}/PwaIcon.kt (97%)
 rename api/src/main/kotlin/de/chaosdorf/mete/{ => model}/PwaManifest.kt (97%)
 rename api/src/main/kotlin/de/chaosdorf/mete/{AuditEntryId.kt => model/TransactionId.kt} (90%)
 rename api/src/main/kotlin/de/chaosdorf/mete/{v1/AuditEntryModelV1.kt => model/TransactionModel.kt} (77%)
 rename api/src/main/kotlin/de/chaosdorf/mete/{ => model}/UserId.kt (91%)
 create mode 100644 api/src/main/kotlin/de/chaosdorf/mete/model/UserModel.kt
 create mode 100644 api/src/main/kotlin/de/chaosdorf/mete/util/BigDecimalSerializer.kt
 create mode 100644 api/src/main/kotlin/de/chaosdorf/mete/util/Code204Interceptor.kt
 create mode 100644 api/src/main/kotlin/de/chaosdorf/mete/v1/TransactionModelV1.kt
 create mode 100644 api/src/main/kotlin/de/chaosdorf/mete/v1/TransactionSummaryModelV1.kt
 create mode 100644 app/src/main/kotlin/de/chaosdorf/meteroid/ui/PriceBadge.kt
 create mode 100644 app/src/main/kotlin/de/chaosdorf/meteroid/ui/SyncManager.kt
 create mode 100644 app/src/main/kotlin/de/chaosdorf/meteroid/ui/drinks/DrinkListFilterChip.kt
 rename app/src/main/kotlin/de/chaosdorf/meteroid/ui/{purchases => transactions}/PurchaseListItem.kt (77%)
 rename app/src/main/kotlin/de/chaosdorf/meteroid/ui/{purchases => transactions}/PurchaseListScreen.kt (77%)
 rename app/src/main/kotlin/de/chaosdorf/meteroid/ui/{purchases => transactions}/PurchaseViewModel.kt (70%)
 rename app/src/main/kotlin/de/chaosdorf/meteroid/ui/{purchases/PurchaseInfo.kt => transactions/TransactionInfo.kt} (88%)
 create mode 100644 app/src/main/kotlin/de/chaosdorf/meteroid/util/Update.kt
 rename persistence/src/main/kotlin/de/chaosdorf/meteroid/model/{Purchase.kt => Transaction.kt} (57%)
 create mode 100644 persistence/src/main/kotlin/de/chaosdorf/meteroid/util/BigDecimalTypeConverter.kt

diff --git a/api/src/main/kotlin/de/chaosdorf/mete/BarcodeId.kt b/api/src/main/kotlin/de/chaosdorf/mete/model/BarcodeId.kt
similarity index 91%
rename from api/src/main/kotlin/de/chaosdorf/mete/BarcodeId.kt
rename to api/src/main/kotlin/de/chaosdorf/mete/model/BarcodeId.kt
index 6124565..11454ca 100644
--- a/api/src/main/kotlin/de/chaosdorf/mete/BarcodeId.kt
+++ b/api/src/main/kotlin/de/chaosdorf/mete/model/BarcodeId.kt
@@ -22,10 +22,12 @@
  * THE SOFTWARE.
  */
 
-package de.chaosdorf.mete
+package de.chaosdorf.mete.model
 
 import kotlinx.serialization.Serializable
 
 @Serializable
 @JvmInline
-value class BarcodeId(val value: Long)
+value class BarcodeId(val value: Long) {
+  override fun toString() = value.toString()
+}
diff --git a/api/src/main/kotlin/de/chaosdorf/mete/model/BarcodeModel.kt b/api/src/main/kotlin/de/chaosdorf/mete/model/BarcodeModel.kt
new file mode 100644
index 0000000..1205858
--- /dev/null
+++ b/api/src/main/kotlin/de/chaosdorf/mete/model/BarcodeModel.kt
@@ -0,0 +1,30 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2013-2023 Chaosdorf e.V.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package de.chaosdorf.mete.model
+
+interface BarcodeModel {
+  val barcodeId: BarcodeId
+  val drinkId: DrinkId
+}
diff --git a/api/src/main/kotlin/de/chaosdorf/mete/DrinkId.kt b/api/src/main/kotlin/de/chaosdorf/mete/model/DrinkId.kt
similarity index 91%
rename from api/src/main/kotlin/de/chaosdorf/mete/DrinkId.kt
rename to api/src/main/kotlin/de/chaosdorf/mete/model/DrinkId.kt
index a7ce209..4670a5f 100644
--- a/api/src/main/kotlin/de/chaosdorf/mete/DrinkId.kt
+++ b/api/src/main/kotlin/de/chaosdorf/mete/model/DrinkId.kt
@@ -22,10 +22,12 @@
  * THE SOFTWARE.
  */
 
-package de.chaosdorf.mete
+package de.chaosdorf.mete.model
 
 import kotlinx.serialization.Serializable
 
 @Serializable
 @JvmInline
-value class DrinkId(val value: Long)
+value class DrinkId(val value: Long) {
+  override fun toString() = value.toString()
+}
diff --git a/api/src/main/kotlin/de/chaosdorf/mete/v1/AuditResponseV1.kt b/api/src/main/kotlin/de/chaosdorf/mete/model/DrinkModel.kt
similarity index 75%
rename from api/src/main/kotlin/de/chaosdorf/mete/v1/AuditResponseV1.kt
rename to api/src/main/kotlin/de/chaosdorf/mete/model/DrinkModel.kt
index c0d79e1..5019a19 100644
--- a/api/src/main/kotlin/de/chaosdorf/mete/v1/AuditResponseV1.kt
+++ b/api/src/main/kotlin/de/chaosdorf/mete/model/DrinkModel.kt
@@ -22,19 +22,16 @@
  * THE SOFTWARE.
  */
 
-package de.chaosdorf.mete.v1
+package de.chaosdorf.mete.model
 
-import kotlinx.serialization.SerialName
-import kotlinx.serialization.Serializable
+import java.math.BigDecimal
 
-@Serializable
-data class AuditResponseV1(
-  @SerialName("payments_sum")
-  val payments: Double,
-  @SerialName("deposits_sum")
-  val deposits: Double,
-  @SerialName("sum")
-  val total: Double,
-  @SerialName("audits")
-  val entries: List<AuditEntryModelV1>
-)
+interface DrinkModel {
+  val drinkId: DrinkId
+  val name: String
+  val active: Boolean
+  val volume: BigDecimal
+  val caffeine: Int?
+  val price: BigDecimal
+  val logoUrl: String
+}
diff --git a/api/src/main/kotlin/de/chaosdorf/mete/model/MeteApi.kt b/api/src/main/kotlin/de/chaosdorf/mete/model/MeteApi.kt
new file mode 100644
index 0000000..8313d89
--- /dev/null
+++ b/api/src/main/kotlin/de/chaosdorf/mete/model/MeteApi.kt
@@ -0,0 +1,42 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2013-2023 Chaosdorf e.V.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package de.chaosdorf.mete.model
+
+import java.math.BigDecimal
+
+interface MeteApi {
+  suspend fun getManifest(): PwaManifest?
+  suspend fun listTransactions(user: UserId? = null): TransactionSummaryModel
+  suspend fun listBarcodes(): List<BarcodeModel>
+  suspend fun getBarcode(id: BarcodeId): BarcodeModel?
+  suspend fun listDrinks(): List<DrinkModel>
+  suspend fun getDrink(id: DrinkId): DrinkModel?
+  suspend fun listUsers(): List<UserModel>
+  suspend fun getUser(id: UserId): UserModel?
+  suspend fun deposit(id: UserId, amount: BigDecimal)
+  suspend fun withdraw(id: UserId, amount: BigDecimal)
+  suspend fun purchase(id: UserId, drink: DrinkId)
+  suspend fun purchase(id: UserId, barcode: BarcodeId)
+}
diff --git a/api/src/main/kotlin/de/chaosdorf/mete/v1/MeteApiFactory.kt b/api/src/main/kotlin/de/chaosdorf/mete/model/MeteApiFactory.kt
similarity index 91%
rename from api/src/main/kotlin/de/chaosdorf/mete/v1/MeteApiFactory.kt
rename to api/src/main/kotlin/de/chaosdorf/mete/model/MeteApiFactory.kt
index a120e4e..ee4da8b 100644
--- a/api/src/main/kotlin/de/chaosdorf/mete/v1/MeteApiFactory.kt
+++ b/api/src/main/kotlin/de/chaosdorf/mete/model/MeteApiFactory.kt
@@ -22,8 +22,8 @@
  * THE SOFTWARE.
  */
 
-package de.chaosdorf.mete.v1
+package de.chaosdorf.mete.model
 
-interface MeteApiFactory<T> {
-  fun newInstance(baseUrl: String): T
+interface MeteApiFactory {
+  fun newInstance(baseUrl: String): MeteApi
 }
diff --git a/api/src/main/kotlin/de/chaosdorf/mete/model/PurchaseSummaryModel.kt b/api/src/main/kotlin/de/chaosdorf/mete/model/PurchaseSummaryModel.kt
new file mode 100644
index 0000000..be21ea4
--- /dev/null
+++ b/api/src/main/kotlin/de/chaosdorf/mete/model/PurchaseSummaryModel.kt
@@ -0,0 +1,35 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2013-2023 Chaosdorf e.V.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package de.chaosdorf.mete.model
+
+import java.math.BigDecimal
+
+interface TransactionSummaryModel {
+  val payments: BigDecimal
+  val deposits: BigDecimal
+  val total: BigDecimal
+  val entries: List<TransactionModel>
+}
+
diff --git a/api/src/main/kotlin/de/chaosdorf/mete/PwaIcon.kt b/api/src/main/kotlin/de/chaosdorf/mete/model/PwaIcon.kt
similarity index 97%
rename from api/src/main/kotlin/de/chaosdorf/mete/PwaIcon.kt
rename to api/src/main/kotlin/de/chaosdorf/mete/model/PwaIcon.kt
index 7734a53..ce833d8 100644
--- a/api/src/main/kotlin/de/chaosdorf/mete/PwaIcon.kt
+++ b/api/src/main/kotlin/de/chaosdorf/mete/model/PwaIcon.kt
@@ -22,7 +22,7 @@
  * THE SOFTWARE.
  */
 
-package de.chaosdorf.mete
+package de.chaosdorf.mete.model
 
 import kotlinx.serialization.Serializable
 
diff --git a/api/src/main/kotlin/de/chaosdorf/mete/PwaManifest.kt b/api/src/main/kotlin/de/chaosdorf/mete/model/PwaManifest.kt
similarity index 97%
rename from api/src/main/kotlin/de/chaosdorf/mete/PwaManifest.kt
rename to api/src/main/kotlin/de/chaosdorf/mete/model/PwaManifest.kt
index 2732335..a3fad87 100644
--- a/api/src/main/kotlin/de/chaosdorf/mete/PwaManifest.kt
+++ b/api/src/main/kotlin/de/chaosdorf/mete/model/PwaManifest.kt
@@ -22,7 +22,7 @@
  * THE SOFTWARE.
  */
 
-package de.chaosdorf.mete
+package de.chaosdorf.mete.model
 
 import kotlinx.serialization.SerialName
 import kotlinx.serialization.Serializable
diff --git a/api/src/main/kotlin/de/chaosdorf/mete/AuditEntryId.kt b/api/src/main/kotlin/de/chaosdorf/mete/model/TransactionId.kt
similarity index 90%
rename from api/src/main/kotlin/de/chaosdorf/mete/AuditEntryId.kt
rename to api/src/main/kotlin/de/chaosdorf/mete/model/TransactionId.kt
index d1725c4..8b3a85e 100644
--- a/api/src/main/kotlin/de/chaosdorf/mete/AuditEntryId.kt
+++ b/api/src/main/kotlin/de/chaosdorf/mete/model/TransactionId.kt
@@ -22,10 +22,12 @@
  * THE SOFTWARE.
  */
 
-package de.chaosdorf.mete
+package de.chaosdorf.mete.model
 
 import kotlinx.serialization.Serializable
 
 @Serializable
 @JvmInline
-value class AuditEntryId(val value: Long)
+value class TransactionId(val value: Long) {
+  override fun toString() = value.toString()
+}
diff --git a/api/src/main/kotlin/de/chaosdorf/mete/v1/AuditEntryModelV1.kt b/api/src/main/kotlin/de/chaosdorf/mete/model/TransactionModel.kt
similarity index 77%
rename from api/src/main/kotlin/de/chaosdorf/mete/v1/AuditEntryModelV1.kt
rename to api/src/main/kotlin/de/chaosdorf/mete/model/TransactionModel.kt
index b3cf901..43bd5cb 100644
--- a/api/src/main/kotlin/de/chaosdorf/mete/v1/AuditEntryModelV1.kt
+++ b/api/src/main/kotlin/de/chaosdorf/mete/model/TransactionModel.kt
@@ -22,19 +22,14 @@
  * THE SOFTWARE.
  */
 
-package de.chaosdorf.mete.v1
+package de.chaosdorf.mete.model
 
-import de.chaosdorf.mete.AuditEntryId
-import de.chaosdorf.mete.DrinkId
 import kotlinx.datetime.Instant
-import kotlinx.serialization.SerialName
-import kotlinx.serialization.Serializable
+import java.math.BigDecimal
 
-@Serializable
-data class AuditEntryModelV1(
-  val id: AuditEntryId,
-  @SerialName("created_at")
-  val createdAt: Instant,
-  val difference: Double,
-  val drink: DrinkId?
-)
+interface TransactionModel {
+  val transactionId: TransactionId
+  val difference: BigDecimal
+  val drinkId: DrinkId?
+  val timestamp: Instant
+}
diff --git a/api/src/main/kotlin/de/chaosdorf/mete/UserId.kt b/api/src/main/kotlin/de/chaosdorf/mete/model/UserId.kt
similarity index 91%
rename from api/src/main/kotlin/de/chaosdorf/mete/UserId.kt
rename to api/src/main/kotlin/de/chaosdorf/mete/model/UserId.kt
index 1f72b51..0eaacf4 100644
--- a/api/src/main/kotlin/de/chaosdorf/mete/UserId.kt
+++ b/api/src/main/kotlin/de/chaosdorf/mete/model/UserId.kt
@@ -22,10 +22,12 @@
  * THE SOFTWARE.
  */
 
-package de.chaosdorf.mete
+package de.chaosdorf.mete.model
 
 import kotlinx.serialization.Serializable
 
 @Serializable
 @JvmInline
-value class UserId(val value: Long)
+value class UserId(val value: Long) {
+  override fun toString() = value.toString()
+}
diff --git a/api/src/main/kotlin/de/chaosdorf/mete/model/UserModel.kt b/api/src/main/kotlin/de/chaosdorf/mete/model/UserModel.kt
new file mode 100644
index 0000000..6d46048
--- /dev/null
+++ b/api/src/main/kotlin/de/chaosdorf/mete/model/UserModel.kt
@@ -0,0 +1,37 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2013-2023 Chaosdorf e.V.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package de.chaosdorf.mete.model
+
+import java.math.BigDecimal
+
+interface UserModel {
+  val userId: UserId
+  val active: Boolean
+  val name: String
+  val email: String
+  val balance: BigDecimal
+  val audit: Boolean
+  val redirect: Boolean
+}
diff --git a/api/src/main/kotlin/de/chaosdorf/mete/util/BigDecimalSerializer.kt b/api/src/main/kotlin/de/chaosdorf/mete/util/BigDecimalSerializer.kt
new file mode 100644
index 0000000..4e1a448
--- /dev/null
+++ b/api/src/main/kotlin/de/chaosdorf/mete/util/BigDecimalSerializer.kt
@@ -0,0 +1,44 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2013-2023 Chaosdorf e.V.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package de.chaosdorf.mete.util
+
+import kotlinx.serialization.KSerializer
+import kotlinx.serialization.descriptors.PrimitiveKind
+import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
+import kotlinx.serialization.descriptors.SerialDescriptor
+import kotlinx.serialization.encoding.Decoder
+import kotlinx.serialization.encoding.Encoder
+import java.math.BigDecimal
+
+object BigDecimalSerializer : KSerializer<BigDecimal> {
+  override val descriptor: SerialDescriptor =
+    PrimitiveSerialDescriptor("BigDecimal", PrimitiveKind.STRING)
+
+  override fun serialize(encoder: Encoder, value: BigDecimal) =
+    encoder.encodeString(value.toPlainString())
+
+  override fun deserialize(decoder: Decoder): BigDecimal =
+    BigDecimal(decoder.decodeString())
+}
diff --git a/api/src/main/kotlin/de/chaosdorf/mete/util/Code204Interceptor.kt b/api/src/main/kotlin/de/chaosdorf/mete/util/Code204Interceptor.kt
new file mode 100644
index 0000000..a64e372
--- /dev/null
+++ b/api/src/main/kotlin/de/chaosdorf/mete/util/Code204Interceptor.kt
@@ -0,0 +1,41 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2013-2023 Chaosdorf e.V.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package de.chaosdorf.mete.util
+
+import okhttp3.Interceptor
+import okhttp3.Response
+
+object Code204Interceptor : Interceptor {
+  override fun intercept(chain: Interceptor.Chain): Response {
+    val request = chain.request()
+    val response = chain.proceed(request)
+
+    return if (response.code == 204) {
+      response.newBuilder().code(200).build()
+    } else {
+      response
+    }
+  }
+}
diff --git a/api/src/main/kotlin/de/chaosdorf/mete/v1/BarcodeModelV1.kt b/api/src/main/kotlin/de/chaosdorf/mete/v1/BarcodeModelV1.kt
index 93f8461..ea35acb 100644
--- a/api/src/main/kotlin/de/chaosdorf/mete/v1/BarcodeModelV1.kt
+++ b/api/src/main/kotlin/de/chaosdorf/mete/v1/BarcodeModelV1.kt
@@ -24,12 +24,16 @@
 
 package de.chaosdorf.mete.v1
 
-import de.chaosdorf.mete.BarcodeId
-import de.chaosdorf.mete.DrinkId
+import de.chaosdorf.mete.model.BarcodeId
+import de.chaosdorf.mete.model.BarcodeModel
+import de.chaosdorf.mete.model.DrinkId
+import kotlinx.serialization.SerialName
 import kotlinx.serialization.Serializable
 
 @Serializable
-data class BarcodeModelV1(
-  val id: BarcodeId,
-  val drink: DrinkId,
-)
+internal data class BarcodeModelV1(
+  @SerialName("id")
+  override val barcodeId: BarcodeId,
+  @SerialName("drink")
+  override val drinkId: DrinkId,
+) : BarcodeModel
diff --git a/api/src/main/kotlin/de/chaosdorf/mete/v1/DrinkModelV1.kt b/api/src/main/kotlin/de/chaosdorf/mete/v1/DrinkModelV1.kt
index bba984b..30755a7 100644
--- a/api/src/main/kotlin/de/chaosdorf/mete/v1/DrinkModelV1.kt
+++ b/api/src/main/kotlin/de/chaosdorf/mete/v1/DrinkModelV1.kt
@@ -24,19 +24,30 @@
 
 package de.chaosdorf.mete.v1
 
-import de.chaosdorf.mete.DrinkId
+import de.chaosdorf.mete.model.DrinkId
+import de.chaosdorf.mete.model.DrinkModel
+import de.chaosdorf.mete.util.BigDecimalSerializer
 import kotlinx.datetime.Instant
 import kotlinx.serialization.SerialName
 import kotlinx.serialization.Serializable
+import java.math.BigDecimal
 
 @Serializable
-data class DrinkModelV1(
-  val id: DrinkId,
-  val name: String,
+internal data class DrinkModelV1(
+  @SerialName("id")
+  override val drinkId: DrinkId,
+  @SerialName("name")
+  override val name: String,
+  @SerialName("active")
+  override val active: Boolean,
   @SerialName("bottle_size")
-  val bottleSize: Double,
-  val caffeine: Int?,
-  val price: Double,
+  @Serializable(with = BigDecimalSerializer::class)
+  override val volume: BigDecimal,
+  @SerialName("caffeine")
+  override val caffeine: Int?,
+  @SerialName("price")
+  @Serializable(with = BigDecimalSerializer::class)
+  override val price: BigDecimal,
   @SerialName("logo_file_name")
   val logoFileName: String,
   @SerialName("created_at")
@@ -50,6 +61,5 @@ data class DrinkModelV1(
   @SerialName("logo_updated_at")
   val logoUpdatedAt: Instant,
   @SerialName("logo_url")
-  val logoUrl: String,
-  val active: Boolean
-)
+  override val logoUrl: String,
+) : DrinkModel
diff --git a/api/src/main/kotlin/de/chaosdorf/mete/v1/MeteApiV1.kt b/api/src/main/kotlin/de/chaosdorf/mete/v1/MeteApiV1.kt
index d08da96..7c25ea6 100644
--- a/api/src/main/kotlin/de/chaosdorf/mete/v1/MeteApiV1.kt
+++ b/api/src/main/kotlin/de/chaosdorf/mete/v1/MeteApiV1.kt
@@ -24,52 +24,71 @@
 
 package de.chaosdorf.mete.v1
 
-import de.chaosdorf.mete.DrinkId
-import de.chaosdorf.mete.PwaManifest
-import de.chaosdorf.mete.UserId
+import de.chaosdorf.mete.model.BarcodeId
+import de.chaosdorf.mete.model.DrinkId
+import de.chaosdorf.mete.model.MeteApi
+import de.chaosdorf.mete.model.PwaManifest
+import de.chaosdorf.mete.model.UserId
+import retrofit2.Response
 import retrofit2.http.GET
 import retrofit2.http.Path
 import retrofit2.http.Query
+import java.math.BigDecimal
 
-interface MeteApiV1 {
+internal interface MeteApiV1 : MeteApi {
   @GET("manifest.json")
-  suspend fun getManifest(): PwaManifest?
+  override suspend fun getManifest(): PwaManifest?
 
   @GET("api/v1/audits.json")
-  suspend fun getAudits(
-    @Query("user") user: Long? = null
-  ): AuditResponseV1
+  override suspend fun listTransactions(
+    @Query("user") user: UserId?
+  ): TransactionSummaryModelV1
 
   @GET("api/v1/barcodes.json")
-  suspend fun listBarcodes(): List<BarcodeModelV1>
+  override suspend fun listBarcodes(): List<BarcodeModelV1>
 
   @GET("api/v1/barcodes/{id}.json")
-  suspend fun getBarcode(): BarcodeModelV1?
+  override suspend fun getBarcode(
+    @Path("id") id: BarcodeId
+  ): BarcodeModelV1?
 
   @GET("api/v1/drinks.json")
-  suspend fun listDrinks(): List<DrinkModelV1>
+  override suspend fun listDrinks(): List<DrinkModelV1>
 
   @GET("api/v1/drinks/{id}.json")
-  suspend fun getDrink(@Path("id") id: DrinkId): DrinkModelV1?
+  override suspend fun getDrink(
+    @Path("id") id: DrinkId
+  ): DrinkModelV1?
 
   @GET("api/v1/users.json")
-  suspend fun listUsers(): List<UserModelV1>
+  override suspend fun listUsers(): List<UserModelV1>
 
   @GET("api/v1/users/{id}.json")
-  suspend fun getUser(@Path("id") id: UserId): UserModelV1?
+  override suspend fun getUser(
+    @Path("id") id: UserId
+  ): UserModelV1?
 
   @GET("api/v1/users/{id}/deposit.json")
-  suspend fun deposit(@Path("id") id: UserId)
+  override suspend fun deposit(
+    @Path("id") id: UserId,
+    @Query("amount") amount: BigDecimal
+  )
 
   @GET("api/v1/users/{id}/payment.json")
-  suspend fun payment(@Path("id") id: UserId)
+  override suspend fun withdraw(
+    @Path("id") id: UserId,
+    @Query("amount") amount: BigDecimal
+  )
 
   @GET("api/v1/users/{id}/buy.json")
-  suspend fun buy(@Path("id") id: UserId)
+  override suspend fun purchase(
+    @Path("id") id: UserId,
+    @Query("drink") drink: DrinkId
+  )
 
   @GET("api/v1/users/{id}/buy_barcode.json")
-  suspend fun buyWithBarcode(@Path("id") id: UserId)
-
-  @GET("api/v1/users/stats.json")
-  suspend fun getStats()
+  override suspend fun purchase(
+    @Path("id") id: UserId,
+    @Query("barcode") barcode: BarcodeId
+  )
 }
diff --git a/api/src/main/kotlin/de/chaosdorf/mete/v1/MeteApiV1Factory.kt b/api/src/main/kotlin/de/chaosdorf/mete/v1/MeteApiV1Factory.kt
index 2e51fec..3fdc434 100644
--- a/api/src/main/kotlin/de/chaosdorf/mete/v1/MeteApiV1Factory.kt
+++ b/api/src/main/kotlin/de/chaosdorf/mete/v1/MeteApiV1Factory.kt
@@ -25,19 +25,28 @@
 package de.chaosdorf.mete.v1
 
 import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
+import de.chaosdorf.mete.model.MeteApi
+import de.chaosdorf.mete.model.MeteApiFactory
+import de.chaosdorf.mete.util.Code204Interceptor
 import kotlinx.serialization.json.Json
 import okhttp3.MediaType.Companion.toMediaType
+import okhttp3.OkHttpClient
 import retrofit2.Retrofit
 import retrofit2.create
 
-object MeteApiV1Factory : MeteApiFactory<MeteApiV1> {
+object MeteApiV1Factory : MeteApiFactory {
   private val json = Json {
     ignoreUnknownKeys = true
   }
 
-  override fun newInstance(baseUrl: String): MeteApiV1 {
+  private val httpClient = OkHttpClient.Builder()
+    .addInterceptor(Code204Interceptor)
+    .build()
+
+  override fun newInstance(baseUrl: String): MeteApi {
     val contentType = "application/json".toMediaType()
     val retrofit = Retrofit.Builder()
+      .client(httpClient)
       .baseUrl(baseUrl)
       .addConverterFactory(json.asConverterFactory(contentType))
       .build()
diff --git a/api/src/main/kotlin/de/chaosdorf/mete/v1/TransactionModelV1.kt b/api/src/main/kotlin/de/chaosdorf/mete/v1/TransactionModelV1.kt
new file mode 100644
index 0000000..02a1b7b
--- /dev/null
+++ b/api/src/main/kotlin/de/chaosdorf/mete/v1/TransactionModelV1.kt
@@ -0,0 +1,47 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2013-2023 Chaosdorf e.V.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package de.chaosdorf.mete.v1
+
+import de.chaosdorf.mete.model.DrinkId
+import de.chaosdorf.mete.model.TransactionId
+import de.chaosdorf.mete.model.TransactionModel
+import de.chaosdorf.mete.util.BigDecimalSerializer
+import kotlinx.datetime.Instant
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+import java.math.BigDecimal
+
+@Serializable
+internal data class TransactionModelV1(
+  @SerialName("id")
+  override val transactionId: TransactionId,
+  @SerialName("difference")
+  @Serializable(with = BigDecimalSerializer::class)
+  override val difference: BigDecimal,
+  @SerialName("drink")
+  override val drinkId: DrinkId?,
+  @SerialName("created_at")
+  override val timestamp: Instant
+) : TransactionModel
diff --git a/api/src/main/kotlin/de/chaosdorf/mete/v1/TransactionSummaryModelV1.kt b/api/src/main/kotlin/de/chaosdorf/mete/v1/TransactionSummaryModelV1.kt
new file mode 100644
index 0000000..7edd234
--- /dev/null
+++ b/api/src/main/kotlin/de/chaosdorf/mete/v1/TransactionSummaryModelV1.kt
@@ -0,0 +1,46 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2013-2023 Chaosdorf e.V.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package de.chaosdorf.mete.v1
+
+import de.chaosdorf.mete.model.TransactionSummaryModel
+import de.chaosdorf.mete.util.BigDecimalSerializer
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+import java.math.BigDecimal
+
+@Serializable
+internal data class TransactionSummaryModelV1(
+  @SerialName("payments_sum")
+  @Serializable(with = BigDecimalSerializer::class)
+  override val payments: BigDecimal,
+  @SerialName("deposits_sum")
+  @Serializable(with = BigDecimalSerializer::class)
+  override val deposits: BigDecimal,
+  @SerialName("sum")
+  @Serializable(with = BigDecimalSerializer::class)
+  override val total: BigDecimal,
+  @SerialName("audits")
+  override val entries: List<TransactionModelV1>
+) : TransactionSummaryModel
diff --git a/api/src/main/kotlin/de/chaosdorf/mete/v1/UserModelV1.kt b/api/src/main/kotlin/de/chaosdorf/mete/v1/UserModelV1.kt
index 33c76ca..75d6edc 100644
--- a/api/src/main/kotlin/de/chaosdorf/mete/v1/UserModelV1.kt
+++ b/api/src/main/kotlin/de/chaosdorf/mete/v1/UserModelV1.kt
@@ -24,22 +24,33 @@
 
 package de.chaosdorf.mete.v1
 
-import de.chaosdorf.mete.UserId
+import de.chaosdorf.mete.model.UserId
+import de.chaosdorf.mete.model.UserModel
+import de.chaosdorf.mete.util.BigDecimalSerializer
 import kotlinx.datetime.Instant
 import kotlinx.serialization.SerialName
 import kotlinx.serialization.Serializable
+import java.math.BigDecimal
 
 @Serializable
-data class UserModelV1(
-  val id: UserId,
-  val name: String,
-  val email: String,
+internal data class UserModelV1(
+  @SerialName("id")
+  override val userId: UserId,
+  @SerialName("name")
+  override val name: String,
+  @SerialName("email")
+  override val email: String,
+  @SerialName("balance")
+  @Serializable(with = BigDecimalSerializer::class)
+  override val balance: BigDecimal,
+  @SerialName("active")
+  override val active: Boolean,
+  @SerialName("audit")
+  override val audit: Boolean,
+  @SerialName("redirect")
+  override val redirect: Boolean,
   @SerialName("created_at")
   val createdAt: Instant,
   @SerialName("updated_at")
   val updatedAt: Instant,
-  val balance: Double,
-  val active: Boolean,
-  val audit: Boolean,
-  val redirect: Boolean
-)
+) : UserModel
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index e68570b..64e87db 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -38,6 +38,7 @@ android {
         getDefaultProguardFile("proguard-android.txt"),
         "proguard-rules.pro"
       )
+      signingConfig = signingConfigs.getByName("debug")
     }
 
     getByName("debug") {
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/di/DatabaseModule.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/di/DatabaseModule.kt
index 8d35e31..c531e4c 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/di/DatabaseModule.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/di/DatabaseModule.kt
@@ -31,10 +31,12 @@ import dagger.Provides
 import dagger.hilt.InstallIn
 import dagger.hilt.android.qualifiers.ApplicationContext
 import dagger.hilt.components.SingletonComponent
+import de.chaosdorf.mete.model.MeteApiFactory
+import de.chaosdorf.mete.v1.MeteApiV1Factory
 import de.chaosdorf.meteroid.MeteroidDatabase
 import de.chaosdorf.meteroid.model.DrinkRepository
-import de.chaosdorf.meteroid.model.PurchaseRepository
 import de.chaosdorf.meteroid.model.ServerRepository
+import de.chaosdorf.meteroid.model.TransactionRepository
 import de.chaosdorf.meteroid.model.UserRepository
 import javax.inject.Singleton
 
@@ -61,12 +63,15 @@ object DatabaseModule {
   ): UserRepository = database.users()
 
   @Provides
-  fun providePurchaseRepository(
+  fun provideTransactionRepository(
     database: MeteroidDatabase
-  ): PurchaseRepository = database.purchases()
+  ): TransactionRepository = database.transactions()
 
   @Provides
   fun provideServerRepository(
     database: MeteroidDatabase
   ): ServerRepository = database.server()
+
+  @Provides
+  fun providesMeteFactory(): MeteApiFactory = MeteApiV1Factory
 }
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/sample/SampleDrinkProvider.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/sample/SampleDrinkProvider.kt
index c29e736..690b73c 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/sample/SampleDrinkProvider.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/sample/SampleDrinkProvider.kt
@@ -25,44 +25,31 @@
 package de.chaosdorf.meteroid.sample
 
 import androidx.compose.ui.tooling.preview.PreviewParameterProvider
-import de.chaosdorf.mete.DrinkId
+import de.chaosdorf.mete.model.DrinkId
 import de.chaosdorf.meteroid.model.Drink
 import de.chaosdorf.meteroid.model.ServerId
-import kotlinx.datetime.Instant
 
 class SampleDrinkProvider : PreviewParameterProvider<Drink> {
   override val values = sequenceOf(
-      Drink(
-          serverId = ServerId(-1),
-          drinkId = DrinkId(27),
-          active = true,
-          name = "Club Mate",
-          volume = 0.5,
-          caffeine = null,
-          price = 1.5,
-          createdAt = Instant.fromEpochMilliseconds(1684598011800),
-          updatedAt = Instant.fromEpochMilliseconds(1684607122132),
-          logoUrl = "http://192.168.188.36:8080/system/drinks/logos/000/000/027/thumb/logo.png",
-          logoFileName = "logo.png",
-          logoContentType = "image/png",
-          logoFileSize = 183063,
-          logoUpdatedAt = Instant.fromEpochMilliseconds(1684607121995)
-      ),
-      Drink(
-          serverId = ServerId(-1),
-          drinkId = DrinkId(15),
-          active = false,
-          name = "Paulaner Spezi",
-          volume = 0.5,
-          caffeine = null,
-          price = 1.5,
-          createdAt = Instant.fromEpochMilliseconds(1684597806099),
-          updatedAt = Instant.fromEpochMilliseconds(1684607346944),
-          logoUrl = "http://192.168.188.36:8080/system/drinks/logos/000/000/015/thumb/logo.png",
-          logoFileName = "logo.png",
-          logoContentType = "image/png",
-          logoFileSize = 173265,
-          logoUpdatedAt = Instant.fromEpochMilliseconds(1684607346835)
-      )
+    Drink(
+      serverId = ServerId(-1),
+      drinkId = DrinkId(27),
+      active = true,
+      name = "Club Mate",
+      volume = 0.5.toBigDecimal(),
+      caffeine = null,
+      price = 1.5.toBigDecimal(),
+      logoUrl = "http://192.168.188.36:8080/system/drinks/logos/000/000/027/thumb/logo.png",
+    ),
+    Drink(
+      serverId = ServerId(-1),
+      drinkId = DrinkId(15),
+      active = false,
+      name = "Paulaner Spezi",
+      volume = 0.5.toBigDecimal(),
+      caffeine = null,
+      price = 1.5.toBigDecimal(),
+      logoUrl = "http://192.168.188.36:8080/system/drinks/logos/000/000/015/thumb/logo.png",
+    )
   )
 }
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/storage/AccountPreferences.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/storage/AccountPreferences.kt
index 75fdfb2..3d0751f 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/storage/AccountPreferences.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/storage/AccountPreferences.kt
@@ -24,7 +24,7 @@
 
 package de.chaosdorf.meteroid.storage
 
-import de.chaosdorf.mete.UserId
+import de.chaosdorf.mete.model.UserId
 import de.chaosdorf.meteroid.model.ServerId
 import kotlinx.coroutines.flow.Flow
 
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/storage/AccountPreferencesImpl.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/storage/AccountPreferencesImpl.kt
index 71a6be1..62f4b80 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/storage/AccountPreferencesImpl.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/storage/AccountPreferencesImpl.kt
@@ -28,7 +28,7 @@ import androidx.datastore.core.DataStore
 import androidx.datastore.preferences.core.Preferences
 import androidx.datastore.preferences.core.edit
 import androidx.datastore.preferences.core.longPreferencesKey
-import de.chaosdorf.mete.UserId
+import de.chaosdorf.mete.model.UserId
 import de.chaosdorf.meteroid.model.ServerId
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.mapLatest
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/sync/DrinkSyncHandler.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/sync/DrinkSyncHandler.kt
index dc8e5ed..d9d2a16 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/sync/DrinkSyncHandler.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/sync/DrinkSyncHandler.kt
@@ -25,8 +25,8 @@
 package de.chaosdorf.meteroid.sync
 
 import androidx.room.withTransaction
-import de.chaosdorf.mete.DrinkId
-import de.chaosdorf.mete.v1.MeteApiV1Factory
+import de.chaosdorf.mete.model.DrinkId
+import de.chaosdorf.mete.model.MeteApiFactory
 import de.chaosdorf.meteroid.MeteroidDatabase
 import de.chaosdorf.meteroid.model.Drink
 import de.chaosdorf.meteroid.model.DrinkRepository
@@ -35,6 +35,7 @@ import de.chaosdorf.meteroid.model.ServerId
 import javax.inject.Inject
 
 class DrinkSyncHandler @Inject constructor(
+  private val factory: MeteApiFactory,
   private val db: MeteroidDatabase,
   private val repository: DrinkRepository
 ) : SyncHandler<Server, Drink, DrinkSyncHandler.Key>() {
@@ -57,8 +58,8 @@ class DrinkSyncHandler @Inject constructor(
     repository.getAll(context.serverId)
 
   override suspend fun loadCurrent(context: Server): List<Drink> {
-    val api = MeteApiV1Factory.newInstance(context.url)
+    val api = factory.newInstance(context.url)
     val loadedEntries = api.listDrinks()
-    return loadedEntries.map { Drink.fromModelV1(context, it) }
+    return loadedEntries.map { Drink.fromModel(context, it) }
   }
 }
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/sync/PurchaseSyncHandler.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/sync/PurchaseSyncHandler.kt
index 8ba6652..071c20c 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/sync/PurchaseSyncHandler.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/sync/PurchaseSyncHandler.kt
@@ -25,42 +25,43 @@
 package de.chaosdorf.meteroid.sync
 
 import androidx.room.withTransaction
-import de.chaosdorf.mete.AuditEntryId
-import de.chaosdorf.mete.UserId
-import de.chaosdorf.mete.v1.MeteApiV1Factory
+import de.chaosdorf.mete.model.MeteApiFactory
+import de.chaosdorf.mete.model.TransactionId
+import de.chaosdorf.mete.model.UserId
 import de.chaosdorf.meteroid.MeteroidDatabase
-import de.chaosdorf.meteroid.model.Purchase
-import de.chaosdorf.meteroid.model.PurchaseRepository
 import de.chaosdorf.meteroid.model.Server
 import de.chaosdorf.meteroid.model.ServerId
+import de.chaosdorf.meteroid.model.Transaction
+import de.chaosdorf.meteroid.model.TransactionRepository
 import javax.inject.Inject
 
-class PurchaseSyncHandler @Inject constructor(
+class TransactionSyncHandler @Inject constructor(
+  private val factory: MeteApiFactory,
   private val db: MeteroidDatabase,
-  private val repository: PurchaseRepository
-) : SyncHandler<Pair<Server, UserId>, Purchase, PurchaseSyncHandler.Key>() {
+  private val repository: TransactionRepository
+) : SyncHandler<Pair<Server, UserId>, Transaction, TransactionSyncHandler.Key>() {
   data class Key(
-    val server: ServerId, val purchase: AuditEntryId
+    val server: ServerId, val transaction: TransactionId
   )
 
   override suspend fun withTransaction(block: suspend () -> Unit) =
     db.withTransaction(block)
 
-  override suspend fun store(entry: Purchase) =
+  override suspend fun store(entry: Transaction) =
     repository.save(entry)
 
   override suspend fun delete(key: Key) =
-    repository.delete(key.server, key.purchase)
+    repository.delete(key.server, key.transaction)
 
-  override fun entryToKey(entry: Purchase) = Key(entry.serverId, entry.purchaseId)
+  override fun entryToKey(entry: Transaction) = Key(entry.serverId, entry.transactionId)
 
-  override suspend fun loadStored(context: Pair<Server, UserId>): List<Purchase> =
+  override suspend fun loadStored(context: Pair<Server, UserId>): List<Transaction> =
     repository.getAll(context.first.serverId, context.second)
 
-  override suspend fun loadCurrent(context: Pair<Server, UserId>): List<Purchase> {
+  override suspend fun loadCurrent(context: Pair<Server, UserId>): List<Transaction> {
     val (server, userId) = context
-    val api = MeteApiV1Factory.newInstance(server.url)
-    val loadedEntries = api.getAudits(user = userId.value).entries
-    return loadedEntries.map { Purchase.fromModelV1(server, userId, it) }
+    val api = factory.newInstance(server.url)
+    val loadedEntries = api.listTransactions(user = userId).entries
+    return loadedEntries.map { Transaction.fromModel(server, userId, it) }
   }
 }
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/sync/UserSyncHandler.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/sync/UserSyncHandler.kt
index a6f9291..179a5db 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/sync/UserSyncHandler.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/sync/UserSyncHandler.kt
@@ -25,8 +25,8 @@
 package de.chaosdorf.meteroid.sync
 
 import androidx.room.withTransaction
-import de.chaosdorf.mete.UserId
-import de.chaosdorf.mete.v1.MeteApiV1Factory
+import de.chaosdorf.mete.model.MeteApiFactory
+import de.chaosdorf.mete.model.UserId
 import de.chaosdorf.meteroid.MeteroidDatabase
 import de.chaosdorf.meteroid.model.Server
 import de.chaosdorf.meteroid.model.ServerId
@@ -35,6 +35,7 @@ import de.chaosdorf.meteroid.model.UserRepository
 import javax.inject.Inject
 
 class UserSyncHandler @Inject constructor(
+  private val factory: MeteApiFactory,
   private val db: MeteroidDatabase,
   private val repository: UserRepository
 ) : SyncHandler<Server, User, UserSyncHandler.Key>() {
@@ -57,8 +58,8 @@ class UserSyncHandler @Inject constructor(
     repository.getAll(context.serverId)
 
   override suspend fun loadCurrent(context: Server): List<User> {
-    val api = MeteApiV1Factory.newInstance(context.url)
+    val api = factory.newInstance(context.url)
     val loadedEntries = api.listUsers()
-    return loadedEntries.map { User.fromModelV1(context, it) }
+    return loadedEntries.map { User.fromModel(context, it) }
   }
 }
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/AppRouter.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/AppRouter.kt
index 81b5e89..6b2e654 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/AppRouter.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/AppRouter.kt
@@ -26,6 +26,7 @@ package de.chaosdorf.meteroid.ui
 
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.material3.CircularProgressIndicator
 import androidx.compose.material3.Text
 import androidx.compose.runtime.Composable
@@ -34,21 +35,23 @@ import androidx.compose.runtime.collectAsState
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.rememberCoroutineScope
 import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
 import androidx.hilt.navigation.compose.hiltViewModel
 import androidx.lifecycle.viewmodel.compose.viewModel
+import androidx.navigation.NavOptions
 import androidx.navigation.compose.NavHost
 import androidx.navigation.compose.composable
 import androidx.navigation.compose.navigation
 import androidx.navigation.compose.rememberNavController
+import de.chaosdorf.meteroid.ui.Transactions.TransactionListScreen
 import de.chaosdorf.meteroid.ui.drinks.DrinkListScreen
 import de.chaosdorf.meteroid.ui.drinks.DrinkListViewModel
 import de.chaosdorf.meteroid.ui.money.MoneyListScreen
 import de.chaosdorf.meteroid.ui.money.MoneyListViewModel
 import de.chaosdorf.meteroid.ui.navigation.Routes
-import de.chaosdorf.meteroid.ui.purchases.PurchaseListScreen
-import de.chaosdorf.meteroid.ui.purchases.PurchaseViewModel
 import de.chaosdorf.meteroid.ui.servers.AddServerScreen
 import de.chaosdorf.meteroid.ui.servers.ServerListScreen
+import de.chaosdorf.meteroid.ui.transactions.TransactionViewModel
 import de.chaosdorf.meteroid.ui.users.UserListScreen
 import kotlinx.coroutines.launch
 
@@ -60,18 +63,37 @@ fun AppRouter(viewModel: AppViewModel = viewModel()) {
 
   LaunchedEffect(initState) {
     when (initState) {
-      AppViewModel.InitState.LOADING -> navController.navigate(Routes.Init)
-      AppViewModel.InitState.CREATE_SERVER -> navController.navigate(Routes.Servers.Add)
-      AppViewModel.InitState.SELECT_SERVER -> navController.navigate(Routes.Servers.List)
-      AppViewModel.InitState.SELECT_USER -> navController.navigate(Routes.Users.List)
-      AppViewModel.InitState.HOME -> navController.navigate(Routes.Home.Root)
+      AppViewModel.InitState.LOADING -> navController.navigate(
+        Routes.Init,
+        NavOptions.Builder().setPopUpTo(Routes.Init, true).build()
+      )
+
+      AppViewModel.InitState.CREATE_SERVER -> navController.navigate(
+        Routes.Servers.Add,
+        NavOptions.Builder().setPopUpTo(Routes.Servers.Add, true).build()
+      )
+
+      AppViewModel.InitState.SELECT_SERVER -> navController.navigate(
+        Routes.Servers.List,
+        NavOptions.Builder().setPopUpTo(Routes.Servers.List, true).build()
+      )
+
+      AppViewModel.InitState.SELECT_USER -> navController.navigate(
+        Routes.Users.List,
+        NavOptions.Builder().setPopUpTo(Routes.Users.List, true).build()
+      )
+
+      AppViewModel.InitState.HOME -> navController.navigate(
+        Routes.Home.Root,
+        NavOptions.Builder().setPopUpTo(Routes.Home.Root, true).build()
+      )
     }
   }
 
   NavHost(navController, startDestination = Routes.Init) {
     composable(route = Routes.Init) { _ ->
-      Box(contentAlignment = Alignment.Center) {
-        Column {
+      Box(Modifier.fillMaxSize()) {
+        Column(Modifier.align(Alignment.Center)) {
           CircularProgressIndicator()
           Text("Loading")
         }
@@ -131,8 +153,8 @@ fun AppRouter(viewModel: AppViewModel = viewModel()) {
         MoneyListScreen(moneyListViewModel, navController::navigate)
       }
       composable(Routes.Home.History) { _ ->
-        val purchaseViewModel = hiltViewModel<PurchaseViewModel>()
-        PurchaseListScreen(purchaseViewModel, navController::navigate)
+        val transactionViewModel = hiltViewModel<TransactionViewModel>()
+        TransactionListScreen(transactionViewModel, navController::navigate)
       }
     }
   }
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/AppViewModel.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/AppViewModel.kt
index bd066bb..751902f 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/AppViewModel.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/AppViewModel.kt
@@ -27,14 +27,14 @@ package de.chaosdorf.meteroid.ui
 import androidx.lifecycle.ViewModel
 import androidx.lifecycle.viewModelScope
 import dagger.hilt.android.lifecycle.HiltViewModel
-import de.chaosdorf.mete.UserId
+import de.chaosdorf.mete.model.UserId
 import de.chaosdorf.meteroid.model.Server
 import de.chaosdorf.meteroid.model.ServerId
 import de.chaosdorf.meteroid.model.ServerRepository
 import de.chaosdorf.meteroid.storage.AccountPreferences
 import de.chaosdorf.meteroid.sync.AccountProvider
 import de.chaosdorf.meteroid.sync.DrinkSyncHandler
-import de.chaosdorf.meteroid.sync.PurchaseSyncHandler
+import de.chaosdorf.meteroid.sync.TransactionSyncHandler
 import de.chaosdorf.meteroid.sync.UserSyncHandler
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
@@ -51,9 +51,7 @@ class AppViewModel @Inject constructor(
   accountProvider: AccountProvider,
   private val accountPreferences: AccountPreferences,
   private val serverRepository: ServerRepository,
-  private val userSyncHandler: UserSyncHandler,
-  private val drinkSyncHandler: DrinkSyncHandler,
-  private val purchaseSyncHandler: PurchaseSyncHandler
+  private val syncManager: SyncManager
 ) : ViewModel() {
   val initState: StateFlow<InitState> = accountPreferences.state
     .flatMapLatest { preferences ->
@@ -69,12 +67,8 @@ class AppViewModel @Inject constructor(
 
   init {
     accountProvider.account.distinctUntilChanged().onEach { account ->
-      account?.let { (server, user) ->
-        userSyncHandler.sync(server)
-        drinkSyncHandler.sync(server)
-        user?.let { user ->
-          purchaseSyncHandler.sync(Pair(server, user.userId))
-        }
+      account?.let { (server, maybeUser) ->
+        syncManager.sync(server, maybeUser)
       }
     }.launchIn(viewModelScope)
   }
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/PriceBadge.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/PriceBadge.kt
new file mode 100644
index 0000000..7ded488
--- /dev/null
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/PriceBadge.kt
@@ -0,0 +1,62 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2013-2023 Chaosdorf e.V.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package de.chaosdorf.meteroid.ui
+
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Badge
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import java.math.BigDecimal
+
+@Composable
+fun PriceBadge(
+  price: BigDecimal,
+  modifier: Modifier = Modifier,
+  containerColor: Color =
+    if (price > BigDecimal.ZERO) MaterialTheme.colorScheme.primary
+    else MaterialTheme.colorScheme.error,
+  textColor: Color =
+    if (price > BigDecimal.ZERO) MaterialTheme.colorScheme.onPrimary
+    else MaterialTheme.colorScheme.onError,
+  textStyle: TextStyle = MaterialTheme.typography.labelLarge
+) {
+  Badge(
+    containerColor = containerColor,
+    modifier = modifier
+  ) {
+    Text(
+      "%.02f €".format(price),
+      style = textStyle,
+      color = textColor,
+      modifier = Modifier.padding(8.dp, 4.dp)
+    )
+  }
+}
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/SyncManager.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/SyncManager.kt
new file mode 100644
index 0000000..48276de
--- /dev/null
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/SyncManager.kt
@@ -0,0 +1,75 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2013-2023 Chaosdorf e.V.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package de.chaosdorf.meteroid.ui
+
+import de.chaosdorf.mete.model.MeteApiFactory
+import de.chaosdorf.meteroid.model.AccountInfo
+import de.chaosdorf.meteroid.model.Drink
+import de.chaosdorf.meteroid.model.Server
+import de.chaosdorf.meteroid.model.User
+import de.chaosdorf.meteroid.sync.DrinkSyncHandler
+import de.chaosdorf.meteroid.sync.TransactionSyncHandler
+import de.chaosdorf.meteroid.sync.UserSyncHandler
+import java.math.BigDecimal
+import javax.inject.Inject
+
+class SyncManager @Inject constructor(
+  private val factory: MeteApiFactory,
+  private val userSyncHandler: UserSyncHandler,
+  private val drinkSyncHandler: DrinkSyncHandler,
+  private val transactionSyncHandler: TransactionSyncHandler
+) {
+  suspend fun sync(server: Server, user: User?) {
+    userSyncHandler.sync(server)
+    drinkSyncHandler.sync(server)
+    if (user != null) {
+      transactionSyncHandler.sync(Pair(server, user.userId))
+    }
+  }
+
+  suspend fun purchase(account: AccountInfo, drink: Drink) {
+    account.user?.let { user ->
+      val api = factory.newInstance(account.server.url)
+      api.purchase(user.userId, drink.drinkId)
+      sync(account.server, user)
+    }
+  }
+
+  suspend fun deposit(account: AccountInfo, amount: BigDecimal) {
+    account.user?.let { user ->
+      val api = factory.newInstance(account.server.url)
+      api.deposit(user.userId, amount)
+      sync(account.server, user)
+    }
+  }
+
+  suspend fun withdraw(account: AccountInfo, amount: BigDecimal) {
+    account.user?.let { user ->
+      val api = factory.newInstance(account.server.url)
+      api.withdraw(user.userId, amount)
+      sync(account.server, user)
+    }
+  }
+}
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/drinks/DrinkListFilterChip.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/drinks/DrinkListFilterChip.kt
new file mode 100644
index 0000000..cdd66c3
--- /dev/null
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/drinks/DrinkListFilterChip.kt
@@ -0,0 +1,60 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2013-2023 Chaosdorf e.V.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package de.chaosdorf.meteroid.ui.drinks
+
+import androidx.compose.foundation.layout.size
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Check
+import androidx.compose.material3.FilterChip
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+
+@Composable
+fun DrinkListFilterChip(
+  label: String,
+  selected: Boolean,
+  onClick: () -> Unit,
+) {
+  FilterChip(
+    label = {
+      Text(label, style = MaterialTheme.typography.labelLarge)
+    },
+    selected = selected,
+    leadingIcon = {
+      if (selected) {
+        Icon(
+          Icons.Default.Check,
+          contentDescription = null,
+          modifier = Modifier.size(18.dp)
+        )
+      }
+    },
+    onClick = onClick
+  )
+}
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/drinks/DrinkListScreen.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/drinks/DrinkListScreen.kt
index e0708c3..b872aef 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/drinks/DrinkListScreen.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/drinks/DrinkListScreen.kt
@@ -26,12 +26,13 @@ package de.chaosdorf.meteroid.ui.drinks
 
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.ExperimentalLayoutApi
+import androidx.compose.foundation.layout.FlowRow
 import androidx.compose.foundation.layout.PaddingValues
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.lazy.grid.GridCells
 import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
 import androidx.compose.foundation.lazy.grid.items
-import androidx.compose.material3.LinearProgressIndicator
 import androidx.compose.material3.Scaffold
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.collectAsState
@@ -39,11 +40,11 @@ import androidx.compose.runtime.getValue
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.unit.dp
 import androidx.navigation.NavOptions
-import de.chaosdorf.meteroid.sync.SyncHandler
 import de.chaosdorf.meteroid.ui.navigation.HomeSections
 import de.chaosdorf.meteroid.ui.navigation.MeteroidBottomBar
 import de.chaosdorf.meteroid.ui.navigation.MeteroidTopBar
 
+@OptIn(ExperimentalLayoutApi::class)
 @Composable
 fun DrinkListScreen(
   viewModel: DrinkListViewModel,
@@ -51,7 +52,8 @@ fun DrinkListScreen(
 ) {
   val account by viewModel.account.collectAsState()
   val drinks by viewModel.drinks.collectAsState()
-  val syncState by viewModel.syncState.collectAsState()
+  val filters by viewModel.filters.collectAsState()
+
   Scaffold(
     topBar = { MeteroidTopBar(account, onNavigate) },
     bottomBar = {
@@ -62,18 +64,28 @@ fun DrinkListScreen(
       )
     }
   ) { paddingValues: PaddingValues ->
-    Column {
-      if (syncState == SyncHandler.State.Loading) {
-        LinearProgressIndicator()
+    Column(Modifier.padding(paddingValues)) {
+      FlowRow(
+        modifier = Modifier.padding(horizontal = 16.dp),
+        horizontalArrangement = Arrangement.spacedBy(8.dp)
+      ) {
+        DrinkListFilterChip(
+          label = "Active",
+          selected = filters.contains(DrinkListViewModel.Filter.Active),
+          onClick = { viewModel.toggleFilter(DrinkListViewModel.Filter.Active) }
+        )
+        DrinkListFilterChip(
+          label = "Coffeine Free",
+          selected = filters.contains(DrinkListViewModel.Filter.CaffeineFree),
+          onClick = { viewModel.toggleFilter(DrinkListViewModel.Filter.CaffeineFree) }
+        )
       }
       LazyVerticalGrid(
-        GridCells.Adaptive(120.dp),
-        modifier = Modifier.padding(paddingValues),
-        contentPadding = PaddingValues(12.dp),
-        horizontalArrangement = Arrangement.SpaceBetween,
+        GridCells.Adaptive(104.dp),
+        modifier = Modifier.padding(horizontal = 8.dp)
       ) {
         items(drinks) { drink ->
-          DrinkTile(drink)
+          DrinkTile(drink, viewModel::purchase)
         }
       }
     }
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/drinks/DrinkListViewModel.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/drinks/DrinkListViewModel.kt
index bca3d10..045d375 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/drinks/DrinkListViewModel.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/drinks/DrinkListViewModel.kt
@@ -24,39 +24,73 @@
 
 package de.chaosdorf.meteroid.ui.drinks
 
+import androidx.lifecycle.SavedStateHandle
 import androidx.lifecycle.ViewModel
 import androidx.lifecycle.viewModelScope
 import dagger.hilt.android.lifecycle.HiltViewModel
 import de.chaosdorf.meteroid.model.AccountInfo
 import de.chaosdorf.meteroid.model.Drink
 import de.chaosdorf.meteroid.model.DrinkRepository
-import de.chaosdorf.meteroid.model.Server
-import de.chaosdorf.meteroid.model.User
 import de.chaosdorf.meteroid.sync.AccountProvider
-import de.chaosdorf.meteroid.sync.DrinkSyncHandler
-import de.chaosdorf.meteroid.sync.SyncHandler
+import de.chaosdorf.meteroid.ui.SyncManager
+import de.chaosdorf.meteroid.util.update
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.flatMapLatest
 import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.launch
 import javax.inject.Inject
 
 @HiltViewModel
 class DrinkListViewModel @Inject constructor(
   accountProvider: AccountProvider,
   repository: DrinkRepository,
-  syncHandler: DrinkSyncHandler
+  private val syncManager: SyncManager,
+  private val savedStateHandle: SavedStateHandle
 ) : ViewModel() {
   val account: StateFlow<AccountInfo?> = accountProvider.account
     .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
 
-  val drinks: StateFlow<List<Drink>> = accountProvider.account
-    .flatMapLatest { account ->
+  val filters: StateFlow<Set<Filter>> =
+    savedStateHandle.getStateFlow("filters", setOf(Filter.Active))
+
+  val drinks: StateFlow<List<Drink>> = combine(
+    accountProvider.account.flatMapLatest { account ->
       account?.let { (server, _) ->
         repository.getAllFlow(server.serverId)
       } ?: flowOf(emptyList())
-    }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList())
+    },
+    filters
+  ) { drinks, filters ->
+    drinks.filter { item ->
+      filters.all { filter -> filter.matches(item) }
+    }
+  }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList())
+
+  fun toggleFilter(filter: Filter) {
+    savedStateHandle.update<Set<Filter>>("filters", emptySet()) { filters ->
+      if (filters.contains(filter)) filters - filter
+      else filters + filter
+    }
+  }
+
+  fun purchase(item: Drink) {
+    account.value?.let { account ->
+      viewModelScope.launch {
+        syncManager.purchase(account, item)
+      }
+    }
+  }
+
+  enum class Filter {
+    CaffeineFree,
+    Active;
 
-  val syncState: StateFlow<SyncHandler.State> = syncHandler.state
+    fun matches(item: Drink) = when (this) {
+      CaffeineFree -> item.caffeine == 0
+      Active -> item.active
+    }
+  }
 }
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/drinks/DrinkTile.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/drinks/DrinkTile.kt
index 2099f68..7c141b9 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/drinks/DrinkTile.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/drinks/DrinkTile.kt
@@ -26,13 +26,18 @@ package de.chaosdorf.meteroid.ui.drinks
 
 import androidx.compose.foundation.Image
 import androidx.compose.foundation.background
-import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.clickable
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.IntrinsicSize
 import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.aspectRatio
+import androidx.compose.foundation.layout.fillMaxHeight
 import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
 import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.paddingFromBaseline
 import androidx.compose.foundation.shape.CircleShape
 import androidx.compose.foundation.shape.RoundedCornerShape
 import androidx.compose.material3.MaterialTheme
@@ -40,21 +45,24 @@ import androidx.compose.material3.Text
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.alpha
 import androidx.compose.ui.draw.clip
 import androidx.compose.ui.layout.ContentScale
 import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.text.style.TextAlign
 import androidx.compose.ui.tooling.preview.Preview
 import androidx.compose.ui.tooling.preview.PreviewParameter
 import androidx.compose.ui.unit.dp
 import coil.compose.rememberAsyncImagePainter
 import de.chaosdorf.meteroid.model.Drink
 import de.chaosdorf.meteroid.sample.SampleDrinkProvider
+import de.chaosdorf.meteroid.ui.PriceBadge
 
 @Preview(widthDp = 120, showBackground = true)
 @Composable
 fun DrinkTile(
-  @PreviewParameter(SampleDrinkProvider::class) item: Drink
+  @PreviewParameter(SampleDrinkProvider::class) item: Drink,
+  onPurchase: (Drink) -> Unit = {}
 ) {
   val thumbPainter = rememberAsyncImagePainter(
     item.logoUrl
@@ -65,7 +73,12 @@ fun DrinkTile(
   )
 
   Column(
-    modifier = Modifier.padding(4.dp)
+    modifier = Modifier
+      .height(IntrinsicSize.Max)
+      .alpha(if (item.active) 1.0f else 0.67f)
+      .clip(RoundedCornerShape(8.dp))
+      .clickable { onPurchase(item) }
+      .padding(8.dp)
   ) {
     Box {
       Image(
@@ -73,39 +86,38 @@ fun DrinkTile(
         contentDescription = null,
         contentScale = ContentScale.Fit,
         modifier = Modifier
+          .aspectRatio(1.0f)
           .clip(CircleShape)
           .background(MaterialTheme.colorScheme.primaryContainer)
-          .aspectRatio(1.0f)
-          .padding(8.dp)
       )
-      Text(
-        String.format("%.02f €", item.price),
-        color = MaterialTheme.colorScheme.onPrimary,
-        fontWeight = FontWeight.SemiBold,
+      PriceBadge(
+        item.price,
         modifier = Modifier
-          .padding(vertical = 12.dp)
-          .clip(RoundedCornerShape(16.dp))
-          .background(MaterialTheme.colorScheme.primary)
           .align(Alignment.BottomEnd)
-          .padding(horizontal = 8.dp)
+          .paddingFromBaseline(bottom = 12.dp)
       )
     }
+    Spacer(Modifier.height(4.dp))
     Text(
       item.name,
-      fontWeight = FontWeight.SemiBold,
-      maxLines = 1,
-      overflow = TextOverflow.Ellipsis,
-      modifier = Modifier.padding(horizontal = 4.dp)
-    )
-    Row(
-      horizontalArrangement = Arrangement.SpaceBetween,
       modifier = Modifier
-        .padding(horizontal = 4.dp)
         .fillMaxWidth()
-    ) {
+        .padding(horizontal = 8.dp),
+      textAlign = TextAlign.Center,
+      fontWeight = FontWeight.SemiBold,
+      style = MaterialTheme.typography.labelLarge,
+    )
+    Spacer(Modifier.height(4.dp))
+    Row(modifier = Modifier.align(Alignment.CenterHorizontally)) {
       Text(
-        String.format("%.02f l", item.volume),
-        color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.8f)
+        String.format("%.02fl · %.02f€/l", item.volume, item.price / item.volume),
+        modifier = Modifier
+          .fillMaxWidth()
+          .padding(horizontal = 8.dp),
+        textAlign = TextAlign.Center,
+        fontWeight = FontWeight.SemiBold,
+        style = MaterialTheme.typography.labelMedium,
+        color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f)
       )
     }
   }
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/money/MoneyListScreen.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/money/MoneyListScreen.kt
index 14386ce..78b8d66 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/money/MoneyListScreen.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/money/MoneyListScreen.kt
@@ -67,7 +67,7 @@ fun MoneyListScreen(
         horizontalArrangement = Arrangement.SpaceBetween,
       ) {
         items(viewModel.money) { monetaryAmount ->
-          MoneyTile(monetaryAmount)
+          MoneyTile(monetaryAmount, viewModel::deposit)
         }
       }
     }
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/money/MoneyListViewModel.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/money/MoneyListViewModel.kt
index 508ccec..7ba2e82 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/money/MoneyListViewModel.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/money/MoneyListViewModel.kt
@@ -28,30 +28,43 @@ import androidx.annotation.DrawableRes
 import androidx.lifecycle.ViewModel
 import androidx.lifecycle.viewModelScope
 import dagger.hilt.android.lifecycle.HiltViewModel
+import de.chaosdorf.mete.model.MeteApiFactory
 import de.chaosdorf.meteroid.R
 import de.chaosdorf.meteroid.model.AccountInfo
 import de.chaosdorf.meteroid.sync.AccountProvider
+import de.chaosdorf.meteroid.ui.SyncManager
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.launch
+import java.math.BigDecimal
 import javax.inject.Inject
 
-enum class MonetaryAmount(val amount: Double, @DrawableRes val image: Int) {
-  MONEY_50(0.50, R.drawable.euro_50),
-  MONEY_100(1.00, R.drawable.euro_100),
-  MONEY_200(2.00, R.drawable.euro_200),
-  MONEY_500(5.00, R.drawable.euro_500),
-  MONEY_1000(10.00, R.drawable.euro_1000),
-  MONEY_2000(20.00, R.drawable.euro_2000),
-  MONEY_5000(50.00, R.drawable.euro_5000),
+enum class MonetaryAmount(val amount: BigDecimal, @DrawableRes val image: Int) {
+  MONEY_50(0.50.toBigDecimal(), R.drawable.euro_50),
+  MONEY_100(1.00.toBigDecimal(), R.drawable.euro_100),
+  MONEY_200(2.00.toBigDecimal(), R.drawable.euro_200),
+  MONEY_500(5.00.toBigDecimal(), R.drawable.euro_500),
+  MONEY_1000(10.00.toBigDecimal(), R.drawable.euro_1000),
+  MONEY_2000(20.00.toBigDecimal(), R.drawable.euro_2000),
+  MONEY_5000(50.00.toBigDecimal(), R.drawable.euro_5000),
 }
 
 @HiltViewModel
 class MoneyListViewModel @Inject constructor(
-  accountProvider: AccountProvider
+  accountProvider: AccountProvider,
+  private val syncManager: SyncManager
 ) : ViewModel() {
   val account: StateFlow<AccountInfo?> = accountProvider.account
     .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
 
   val money: List<MonetaryAmount> = MonetaryAmount.entries
+
+  fun deposit(item: MonetaryAmount) {
+    account.value?.let { account ->
+      viewModelScope.launch {
+        syncManager.deposit(account, item.amount)
+      }
+    }
+  }
 }
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/money/MoneyTile.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/money/MoneyTile.kt
index 5ec0760..5254c6f 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/money/MoneyTile.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/money/MoneyTile.kt
@@ -26,50 +26,47 @@ package de.chaosdorf.meteroid.ui.money
 
 import androidx.compose.foundation.Image
 import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
 import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.IntrinsicSize
 import androidx.compose.foundation.layout.aspectRatio
-import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.paddingFromBaseline
 import androidx.compose.foundation.shape.CircleShape
 import androidx.compose.foundation.shape.RoundedCornerShape
 import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Text
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.draw.clip
 import androidx.compose.ui.layout.ContentScale
 import androidx.compose.ui.res.painterResource
-import androidx.compose.ui.text.font.FontWeight
 import androidx.compose.ui.unit.dp
+import de.chaosdorf.meteroid.ui.PriceBadge
 
 @Composable
 fun MoneyTile(
-  item: MonetaryAmount
+  item: MonetaryAmount,
+  onDeposit: (MonetaryAmount) -> Unit = {}
 ) {
-  Column(
-    modifier = Modifier.padding(4.dp)
+  Box(
+    modifier = Modifier
+      .height(IntrinsicSize.Max)
+      .clip(RoundedCornerShape(8.dp))
+      .clickable { onDeposit(item) }
   ) {
-    Box {
-      Image(
-        painterResource(item.image),
-        contentDescription = null,
-        contentScale = ContentScale.Fit,
-        modifier = Modifier
-          .aspectRatio(1.0f)
-          .padding(8.dp)
-      )
-      Text(
-        String.format("%.02f €", item.amount),
-        color = MaterialTheme.colorScheme.onPrimary,
-        fontWeight = FontWeight.SemiBold,
-        modifier = Modifier
-          .padding(vertical = 12.dp)
-          .clip(RoundedCornerShape(16.dp))
-          .background(MaterialTheme.colorScheme.primary)
-          .align(Alignment.BottomEnd)
-          .padding(horizontal = 8.dp)
-      )
-    }
+    Image(
+      painterResource(item.image),
+      contentDescription = null,
+      contentScale = ContentScale.Fit,
+      modifier = Modifier
+        .aspectRatio(1.0f)
+    )
+    PriceBadge(
+      item.amount,
+      modifier = Modifier
+        .align(Alignment.BottomEnd)
+        .paddingFromBaseline(bottom = 24.dp)
+    )
   }
 }
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/MeteroidTopBar.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/MeteroidTopBar.kt
index e796d4f..3a5d95e 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/MeteroidTopBar.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/MeteroidTopBar.kt
@@ -25,93 +25,94 @@
 package de.chaosdorf.meteroid.ui.navigation
 
 import androidx.compose.foundation.background
-import androidx.compose.foundation.border
-import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
 import androidx.compose.foundation.shape.CircleShape
 import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.material3.IconButton
 import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
 import androidx.compose.material3.Text
-import androidx.compose.material3.TopAppBar
-import androidx.compose.material3.TopAppBarColors
-import androidx.compose.material3.TopAppBarDefaults
 import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.draw.clip
-import androidx.compose.ui.draw.shadow
-import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.layout.ContentScale
 import androidx.compose.ui.text.font.FontWeight
 import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.sp
 import androidx.navigation.NavOptions
 import coil.compose.AsyncImage
 import de.chaosdorf.meteroid.model.AccountInfo
-import de.chaosdorf.meteroid.model.Server
-import de.chaosdorf.meteroid.model.User
+import de.chaosdorf.meteroid.ui.PriceBadge
+import okhttp3.HttpUrl.Companion.toHttpUrl
+import okhttp3.internal.toCanonicalHost
 
 @Composable
 fun MeteroidTopBar(
   account: AccountInfo?,
   onNavigate: (String, NavOptions) -> Unit
 ) {
-  TopAppBar(
-    title = {
-      Text(
-        account?.user?.name
-          ?: account?.server?.name
-          ?: "Meteroid"
+  Surface(
+    modifier = Modifier.padding(8.dp),
+    color = MaterialTheme.colorScheme.surface,
+    shadowElevation = 6.dp,
+    tonalElevation = 6.dp,
+    shape = RoundedCornerShape(32.dp),
+    onClick = {
+      onNavigate(Routes.Users.List, NavOptions.Builder().build())
+    }
+  ) {
+    Row(modifier = Modifier.padding(8.dp)) {
+      AsyncImage(
+        account?.user?.gravatarUrl(),
+        contentDescription = "User List",
+        contentScale = ContentScale.Crop,
+        modifier = Modifier
+          .size(48.dp)
+          .clip(CircleShape)
+          .background(MaterialTheme.colorScheme.tertiary)
       )
-    },
-    navigationIcon = {
-        AsyncImage(
-          account?.user?.gravatarUrl(),
-          contentDescription = "User List",
-          contentScale = ContentScale.Crop,
-          modifier = Modifier
-            .padding(start = 6.dp, end = 24.dp)
-            .size(40.dp)
-            .clip(CircleShape)
-            .border(1.dp, Color.White, CircleShape)
-            .background(MaterialTheme.colorScheme.primaryContainer)
-        )
-    },
-    actions = {
+      Spacer(Modifier.width(16.dp))
+      Column(modifier = Modifier.align(Alignment.CenterVertically)) {
+        if (account != null) {
+          if (account.user != null) {
+            Text(
+              account.user!!.name,
+              fontWeight = FontWeight.SemiBold
+            )
+            Text(
+              account.server.url.toHttpUrl().host,
+              color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.67f),
+              fontWeight = FontWeight.Medium
+            )
+          } else {
+            Text(
+              account.server.url.toHttpUrl().host,
+              fontWeight = FontWeight.SemiBold
+            )
+          }
+        } else {
+          Text(
+            "Meteroid",
+            fontWeight = FontWeight.SemiBold
+          )
+        }
+      }
+      Spacer(
+        Modifier
+          .weight(1.0f)
+          .width(16.dp))
       account?.user?.let { user ->
-        val (foreground, background) =
-          if (user.balance < 0)
-            Pair(MaterialTheme.colorScheme.onError, MaterialTheme.colorScheme.error)
-          else
-            Pair(MaterialTheme.colorScheme.onPrimary, MaterialTheme.colorScheme.primary)
-
-        Text(
-          String.format("%.02f €", user.balance),
-          color = foreground,
-          fontSize = 14.sp,
-          lineHeight = 20.sp,
-          fontWeight = FontWeight.SemiBold,
+        PriceBadge(
+          user.balance,
           modifier = Modifier
-            .padding(end = 20.dp)
-            .clip(RoundedCornerShape(16.dp))
-            .background(background)
-            .padding(horizontal = 8.dp)
+            .align(Alignment.CenterVertically)
+            .padding(end = 12.dp)
         )
       }
-    },
-    modifier = Modifier
-      .clickable {
-        onNavigate(
-          Routes.Users.Root,
-          NavOptions
-            .Builder()
-            .build()
-        )
-      },
-    colors = TopAppBarDefaults.topAppBarColors(
-      containerColor = MaterialTheme.colorScheme.primaryContainer,
-      titleContentColor = MaterialTheme.colorScheme.onPrimaryContainer
-    )
-  )
+    }
+  }
 }
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/servers/AddServerViewModel.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/servers/AddServerViewModel.kt
index 3689ee0..7950b83 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/servers/AddServerViewModel.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/servers/AddServerViewModel.kt
@@ -27,7 +27,7 @@ package de.chaosdorf.meteroid.ui.servers
 import androidx.lifecycle.ViewModel
 import androidx.lifecycle.viewModelScope
 import dagger.hilt.android.lifecycle.HiltViewModel
-import de.chaosdorf.mete.v1.MeteApiV1Factory
+import de.chaosdorf.mete.model.MeteApiFactory
 import de.chaosdorf.meteroid.model.Server
 import de.chaosdorf.meteroid.model.ServerId
 import de.chaosdorf.meteroid.model.ServerRepository
@@ -44,6 +44,7 @@ import kotlin.time.Duration.Companion.milliseconds
 
 @HiltViewModel
 class AddServerViewModel @Inject constructor(
+  private val factory: MeteApiFactory,
   private val repository: ServerRepository
 ) : ViewModel() {
   val url = MutableStateFlow("")
@@ -52,7 +53,7 @@ class AddServerViewModel @Inject constructor(
     id: ServerId,
     url: String
   ): Server? = try {
-    val api = MeteApiV1Factory.newInstance(url)
+    val api = factory.newInstance(url)
     val manifest = api.getManifest()
     val icon = manifest?.findBestIcon()
     Server(
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/purchases/PurchaseListItem.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/transactions/PurchaseListItem.kt
similarity index 77%
rename from app/src/main/kotlin/de/chaosdorf/meteroid/ui/purchases/PurchaseListItem.kt
rename to app/src/main/kotlin/de/chaosdorf/meteroid/ui/transactions/PurchaseListItem.kt
index ed56a75..5b5c668 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/purchases/PurchaseListItem.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/transactions/PurchaseListItem.kt
@@ -22,7 +22,7 @@
  * THE SOFTWARE.
  */
 
-package de.chaosdorf.meteroid.ui.purchases
+package de.chaosdorf.meteroid.ui.transactions
 
 import androidx.compose.foundation.Image
 import androidx.compose.foundation.background
@@ -32,7 +32,6 @@ import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.size
 import androidx.compose.foundation.shape.CircleShape
-import androidx.compose.foundation.shape.RoundedCornerShape
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.filled.AttachMoney
 import androidx.compose.material.icons.filled.QuestionMark
@@ -45,32 +44,33 @@ import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.draw.clip
 import androidx.compose.ui.layout.ContentScale
-import androidx.compose.ui.text.font.FontWeight
 import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.sp
 import coil.compose.rememberAsyncImagePainter
 import de.chaosdorf.meteroid.model.Drink
-import de.chaosdorf.meteroid.model.Purchase
+import de.chaosdorf.meteroid.model.Transaction
+import de.chaosdorf.meteroid.ui.PriceBadge
 import kotlinx.datetime.TimeZone
 import kotlinx.datetime.toJavaLocalDateTime
 import kotlinx.datetime.toLocalDateTime
+import java.math.BigDecimal
 import java.time.format.DateTimeFormatter
 import java.time.format.FormatStyle
 
 @Composable
-fun PurchaseListItem(
-  purchase: Purchase,
+fun TransactionListItem(
+  transaction: Transaction,
   drink: Drink?
 ) {
-  val timestamp = purchase.createdAt.toLocalDateTime(TimeZone.currentSystemDefault())
+  val timestamp = transaction.timestamp.toLocalDateTime(TimeZone.currentSystemDefault())
   val formatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.SHORT)
 
   ListItem(
     headlineContent = {
-      val label =
-        if (drink != null) drink.name
-        else if (purchase.difference > 0.0) "Deposit"
-        else "Unknown"
+      val label = when {
+        drink != null -> drink.name
+        transaction.difference > BigDecimal.ZERO -> "Deposit"
+        else -> "Unknown"
+      }
       Text(label)
     },
     supportingContent = {
@@ -101,7 +101,7 @@ fun PurchaseListItem(
               .align(Alignment.Center)
               .fillMaxSize()
           )
-        } else if (purchase.difference > 0) {
+        } else if (transaction.difference > BigDecimal.ZERO) {
           Icon(
             Icons.Default.AttachMoney,
             contentDescription = null,
@@ -117,22 +117,9 @@ fun PurchaseListItem(
       }
     },
     trailingContent = {
-      val (foreground, background) =
-        if (purchase.difference < 0)
-          Pair(MaterialTheme.colorScheme.onError, MaterialTheme.colorScheme.error)
-        else
-          Pair(MaterialTheme.colorScheme.onPrimary, MaterialTheme.colorScheme.primary)
-
-      Text(
-        String.format("%.02f €", purchase.difference),
-        color = foreground,
-        fontSize = 14.sp,
-        lineHeight = 20.sp,
-        fontWeight = FontWeight.SemiBold,
-        modifier = Modifier
-          .clip(RoundedCornerShape(16.dp))
-          .background(background)
-          .padding(horizontal = 8.dp)
+      PriceBadge(
+        transaction.difference,
+        modifier = Modifier.padding(horizontal = 8.dp)
       )
     }
   )
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/purchases/PurchaseListScreen.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/transactions/PurchaseListScreen.kt
similarity index 77%
rename from app/src/main/kotlin/de/chaosdorf/meteroid/ui/purchases/PurchaseListScreen.kt
rename to app/src/main/kotlin/de/chaosdorf/meteroid/ui/transactions/PurchaseListScreen.kt
index ac31c25..d6ceedb 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/purchases/PurchaseListScreen.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/transactions/PurchaseListScreen.kt
@@ -22,33 +22,31 @@
  * THE SOFTWARE.
  */
 
-package de.chaosdorf.meteroid.ui.purchases
+package de.chaosdorf.meteroid.ui.Transactions
 
-import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.PaddingValues
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.lazy.LazyColumn
 import androidx.compose.foundation.lazy.items
-import androidx.compose.material3.LinearProgressIndicator
 import androidx.compose.material3.Scaffold
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.collectAsState
 import androidx.compose.runtime.getValue
 import androidx.compose.ui.Modifier
 import androidx.navigation.NavOptions
-import de.chaosdorf.meteroid.sync.SyncHandler
 import de.chaosdorf.meteroid.ui.navigation.HomeSections
 import de.chaosdorf.meteroid.ui.navigation.MeteroidBottomBar
 import de.chaosdorf.meteroid.ui.navigation.MeteroidTopBar
+import de.chaosdorf.meteroid.ui.transactions.TransactionListItem
+import de.chaosdorf.meteroid.ui.transactions.TransactionViewModel
 
 @Composable
-fun PurchaseListScreen(
-  viewModel: PurchaseViewModel,
+fun TransactionListScreen(
+  viewModel: TransactionViewModel,
   onNavigate: (String, NavOptions) -> Unit
 ) {
   val account by viewModel.account.collectAsState()
-  val purchases by viewModel.purchases.collectAsState()
-  val syncState by viewModel.syncState.collectAsState()
+  val transactions by viewModel.transactions.collectAsState()
 
   Scaffold(
     topBar = { MeteroidTopBar(account, onNavigate) },
@@ -60,14 +58,9 @@ fun PurchaseListScreen(
       )
     }
   ) { paddingValues: PaddingValues ->
-    Column {
-      if (syncState == SyncHandler.State.Loading) {
-        LinearProgressIndicator()
-      }
-      LazyColumn(modifier = Modifier.padding(paddingValues)) {
-        items(purchases) { (purchase, drink) ->
-          PurchaseListItem(purchase, drink)
-        }
+    LazyColumn(contentPadding = paddingValues) {
+      items(transactions) { (transaction, drink) ->
+        TransactionListItem(transaction, drink)
       }
     }
   }
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/purchases/PurchaseViewModel.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/transactions/PurchaseViewModel.kt
similarity index 70%
rename from app/src/main/kotlin/de/chaosdorf/meteroid/ui/purchases/PurchaseViewModel.kt
rename to app/src/main/kotlin/de/chaosdorf/meteroid/ui/transactions/PurchaseViewModel.kt
index b31def9..8c053ee 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/purchases/PurchaseViewModel.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/transactions/PurchaseViewModel.kt
@@ -22,17 +22,17 @@
  * THE SOFTWARE.
  */
 
-package de.chaosdorf.meteroid.ui.purchases
+package de.chaosdorf.meteroid.ui.transactions
 
 import androidx.lifecycle.ViewModel
 import androidx.lifecycle.viewModelScope
 import dagger.hilt.android.lifecycle.HiltViewModel
 import de.chaosdorf.meteroid.model.AccountInfo
 import de.chaosdorf.meteroid.model.DrinkRepository
-import de.chaosdorf.meteroid.model.PurchaseRepository
+import de.chaosdorf.meteroid.model.TransactionRepository
 import de.chaosdorf.meteroid.sync.AccountProvider
-import de.chaosdorf.meteroid.sync.PurchaseSyncHandler
 import de.chaosdorf.meteroid.sync.SyncHandler
+import de.chaosdorf.meteroid.sync.TransactionSyncHandler
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.combine
@@ -40,31 +40,31 @@ import kotlinx.coroutines.flow.flatMapLatest
 import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.flow.mapLatest
 import kotlinx.coroutines.flow.stateIn
+import java.math.BigDecimal
 import javax.inject.Inject
 import kotlin.time.Duration.Companion.minutes
 
 @HiltViewModel
-class PurchaseViewModel @Inject constructor(
+class TransactionViewModel @Inject constructor(
   accountProvider: AccountProvider,
-  repository: PurchaseRepository,
-  drinkRepository: DrinkRepository,
-  syncHandler: PurchaseSyncHandler
+  repository: TransactionRepository,
+  drinkRepository: DrinkRepository
 ) : ViewModel() {
   val account: StateFlow<AccountInfo?> = accountProvider.account
     .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
 
-  val purchases: StateFlow<List<PurchaseInfo>> = accountProvider.account
+  val transactions: StateFlow<List<TransactionInfo>> = accountProvider.account
     .flatMapLatest { account ->
       account?.let { (server, maybeUser) ->
         maybeUser?.let { user ->
           combine(
             repository.getAllFlow(server.serverId, user.userId),
             drinkRepository.getAllFlow(server.serverId)
-          ) { purchases, drinks ->
-            purchases.map { purchase ->
-              PurchaseInfo(
-                purchase,
-                drinks.firstOrNull { drink -> drink.drinkId == purchase.drinkId }
+          ) { transactions, drinks ->
+            transactions.map { transaction ->
+              TransactionInfo(
+                transaction,
+                drinks.firstOrNull { drink -> drink.drinkId == transaction.drinkId }
               )
             }
           }
@@ -72,28 +72,26 @@ class PurchaseViewModel @Inject constructor(
       } ?: flowOf(emptyList())
     }.mapLatest { list ->
       list.mergeAdjecentDeposits()
-        .filter { it.drink != null || it.purchase.difference != 0.0 }
+        .filter { it.drink != null || it.transaction.difference != BigDecimal.ZERO }
     }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList())
-
-  val syncState: StateFlow<SyncHandler.State> = syncHandler.state
 }
 
-fun List<PurchaseInfo>.mergeAdjecentDeposits(): List<PurchaseInfo> {
-  val result = mutableListOf<PurchaseInfo>()
+fun List<TransactionInfo>.mergeAdjecentDeposits(): List<TransactionInfo> {
+  val result = mutableListOf<TransactionInfo>()
   for (entry in this) {
     val previous = result.lastOrNull()
     if (previous != null
-      && previous.purchase.difference > 0
-      && entry.purchase.difference > 0
+      && previous.transaction.difference > BigDecimal.ZERO
+      && entry.transaction.difference > BigDecimal.ZERO
       && previous.drink == null
       && entry.drink == null
-      && entry.purchase.createdAt.minus(previous.purchase.createdAt) < 5.minutes
+      && entry.transaction.timestamp.minus(previous.transaction.timestamp) < 5.minutes
     ) {
       result.removeLast()
       result.add(
         entry.copy(
-          purchase = entry.purchase.copy(
-            difference = entry.purchase.difference + previous.purchase.difference
+          transaction = entry.transaction.copy(
+            difference = entry.transaction.difference + previous.transaction.difference
           )
         )
       )
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/purchases/PurchaseInfo.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/transactions/TransactionInfo.kt
similarity index 88%
rename from app/src/main/kotlin/de/chaosdorf/meteroid/ui/purchases/PurchaseInfo.kt
rename to app/src/main/kotlin/de/chaosdorf/meteroid/ui/transactions/TransactionInfo.kt
index b389efe..c4654ce 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/purchases/PurchaseInfo.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/transactions/TransactionInfo.kt
@@ -22,12 +22,12 @@
  * THE SOFTWARE.
  */
 
-package de.chaosdorf.meteroid.ui.purchases
+package de.chaosdorf.meteroid.ui.transactions
 
 import de.chaosdorf.meteroid.model.Drink
-import de.chaosdorf.meteroid.model.Purchase
+import de.chaosdorf.meteroid.model.Transaction
 
-data class PurchaseInfo(
-    val purchase: Purchase,
+data class TransactionInfo(
+    val transaction: Transaction,
     val drink: Drink?
 )
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/users/UserListScreen.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/users/UserListScreen.kt
index 02b1229..a6890ee 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/users/UserListScreen.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/users/UserListScreen.kt
@@ -27,13 +27,11 @@ package de.chaosdorf.meteroid.ui.users
 import androidx.compose.foundation.background
 import androidx.compose.foundation.border
 import androidx.compose.foundation.clickable
-import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.size
 import androidx.compose.foundation.lazy.LazyColumn
 import androidx.compose.foundation.lazy.items
 import androidx.compose.foundation.shape.CircleShape
-import androidx.compose.material3.LinearProgressIndicator
 import androidx.compose.material3.ListItem
 import androidx.compose.material3.MaterialTheme
 import androidx.compose.material3.Scaffold
@@ -49,8 +47,7 @@ import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.layout.ContentScale
 import androidx.compose.ui.unit.dp
 import coil.compose.AsyncImage
-import de.chaosdorf.mete.UserId
-import de.chaosdorf.meteroid.sync.SyncHandler
+import de.chaosdorf.mete.model.UserId
 
 @Composable
 fun UserListScreen(
@@ -61,7 +58,6 @@ fun UserListScreen(
 ) {
   val server by viewModel.account.collectAsState()
   val users by viewModel.users.collectAsState()
-  val syncState by viewModel.syncState.collectAsState()
 
   Scaffold(
     topBar = {
@@ -90,24 +86,19 @@ fun UserListScreen(
       )
     }
   ) { paddingValues ->
-    Column {
-      if (syncState == SyncHandler.State.Loading) {
-        LinearProgressIndicator()
+    LazyColumn(modifier = Modifier.padding(paddingValues)) {
+      items(users) { user ->
+        ListItem(
+          headlineContent = { Text(user.name) },
+          supportingContent = { Text(user.email) },
+          modifier = Modifier.clickable { onSelect(user.userId) }
+        )
       }
-      LazyColumn(modifier = Modifier.padding(paddingValues)) {
-        items(users) { user ->
-          ListItem(
-            headlineContent = { Text(user.name) },
-            supportingContent = { Text(user.email) },
-            modifier = Modifier.clickable { onSelect(user.userId) }
-          )
-        }
-        item {
-          ListItem(
-            headlineContent = { Text("Add User") },
-            modifier = Modifier.clickable { onAdd() }
-          )
-        }
+      item {
+        ListItem(
+          headlineContent = { Text("Add User") },
+          modifier = Modifier.clickable { onAdd() }
+        )
       }
     }
   }
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/users/UserListViewModel.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/users/UserListViewModel.kt
index bc36b14..74029fd 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/users/UserListViewModel.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/users/UserListViewModel.kt
@@ -28,11 +28,9 @@ import androidx.lifecycle.ViewModel
 import androidx.lifecycle.viewModelScope
 import dagger.hilt.android.lifecycle.HiltViewModel
 import de.chaosdorf.meteroid.model.AccountInfo
-import de.chaosdorf.meteroid.model.Server
 import de.chaosdorf.meteroid.model.User
 import de.chaosdorf.meteroid.model.UserRepository
 import de.chaosdorf.meteroid.sync.AccountProvider
-import de.chaosdorf.meteroid.sync.SyncHandler
 import de.chaosdorf.meteroid.sync.UserSyncHandler
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
@@ -44,8 +42,7 @@ import javax.inject.Inject
 @HiltViewModel
 class UserListViewModel @Inject constructor(
   accountProvider: AccountProvider,
-  repository: UserRepository,
-  syncHandler: UserSyncHandler
+  repository: UserRepository
 ) : ViewModel() {
   val account: StateFlow<AccountInfo?> = accountProvider.account
     .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
@@ -56,6 +53,4 @@ class UserListViewModel @Inject constructor(
         repository.getAllFlow(server.serverId)
       } ?: flowOf(emptyList())
     }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList())
-
-  val syncState: StateFlow<SyncHandler.State> = syncHandler.state
 }
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/util/PwaManifestExtension.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/util/PwaManifestExtension.kt
index cd7ecd4..65416db 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/util/PwaManifestExtension.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/util/PwaManifestExtension.kt
@@ -24,8 +24,8 @@
 
 package de.chaosdorf.meteroid.util
 
-import de.chaosdorf.mete.PwaIcon
-import de.chaosdorf.mete.PwaManifest
+import de.chaosdorf.mete.model.PwaIcon
+import de.chaosdorf.mete.model.PwaManifest
 import okhttp3.HttpUrl.Companion.toHttpUrl
 
 fun PwaManifest.findBestIcon(): PwaIcon? = icons.maxByOrNull {
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/util/Update.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/util/Update.kt
new file mode 100644
index 0000000..f357815
--- /dev/null
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/util/Update.kt
@@ -0,0 +1,35 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2013-2023 Chaosdorf e.V.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package de.chaosdorf.meteroid.util
+
+import androidx.lifecycle.SavedStateHandle
+
+fun <T> SavedStateHandle.update(key: String, block: (T?) -> T) {
+  this[key] = block(this[key])
+}
+
+fun <T> SavedStateHandle.update(key: String, default: T, block: (T) -> T) {
+  this[key] = block(this[key] ?: default)
+}
diff --git a/persistence/room/schemas/de.chaosdorf.meteroid.MeteroidDatabase/1.json b/persistence/room/schemas/de.chaosdorf.meteroid.MeteroidDatabase/1.json
index b8a0cbb..9efb6d6 100644
--- a/persistence/room/schemas/de.chaosdorf.meteroid.MeteroidDatabase/1.json
+++ b/persistence/room/schemas/de.chaosdorf.meteroid.MeteroidDatabase/1.json
@@ -2,11 +2,11 @@
   "formatVersion": 1,
   "database": {
     "version": 1,
-    "identityHash": "957bb839c1c422a256c594595437ee45",
+    "identityHash": "7aca7cdc33cbadb81643b32c5838f2a9",
     "entities": [
       {
         "tableName": "Drink",
-        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` INTEGER NOT NULL, `drinkId` INTEGER NOT NULL, `active` INTEGER NOT NULL, `name` TEXT NOT NULL, `volume` REAL NOT NULL, `caffeine` INTEGER, `price` REAL NOT NULL, `createdAt` INTEGER NOT NULL, `updatedAt` INTEGER NOT NULL, `logoUrl` TEXT NOT NULL, `logoFileName` TEXT NOT NULL, `logoContentType` TEXT NOT NULL, `logoFileSize` INTEGER NOT NULL, `logoUpdatedAt` INTEGER NOT NULL, PRIMARY KEY(`serverId`, `drinkId`), FOREIGN KEY(`serverId`) REFERENCES `Server`(`serverId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` INTEGER NOT NULL, `drinkId` INTEGER NOT NULL, `active` INTEGER NOT NULL, `name` TEXT NOT NULL, `volume` TEXT NOT NULL, `caffeine` INTEGER, `price` TEXT NOT NULL, `logoUrl` TEXT NOT NULL, PRIMARY KEY(`serverId`, `drinkId`), FOREIGN KEY(`serverId`) REFERENCES `Server`(`serverId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
         "fields": [
           {
             "fieldPath": "serverId",
@@ -35,7 +35,7 @@
           {
             "fieldPath": "volume",
             "columnName": "volume",
-            "affinity": "REAL",
+            "affinity": "TEXT",
             "notNull": true
           },
           {
@@ -47,19 +47,7 @@
           {
             "fieldPath": "price",
             "columnName": "price",
-            "affinity": "REAL",
-            "notNull": true
-          },
-          {
-            "fieldPath": "createdAt",
-            "columnName": "createdAt",
-            "affinity": "INTEGER",
-            "notNull": true
-          },
-          {
-            "fieldPath": "updatedAt",
-            "columnName": "updatedAt",
-            "affinity": "INTEGER",
+            "affinity": "TEXT",
             "notNull": true
           },
           {
@@ -67,30 +55,6 @@
             "columnName": "logoUrl",
             "affinity": "TEXT",
             "notNull": true
-          },
-          {
-            "fieldPath": "logoFileName",
-            "columnName": "logoFileName",
-            "affinity": "TEXT",
-            "notNull": true
-          },
-          {
-            "fieldPath": "logoContentType",
-            "columnName": "logoContentType",
-            "affinity": "TEXT",
-            "notNull": true
-          },
-          {
-            "fieldPath": "logoFileSize",
-            "columnName": "logoFileSize",
-            "affinity": "INTEGER",
-            "notNull": true
-          },
-          {
-            "fieldPath": "logoUpdatedAt",
-            "columnName": "logoUpdatedAt",
-            "affinity": "INTEGER",
-            "notNull": true
           }
         ],
         "primaryKey": {
@@ -155,7 +119,7 @@
       },
       {
         "tableName": "User",
-        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` INTEGER NOT NULL, `userId` INTEGER NOT NULL, `active` INTEGER NOT NULL, `name` TEXT NOT NULL, `email` TEXT NOT NULL, `balance` REAL NOT NULL, `audit` INTEGER NOT NULL, `redirect` INTEGER NOT NULL, `createdAt` INTEGER NOT NULL, `updatedAt` INTEGER NOT NULL, PRIMARY KEY(`serverId`, `userId`), FOREIGN KEY(`serverId`) REFERENCES `Server`(`serverId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` INTEGER NOT NULL, `userId` INTEGER NOT NULL, `active` INTEGER NOT NULL, `name` TEXT NOT NULL, `email` TEXT NOT NULL, `balance` TEXT NOT NULL, `audit` INTEGER NOT NULL, `redirect` INTEGER NOT NULL, PRIMARY KEY(`serverId`, `userId`), FOREIGN KEY(`serverId`) REFERENCES `Server`(`serverId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
         "fields": [
           {
             "fieldPath": "serverId",
@@ -190,7 +154,7 @@
           {
             "fieldPath": "balance",
             "columnName": "balance",
-            "affinity": "REAL",
+            "affinity": "TEXT",
             "notNull": true
           },
           {
@@ -204,18 +168,6 @@
             "columnName": "redirect",
             "affinity": "INTEGER",
             "notNull": true
-          },
-          {
-            "fieldPath": "createdAt",
-            "columnName": "createdAt",
-            "affinity": "INTEGER",
-            "notNull": true
-          },
-          {
-            "fieldPath": "updatedAt",
-            "columnName": "updatedAt",
-            "affinity": "INTEGER",
-            "notNull": true
           }
         ],
         "primaryKey": {
@@ -241,8 +193,8 @@
         ]
       },
       {
-        "tableName": "Purchase",
-        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` INTEGER NOT NULL, `purchaseId` INTEGER NOT NULL, `userId` INTEGER NOT NULL, `drinkId` INTEGER, `difference` REAL NOT NULL, `createdAt` INTEGER NOT NULL, PRIMARY KEY(`serverId`, `purchaseId`), FOREIGN KEY(`serverId`) REFERENCES `Server`(`serverId`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`serverId`, `userId`) REFERENCES `User`(`serverId`, `userId`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`serverId`, `drinkId`) REFERENCES `Drink`(`serverId`, `drinkId`) ON UPDATE NO ACTION ON DELETE NO ACTION )",
+        "tableName": "Transaction",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` INTEGER NOT NULL, `transactionId` INTEGER NOT NULL, `userId` INTEGER NOT NULL, `drinkId` INTEGER, `difference` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, PRIMARY KEY(`serverId`, `transactionId`), FOREIGN KEY(`serverId`) REFERENCES `Server`(`serverId`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`serverId`, `userId`) REFERENCES `User`(`serverId`, `userId`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`serverId`, `drinkId`) REFERENCES `Drink`(`serverId`, `drinkId`) ON UPDATE NO ACTION ON DELETE NO ACTION )",
         "fields": [
           {
             "fieldPath": "serverId",
@@ -251,8 +203,8 @@
             "notNull": true
           },
           {
-            "fieldPath": "purchaseId",
-            "columnName": "purchaseId",
+            "fieldPath": "transactionId",
+            "columnName": "transactionId",
             "affinity": "INTEGER",
             "notNull": true
           },
@@ -271,12 +223,12 @@
           {
             "fieldPath": "difference",
             "columnName": "difference",
-            "affinity": "REAL",
+            "affinity": "TEXT",
             "notNull": true
           },
           {
-            "fieldPath": "createdAt",
-            "columnName": "createdAt",
+            "fieldPath": "timestamp",
+            "columnName": "timestamp",
             "affinity": "INTEGER",
             "notNull": true
           }
@@ -285,29 +237,29 @@
           "autoGenerate": false,
           "columnNames": [
             "serverId",
-            "purchaseId"
+            "transactionId"
           ]
         },
         "indices": [
           {
-            "name": "index_Purchase_serverId_userId",
+            "name": "index_Transaction_serverId_userId",
             "unique": false,
             "columnNames": [
               "serverId",
               "userId"
             ],
             "orders": [],
-            "createSql": "CREATE INDEX IF NOT EXISTS `index_Purchase_serverId_userId` ON `${TABLE_NAME}` (`serverId`, `userId`)"
+            "createSql": "CREATE INDEX IF NOT EXISTS `index_Transaction_serverId_userId` ON `${TABLE_NAME}` (`serverId`, `userId`)"
           },
           {
-            "name": "index_Purchase_serverId_drinkId",
+            "name": "index_Transaction_serverId_drinkId",
             "unique": false,
             "columnNames": [
               "serverId",
               "drinkId"
             ],
             "orders": [],
-            "createSql": "CREATE INDEX IF NOT EXISTS `index_Purchase_serverId_drinkId` ON `${TABLE_NAME}` (`serverId`, `drinkId`)"
+            "createSql": "CREATE INDEX IF NOT EXISTS `index_Transaction_serverId_drinkId` ON `${TABLE_NAME}` (`serverId`, `drinkId`)"
           }
         ],
         "foreignKeys": [
@@ -354,7 +306,7 @@
     "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, '957bb839c1c422a256c594595437ee45')"
+      "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '7aca7cdc33cbadb81643b32c5838f2a9')"
     ]
   }
 }
\ No newline at end of file
diff --git a/persistence/src/main/kotlin/de/chaosdorf/meteroid/MeteroidDatabase.kt b/persistence/src/main/kotlin/de/chaosdorf/meteroid/MeteroidDatabase.kt
index a8b3286..8cf5fac 100644
--- a/persistence/src/main/kotlin/de/chaosdorf/meteroid/MeteroidDatabase.kt
+++ b/persistence/src/main/kotlin/de/chaosdorf/meteroid/MeteroidDatabase.kt
@@ -29,12 +29,13 @@ import androidx.room.RoomDatabase
 import androidx.room.TypeConverters
 import de.chaosdorf.meteroid.model.Drink
 import de.chaosdorf.meteroid.model.DrinkRepository
-import de.chaosdorf.meteroid.model.Purchase
-import de.chaosdorf.meteroid.model.PurchaseRepository
 import de.chaosdorf.meteroid.model.Server
 import de.chaosdorf.meteroid.model.ServerRepository
+import de.chaosdorf.meteroid.model.Transaction
+import de.chaosdorf.meteroid.model.TransactionRepository
 import de.chaosdorf.meteroid.model.User
 import de.chaosdorf.meteroid.model.UserRepository
+import de.chaosdorf.meteroid.util.BigDecimalTypeConverter
 import de.chaosdorf.meteroid.util.KotlinDatetimeTypeConverter
 
 @Database(
@@ -43,14 +44,19 @@ import de.chaosdorf.meteroid.util.KotlinDatetimeTypeConverter
     Drink::class,
     Server::class,
     User::class,
-    Purchase::class
+    Transaction::class
   ],
   autoMigrations = [],
 )
-@TypeConverters(value = [KotlinDatetimeTypeConverter::class])
+@TypeConverters(
+  value = [
+    KotlinDatetimeTypeConverter::class,
+    BigDecimalTypeConverter::class
+  ]
+)
 abstract class MeteroidDatabase : RoomDatabase() {
   abstract fun drinks(): DrinkRepository
   abstract fun server(): ServerRepository
   abstract fun users(): UserRepository
-  abstract fun purchases(): PurchaseRepository
+  abstract fun transactions(): TransactionRepository
 }
diff --git a/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/Drink.kt b/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/Drink.kt
index ade8587..6ea4886 100644
--- a/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/Drink.kt
+++ b/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/Drink.kt
@@ -30,10 +30,10 @@ import androidx.room.ForeignKey
 import androidx.room.Insert
 import androidx.room.OnConflictStrategy
 import androidx.room.Query
-import de.chaosdorf.mete.DrinkId
-import de.chaosdorf.mete.v1.DrinkModelV1
+import de.chaosdorf.mete.model.DrinkId
+import de.chaosdorf.mete.model.DrinkModel
 import kotlinx.coroutines.flow.Flow
-import kotlinx.datetime.Instant
+import java.math.BigDecimal
 import java.net.URI
 
 @Entity(
@@ -47,33 +47,21 @@ data class Drink(
   val drinkId: DrinkId,
   val active: Boolean,
   val name: String,
-  val volume: Double,
+  val volume: BigDecimal,
   val caffeine: Int?,
-  val price: Double,
-  val createdAt: Instant,
-  val updatedAt: Instant,
+  val price: BigDecimal,
   val logoUrl: String,
-  val logoFileName: String,
-  val logoContentType: String,
-  val logoFileSize: Long,
-  val logoUpdatedAt: Instant
 ) {
   companion object {
-    fun fromModelV1(server: Server, value: DrinkModelV1) = Drink(
+    fun fromModel(server: Server, value: DrinkModel) = Drink(
       server.serverId,
-      value.id,
+      value.drinkId,
       value.active,
       value.name,
-      value.bottleSize,
+      value.volume,
       value.caffeine,
       value.price,
-      value.createdAt,
-      value.updatedAt,
-      URI.create(server.url).resolve(value.logoUrl).toString(),
-      value.logoFileName,
-      value.logoContentType,
-      value.logoFileSize,
-      value.logoUpdatedAt
+      URI.create(server.url).resolve(value.logoUrl).toString()
     )
   }
 }
@@ -89,7 +77,7 @@ interface DrinkRepository {
   @Query("SELECT * FROM Drink WHERE serverId = :serverId ORDER BY NAME ASC")
   suspend fun getAll(serverId: ServerId): List<Drink>
 
-  @Query("SELECT * FROM Drink WHERE serverId = :serverId ORDER BY NAME ASC")
+  @Query("SELECT * FROM Drink WHERE serverId = :serverId ORDER BY ACTIVE DESC, NAME ASC")
   fun getAllFlow(serverId: ServerId): Flow<List<Drink>>
 
   @Insert(onConflict = OnConflictStrategy.REPLACE)
diff --git a/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/Purchase.kt b/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/Transaction.kt
similarity index 57%
rename from persistence/src/main/kotlin/de/chaosdorf/meteroid/model/Purchase.kt
rename to persistence/src/main/kotlin/de/chaosdorf/meteroid/model/Transaction.kt
index fad3d86..c564bbd 100644
--- a/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/Purchase.kt
+++ b/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/Transaction.kt
@@ -31,62 +31,73 @@ import androidx.room.Index
 import androidx.room.Insert
 import androidx.room.OnConflictStrategy
 import androidx.room.Query
-import de.chaosdorf.mete.AuditEntryId
-import de.chaosdorf.mete.DrinkId
-import de.chaosdorf.mete.UserId
-import de.chaosdorf.mete.v1.AuditEntryModelV1
+import de.chaosdorf.mete.model.DrinkId
+import de.chaosdorf.mete.model.TransactionId
+import de.chaosdorf.mete.model.TransactionModel
+import de.chaosdorf.mete.model.UserId
 import kotlinx.coroutines.flow.Flow
 import kotlinx.datetime.Instant
+import java.math.BigDecimal
 
 @Entity(
-  primaryKeys = ["serverId", "purchaseId"],
+  primaryKeys = ["serverId", "transactionId"],
   foreignKeys = [
     ForeignKey(Server::class, ["serverId"], ["serverId"], onDelete = ForeignKey.CASCADE),
-    ForeignKey(User::class, ["serverId", "userId"], ["serverId", "userId"], onDelete = ForeignKey.CASCADE),
-    ForeignKey(Drink::class, ["serverId", "drinkId"], ["serverId", "drinkId"], onDelete = ForeignKey.NO_ACTION)
+    ForeignKey(
+      User::class,
+      ["serverId", "userId"],
+      ["serverId", "userId"],
+      onDelete = ForeignKey.CASCADE
+    ),
+    ForeignKey(
+      Drink::class,
+      ["serverId", "drinkId"],
+      ["serverId", "drinkId"],
+      onDelete = ForeignKey.NO_ACTION
+    )
   ],
   indices = [
     Index("serverId", "userId"),
     Index("serverId", "drinkId")
   ]
 )
-data class Purchase(
+data class Transaction(
   val serverId: ServerId,
-  val purchaseId: AuditEntryId,
+  val transactionId: TransactionId,
   val userId: UserId,
   val drinkId: DrinkId?,
-  val difference: Double,
-  val createdAt: Instant
+  val difference: BigDecimal,
+  val timestamp: Instant
 ) {
   companion object {
-    fun fromModelV1(server: Server, userId: UserId, value: AuditEntryModelV1) = Purchase(
+    fun fromModel(server: Server, userId: UserId, value: TransactionModel) = Transaction(
       server.serverId,
-      value.id,
+      value.transactionId,
       userId,
-      value.drink,
+      value.drinkId,
       value.difference,
-      value.createdAt
+      value.timestamp
     )
   }
 }
 
 @Dao
-interface PurchaseRepository {
-  @Query("SELECT * FROM Purchase WHERE serverId = :serverId AND userId = :userId ORDER BY createdAt DESC")
-  suspend fun getAll(serverId: ServerId, userId: UserId): List<Purchase>
+interface TransactionRepository {
+  @Query("SELECT * FROM `Transaction` WHERE serverId = :serverId AND userId = :userId ORDER BY timestamp DESC")
+  suspend fun getAll(serverId: ServerId, userId: UserId): List<Transaction>
 
-  @Query("SELECT * FROM Purchase WHERE serverId = :serverId AND userId = :userId ORDER BY createdAt DESC")
-  fun getAllFlow(serverId: ServerId, userId: UserId): Flow<List<Purchase>>
+  @Query("SELECT * FROM `Transaction` WHERE serverId = :serverId AND userId = :userId ORDER BY timestamp DESC")
+  fun getAllFlow(serverId: ServerId, userId: UserId): Flow<List<Transaction>>
 
   @Insert(onConflict = OnConflictStrategy.REPLACE)
-  suspend fun save(purchase: Purchase)
+  suspend fun save(transaction: Transaction)
 
-  @Query("DELETE FROM Purchase WHERE serverId = :serverId AND purchaseId = :purchaseId")
-  suspend fun delete(serverId: ServerId, purchaseId: AuditEntryId)
+  @Query("DELETE FROM `Transaction` WHERE serverId = :serverId AND transactionId = :transactionId")
+  suspend fun delete(serverId: ServerId, transactionId: TransactionId)
 
-  @Query("DELETE FROM Purchase WHERE serverId = :serverId AND userId = :userId")
+  @Query("DELETE FROM `Transaction` WHERE serverId = :serverId AND userId = :userId")
   suspend fun deleteAll(serverId: ServerId, userId: UserId)
 
-  @Query("DELETE FROM Purchase WHERE serverId = :serverId")
+  @Query("DELETE FROM `Transaction` WHERE serverId = :serverId")
   suspend fun deleteAll(serverId: ServerId)
 }
diff --git a/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/User.kt b/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/User.kt
index 40d52ae..6056168 100644
--- a/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/User.kt
+++ b/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/User.kt
@@ -30,10 +30,10 @@ import androidx.room.ForeignKey
 import androidx.room.Insert
 import androidx.room.OnConflictStrategy
 import androidx.room.Query
-import de.chaosdorf.mete.UserId
-import de.chaosdorf.mete.v1.UserModelV1
+import de.chaosdorf.mete.model.UserId
+import de.chaosdorf.mete.model.UserModel
 import kotlinx.coroutines.flow.Flow
-import kotlinx.datetime.Instant
+import java.math.BigDecimal
 import java.security.MessageDigest
 import java.util.Locale
 
@@ -49,24 +49,20 @@ data class User(
   val active: Boolean,
   val name: String,
   val email: String,
-  val balance: Double,
+  val balance: BigDecimal,
   val audit: Boolean,
   val redirect: Boolean,
-  val createdAt: Instant,
-  val updatedAt: Instant,
 ) {
   companion object {
-    fun fromModelV1(server: Server, value: UserModelV1) = User(
+    fun fromModel(server: Server, value: UserModel) = User(
       server.serverId,
-      value.id,
+      value.userId,
       value.active,
       value.name,
       value.email,
       value.balance,
       value.audit,
       value.redirect,
-      value.createdAt,
-      value.updatedAt
     )
   }
 
diff --git a/persistence/src/main/kotlin/de/chaosdorf/meteroid/util/BigDecimalTypeConverter.kt b/persistence/src/main/kotlin/de/chaosdorf/meteroid/util/BigDecimalTypeConverter.kt
new file mode 100644
index 0000000..4e46f64
--- /dev/null
+++ b/persistence/src/main/kotlin/de/chaosdorf/meteroid/util/BigDecimalTypeConverter.kt
@@ -0,0 +1,36 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2013-2023 Chaosdorf e.V.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package de.chaosdorf.meteroid.util
+
+import androidx.room.TypeConverter
+import java.math.BigDecimal
+
+class BigDecimalTypeConverter {
+  @TypeConverter
+  fun load(value: String): BigDecimal = BigDecimal(value)
+
+  @TypeConverter
+  fun store(value: BigDecimal): String = value.toString()
+}
-- 
GitLab