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..02f3f78 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,12 @@ 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..eb32bc7 --- /dev/null +++ b/core/database/src/test/kotlin/com/thesetox/database/BalanceDaoTest.kt @@ -0,0 +1,87 @@ +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) + } +}