Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions core/database/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

21 changes: 21 additions & 0 deletions core/database/src/main/kotlin/com/thesetox/database/BalanceDao.kt
Original file line number Diff line number Diff line change
@@ -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<List<BalanceEntity>>

@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()
}
Original file line number Diff line number Diff line change
@@ -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,
)
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -26,4 +27,5 @@ val databaseModule =
}

single { get<ConversionAppDatabase>().currencyRateDao() }
single { get<ConversionAppDatabase>().balanceDao() }
}
Original file line number Diff line number Diff line change
@@ -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<android.content.Context>()
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)
}
}