From 5b7931b2bc452f631eb6098e19cf09278c66d503 Mon Sep 17 00:00:00 2001 From: Stephen Siapno Date: Thu, 12 Jun 2025 22:27:47 +0800 Subject: [PATCH 1/2] Add Balance table and DAO --- core/database/README.md | 9 +- .../com/thesetox/database/BalanceDao.kt | 21 +++++ .../com/thesetox/database/BalanceEntity.kt | 10 +++ .../database/ConversionAppDatabase.kt | 6 +- .../com/thesetox/database/DatabaseModule.kt | 6 +- .../com/thesetox/database/BalanceDaoTest.kt | 83 +++++++++++++++++++ 6 files changed, 128 insertions(+), 7 deletions(-) create mode 100644 core/database/src/main/kotlin/com/thesetox/database/BalanceDao.kt create mode 100644 core/database/src/main/kotlin/com/thesetox/database/BalanceEntity.kt create mode 100644 core/database/src/test/kotlin/com/thesetox/database/BalanceDaoTest.kt diff --git a/core/database/README.md b/core/database/README.md index d1d8072..a6fff24 100644 --- a/core/database/README.md +++ b/core/database/README.md @@ -5,16 +5,17 @@ It stores currency rates that are synchronized from the network and exposes them ## Features -- **Room database** named `conversion.db` with a single table `currency_rate`. -- **Data access** via `CurrencyRateDao` which allows observing rates, fetching a single rate, inserting new rates and clearing the table. +- **Room database** named `conversion.db` with tables `currency_rate` and `balance`. +- **Data access** via `CurrencyRateDao` and `BalanceDao` for observing, updating and clearing data. - **Dependency injection** with Koin via `DatabaseModule` for easy database and DAO provisioning. ## Main Classes - `CurrencyRateEntity` – Room entity representing a currency rate entry. - `CurrencyRateDao` – DAO with queries for retrieving and updating rates. -- `ConversionAppDatabase` – `RoomDatabase` implementation registering the DAO. -- `DatabaseModule` – Koin module that builds the database and exposes the DAO. +- `BalanceDao` – DAO for reading and updating wallet balances. +- `ConversionAppDatabase` – `RoomDatabase` implementation registering the DAOs. +- `DatabaseModule` – Koin module that builds the database and exposes the DAOs. This module is used by the feature modules to cache exchange rates locally so they can be displayed even when offline. diff --git a/core/database/src/main/kotlin/com/thesetox/database/BalanceDao.kt b/core/database/src/main/kotlin/com/thesetox/database/BalanceDao.kt new file mode 100644 index 0000000..9ab8142 --- /dev/null +++ b/core/database/src/main/kotlin/com/thesetox/database/BalanceDao.kt @@ -0,0 +1,21 @@ +package com.thesetox.database + +import androidx.room.Dao +import androidx.room.Query +import androidx.room.Upsert +import kotlinx.coroutines.flow.Flow + +@Dao +interface BalanceDao { + @Query("SELECT * FROM balance") + fun getBalanceList(): Flow> + + @Query("SELECT * FROM balance WHERE code = :code") + suspend fun getBalance(code: String): BalanceEntity? + + @Upsert + suspend fun updateBalance(balance: BalanceEntity) + + @Query("DELETE FROM balance") + suspend fun clearBalances() +} diff --git a/core/database/src/main/kotlin/com/thesetox/database/BalanceEntity.kt b/core/database/src/main/kotlin/com/thesetox/database/BalanceEntity.kt new file mode 100644 index 0000000..f79d822 --- /dev/null +++ b/core/database/src/main/kotlin/com/thesetox/database/BalanceEntity.kt @@ -0,0 +1,10 @@ +package com.thesetox.database + +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity(tableName = "balance") +data class BalanceEntity( + @PrimaryKey val code: String = "", + val value: Double = 0.0, +) diff --git a/core/database/src/main/kotlin/com/thesetox/database/ConversionAppDatabase.kt b/core/database/src/main/kotlin/com/thesetox/database/ConversionAppDatabase.kt index 1fc0726..e3c085b 100644 --- a/core/database/src/main/kotlin/com/thesetox/database/ConversionAppDatabase.kt +++ b/core/database/src/main/kotlin/com/thesetox/database/ConversionAppDatabase.kt @@ -3,7 +3,11 @@ package com.thesetox.database import androidx.room.Database import androidx.room.RoomDatabase -@Database(entities = [CurrencyRateEntity::class], version = 1) +@Database( + entities = [CurrencyRateEntity::class, BalanceEntity::class], + version = 2, +) abstract class ConversionAppDatabase : RoomDatabase() { abstract fun currencyRateDao(): CurrencyRateDao + abstract fun balanceDao(): BalanceDao } diff --git a/core/database/src/main/kotlin/com/thesetox/database/DatabaseModule.kt b/core/database/src/main/kotlin/com/thesetox/database/DatabaseModule.kt index addcdfd..bbaeff5 100644 --- a/core/database/src/main/kotlin/com/thesetox/database/DatabaseModule.kt +++ b/core/database/src/main/kotlin/com/thesetox/database/DatabaseModule.kt @@ -11,8 +11,9 @@ import org.koin.dsl.module * Koin module that provides the Room database and its DAO dependencies. * * The module exposes a singleton instance of [ConversionAppDatabase] built with - * `Room.databaseBuilder` and a singleton [CurrencyRateDao] retrieved from the - * database. These can then be injected throughout the app. + * `Room.databaseBuilder` and singleton DAOs ([CurrencyRateDao] and + * [BalanceDao]) retrieved from the database. These can then be injected + * throughout the app. */ val databaseModule = module { @@ -26,4 +27,5 @@ val databaseModule = } single { get().currencyRateDao() } + single { get().balanceDao() } } diff --git a/core/database/src/test/kotlin/com/thesetox/database/BalanceDaoTest.kt b/core/database/src/test/kotlin/com/thesetox/database/BalanceDaoTest.kt new file mode 100644 index 0000000..3390b81 --- /dev/null +++ b/core/database/src/test/kotlin/com/thesetox/database/BalanceDaoTest.kt @@ -0,0 +1,83 @@ +package com.thesetox.database + +import androidx.room.Room +import androidx.test.core.app.ApplicationProvider +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config + +@RunWith(RobolectricTestRunner::class) +@Config(manifest = Config.NONE) +class BalanceDaoTest { + private lateinit var database: ConversionAppDatabase + private lateinit var dao: BalanceDao + + @Before + fun setUp() { + val context = ApplicationProvider.getApplicationContext() + database = + Room.inMemoryDatabaseBuilder( + context, + ConversionAppDatabase::class.java, + ).allowMainThreadQueries().build() + dao = database.balanceDao() + } + + @After + fun tearDown() { + database.close() + } + + @Test + fun `updateBalance inserts when new`() = runTest { + // Act + dao.updateBalance(BalanceEntity("EUR", 1000.0)) + + // Assert + val list = dao.getBalanceList().first() + assertEquals(1, list.size) + assertEquals("EUR", list.first().code) + } + + @Test + fun `updateBalance updates existing record`() = runTest { + // Arrange + dao.updateBalance(BalanceEntity("EUR", 1000.0)) + + // Act + dao.updateBalance(BalanceEntity("EUR", 2000.0)) + + // Assert + val balance = dao.getBalance("EUR") + assertEquals(2000.0, balance?.value) + } + + @Test + fun `clearBalances deletes all entries`() = runTest { + // Arrange + dao.updateBalance(BalanceEntity("EUR", 1000.0)) + + // Act + dao.clearBalances() + + // Assert + val list = dao.getBalanceList().first() + assertEquals(0, list.size) + } + + @Test + fun `getBalance returns null when balance is absent`() = runTest { + // Act + val result = dao.getBalance("EUR") + + // Assert + assertNull(result) + } +} From f3983b09b7e5647468c3897510aacc0803a16742 Mon Sep 17 00:00:00 2001 From: Stephen Siapno Date: Thu, 12 Jun 2025 23:01:06 +0800 Subject: [PATCH 2/2] Run spotlessApply --- .../database/ConversionAppDatabase.kt | 1 + .../com/thesetox/database/BalanceDaoTest.kt | 68 ++++++++++--------- 2 files changed, 37 insertions(+), 32 deletions(-) diff --git a/core/database/src/main/kotlin/com/thesetox/database/ConversionAppDatabase.kt b/core/database/src/main/kotlin/com/thesetox/database/ConversionAppDatabase.kt index e3c085b..02f3f78 100644 --- a/core/database/src/main/kotlin/com/thesetox/database/ConversionAppDatabase.kt +++ b/core/database/src/main/kotlin/com/thesetox/database/ConversionAppDatabase.kt @@ -9,5 +9,6 @@ import androidx.room.RoomDatabase ) abstract class ConversionAppDatabase : RoomDatabase() { abstract fun currencyRateDao(): CurrencyRateDao + abstract fun balanceDao(): BalanceDao } diff --git a/core/database/src/test/kotlin/com/thesetox/database/BalanceDaoTest.kt b/core/database/src/test/kotlin/com/thesetox/database/BalanceDaoTest.kt index 3390b81..eb32bc7 100644 --- a/core/database/src/test/kotlin/com/thesetox/database/BalanceDaoTest.kt +++ b/core/database/src/test/kotlin/com/thesetox/database/BalanceDaoTest.kt @@ -36,48 +36,52 @@ class BalanceDaoTest { } @Test - fun `updateBalance inserts when new`() = runTest { - // Act - dao.updateBalance(BalanceEntity("EUR", 1000.0)) + fun `updateBalance inserts when new`() = + runTest { + // Act + dao.updateBalance(BalanceEntity("EUR", 1000.0)) - // Assert - val list = dao.getBalanceList().first() - assertEquals(1, list.size) - assertEquals("EUR", list.first().code) - } + // Assert + val list = dao.getBalanceList().first() + assertEquals(1, list.size) + assertEquals("EUR", list.first().code) + } @Test - fun `updateBalance updates existing record`() = runTest { - // Arrange - dao.updateBalance(BalanceEntity("EUR", 1000.0)) + fun `updateBalance updates existing record`() = + runTest { + // Arrange + dao.updateBalance(BalanceEntity("EUR", 1000.0)) - // Act - dao.updateBalance(BalanceEntity("EUR", 2000.0)) + // Act + dao.updateBalance(BalanceEntity("EUR", 2000.0)) - // Assert - val balance = dao.getBalance("EUR") - assertEquals(2000.0, balance?.value) - } + // Assert + val balance = dao.getBalance("EUR") + assertEquals(2000.0, balance?.value) + } @Test - fun `clearBalances deletes all entries`() = runTest { - // Arrange - dao.updateBalance(BalanceEntity("EUR", 1000.0)) + fun `clearBalances deletes all entries`() = + runTest { + // Arrange + dao.updateBalance(BalanceEntity("EUR", 1000.0)) - // Act - dao.clearBalances() + // Act + dao.clearBalances() - // Assert - val list = dao.getBalanceList().first() - assertEquals(0, list.size) - } + // Assert + val list = dao.getBalanceList().first() + assertEquals(0, list.size) + } @Test - fun `getBalance returns null when balance is absent`() = runTest { - // Act - val result = dao.getBalance("EUR") + fun `getBalance returns null when balance is absent`() = + runTest { + // Act + val result = dao.getBalance("EUR") - // Assert - assertNull(result) - } + // Assert + assertNull(result) + } }