Persisting AccountTransactions in db

This commit is contained in:
dankito 2024-08-27 11:48:46 +02:00
parent 7495cb89c2
commit 6d35b9c64f
15 changed files with 478 additions and 14 deletions

2
.gitignore vendored
View File

@ -22,4 +22,4 @@ xcuserdata
!*.xcworkspace/contents.xcworkspacedata !*.xcworkspace/contents.xcworkspacedata
**/xcshareddata/WorkspaceSettings.xcsettings **/xcshareddata/WorkspaceSettings.xcsettings
composeApp/data/logs composeApp/data/

View File

@ -11,6 +11,8 @@ plugins {
alias(libs.plugins.compose.compiler) alias(libs.plugins.compose.compiler)
alias(libs.plugins.kotlinxSerialization) alias(libs.plugins.kotlinxSerialization)
alias(libs.plugins.sqldelight)
} }
@ -63,6 +65,10 @@ kotlin {
implementation(libs.klf) implementation(libs.klf)
implementation(libs.kotlinx.serializable) implementation(libs.kotlinx.serializable)
implementation(libs.sqldelight.runtime)
implementation(libs.sqldelight.coroutines.extensions)
implementation(libs.sqldelight.paging.extensions)
// UI // UI
implementation(compose.runtime) implementation(compose.runtime)
implementation(compose.foundation) implementation(compose.foundation)
@ -75,20 +81,50 @@ kotlin {
implementation(libs.androidx.lifecycle.runtime.compose) implementation(libs.androidx.lifecycle.runtime.compose)
} }
commonTest.dependencies {
implementation(libs.kotlin.test)
}
androidMain.dependencies { androidMain.dependencies {
implementation(compose.preview) implementation(compose.preview)
implementation(libs.androidx.activity.compose) implementation(libs.androidx.activity.compose)
implementation(libs.sqldelight.android.driver)
}
nativeMain.dependencies {
implementation(libs.sqldelight.native.driver)
}
jvmMain.dependencies {
implementation(libs.sqldelight.sqlite.driver)
}
jvmTest.dependencies {
implementation(libs.kotlin.test.junit)
} }
desktopMain.dependencies { desktopMain.dependencies {
implementation(compose.desktop.currentOs) implementation(compose.desktop.currentOs)
implementation(libs.kotlinx.coroutines.swing) implementation(libs.kotlinx.coroutines.swing)
implementation(libs.sqldelight.sqlite.driver)
implementation(libs.logback) implementation(libs.logback)
} }
} }
} }
sqldelight {
databases {
create("BankmeisterDb") {
packageName.set("net.codinux.banking.dataaccess")
generateAsync.set(true)
}
}
}
android { android {
namespace = "net.codinux.banking.ui" namespace = "net.codinux.banking.ui"
compileSdk = libs.versions.android.compileSdk.get().toInt() compileSdk = libs.versions.android.compileSdk.get().toInt()

View File

@ -5,11 +5,17 @@ import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import app.cash.sqldelight.async.coroutines.synchronous
import app.cash.sqldelight.driver.android.AndroidSqliteDriver
import net.codinux.banking.dataaccess.BankmeisterDb
import net.codinux.banking.ui.config.DI
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
DI.setRepository(AndroidSqliteDriver(BankmeisterDb.Schema.synchronous(), this, "Bankmeister.db"))
setContent { setContent {
App() App()
} }

View File

@ -0,0 +1,12 @@
package net.codinux.banking.dataaccess
import net.codinux.banking.client.model.AccountTransaction
import net.codinux.banking.dataaccess.entities.AccountTransactionEntity
interface BankingRepository {
fun getAllAccountTransactions(): List<AccountTransactionEntity>
suspend fun persistAccountTransactions(transactions: Collection<AccountTransaction>)
}

View File

@ -0,0 +1,30 @@
package net.codinux.banking.dataaccess
import net.codinux.banking.client.model.AccountTransaction
import net.codinux.banking.dataaccess.entities.AccountTransactionEntity
class InMemoryBankingRepository(
transactions: Collection<AccountTransaction>
) : BankingRepository {
private var nextId = 0L // TODO: make thread-safe
private val transactions = transactions.map { map(it) }.toMutableList()
override fun getAllAccountTransactions(): List<AccountTransactionEntity> = transactions.toList()
override suspend fun persistAccountTransactions(transactions: Collection<AccountTransaction>) {
this.transactions.addAll(transactions.map { map(it) })
}
private fun map(transaction: AccountTransaction) = AccountTransactionEntity(
nextId++,
transaction.amount, transaction.currency, transaction.reference,
transaction.bookingDate, transaction.valueDate,
transaction.otherPartyName, transaction.otherPartyBankCode, transaction.otherPartyAccountId,
transaction.bookingText
)
}

View File

@ -0,0 +1,92 @@
package net.codinux.banking.dataaccess
import app.cash.sqldelight.db.SqlDriver
import kotlinx.datetime.LocalDate
import net.codinux.banking.client.model.AccountTransaction
import net.codinux.banking.client.model.Amount
import net.codinux.banking.dataaccess.entities.AccountTransactionEntity
class SqliteBankingRepository(
sqlDriver: SqlDriver
) : BankingRepository {
private val database = BankmeisterDb(sqlDriver)
private val accountTransactionQueries = database.accountTransactionQueries
override fun getAllAccountTransactions(): List<AccountTransactionEntity> {
return accountTransactionQueries.selectAllTransactions { id, amount, currency, reference, bookingDate, valueDate, otherPartyName, otherPartyBankCode, otherPartyAccountId, bookingText, userSetDisplayName, notes, information, statementNumber, sequenceNumber, openingBalance, closingBalance, endToEndReference, customerReference, mandateReference, creditorIdentifier, originatorsIdentificationCode, compensationAmount, originalAmount, sepaReference, deviantOriginator, deviantRecipient, referenceWithNoSpecialType, primaNotaNumber, textKeySupplement, currencyType, bookingKey, referenceForTheAccountOwner, referenceOfTheAccountServicingInstitution, supplementaryDetails, transactionReferenceNumber, relatedReferenceNumber ->
AccountTransactionEntity(
id,
Amount(amount), currency, reference,
mapToDate(bookingDate), mapToDate(valueDate),
otherPartyName, otherPartyBankCode, otherPartyAccountId,
bookingText,
userSetDisplayName, notes,
information,
statementNumber?.toInt(), sequenceNumber?.toInt(),
mapToAmount(openingBalance), mapToAmount(closingBalance),
endToEndReference, customerReference, mandateReference,
creditorIdentifier, originatorsIdentificationCode,
compensationAmount, originalAmount,
sepaReference,
deviantOriginator, deviantRecipient,
referenceWithNoSpecialType, primaNotaNumber,
textKeySupplement,
currencyType, bookingKey,
referenceForTheAccountOwner, referenceOfTheAccountServicingInstitution,
supplementaryDetails,
transactionReferenceNumber, relatedReferenceNumber
)
}.executeAsList()
}
override suspend fun persistAccountTransactions(transactions: Collection<AccountTransaction>) {
transactions.forEach {
saveAccountTransaction(it)
}
}
private suspend fun saveAccountTransaction(transaction: AccountTransaction) {
accountTransactionQueries.insertTransaction(
transaction.amount.amount, transaction.currency, transaction.reference,
mapDate(transaction.bookingDate), mapDate(transaction.valueDate),
transaction.otherPartyName, transaction.otherPartyBankCode, transaction.otherPartyAccountId,
transaction.bookingText,
transaction.userSetDisplayName, transaction.notes,
transaction.information,
transaction.statementNumber?.toLong(), transaction.sequenceNumber?.toLong(),
transaction.openingBalance?.amount, transaction.closingBalance?.amount,
transaction.endToEndReference, transaction.customerReference, transaction.mandateReference,
transaction.creditorIdentifier, transaction.originatorsIdentificationCode,
transaction.compensationAmount, transaction.originalAmount,
transaction.sepaReference,
transaction.deviantOriginator, transaction.deviantRecipient,
transaction.referenceWithNoSpecialType, transaction.primaNotaNumber, transaction.textKeySupplement,
transaction.currencyType, transaction.bookingKey,
transaction.referenceForTheAccountOwner, transaction.referenceOfTheAccountServicingInstitution,
transaction.supplementaryDetails,
transaction.transactionReferenceNumber, transaction.relatedReferenceNumber
)
}
private fun mapToAmount(serializedAmount: String?): Amount? = serializedAmount?.let { Amount(it) }
private fun mapDate(date: LocalDate): String = date.toString()
private fun mapToDate(serializedDate: String): LocalDate = LocalDate.parse(serializedDate)
}

View File

@ -0,0 +1,80 @@
package net.codinux.banking.dataaccess.entities
import kotlinx.datetime.LocalDate
import net.codinux.banking.client.model.AccountTransaction
import net.codinux.banking.client.model.Amount
class AccountTransactionEntity(
val id: Long,
amount: Amount,
currency: String,
reference: String,
bookingDate: LocalDate,
valueDate: LocalDate,
otherPartyName: String? = null,
otherPartyBankCode: String? = null,
otherPartyAccountId: String? = null,
bookingText: String? = null,
userSetDisplayName: String? = null,
notes: String? = null,
information: String? = null,
statementNumber: Int? = null,
sequenceNumber: Int? = null,
openingBalance: Amount? = null,
closingBalance: Amount? = null,
endToEndReference: String? = null,
customerReference: String? = null,
mandateReference: String? = null,
creditorIdentifier: String? = null,
originatorsIdentificationCode: String? = null,
compensationAmount: String? = null,
originalAmount: String? = null,
sepaReference: String? = null,
deviantOriginator: String? = null,
deviantRecipient: String? = null,
referenceWithNoSpecialType: String? = null,
primaNotaNumber: String? = null,
textKeySupplement: String? = null,
currencyType: String? = null,
bookingKey: String? = null,
referenceForTheAccountOwner: String? = null,
referenceOfTheAccountServicingInstitution: String? = null,
supplementaryDetails: String? = null,
transactionReferenceNumber: String? = null,
relatedReferenceNumber: String? = null
) : AccountTransaction(
amount, currency, reference,
bookingDate, valueDate,
otherPartyName, otherPartyBankCode, otherPartyAccountId,
bookingText,
information,
statementNumber, sequenceNumber,
openingBalance, closingBalance,
endToEndReference, customerReference, mandateReference,
creditorIdentifier, originatorsIdentificationCode,
compensationAmount, originalAmount,
sepaReference,
deviantOriginator, deviantRecipient,
referenceWithNoSpecialType, primaNotaNumber,
textKeySupplement,
currencyType, bookingKey,
referenceForTheAccountOwner, referenceOfTheAccountServicingInstitution,
supplementaryDetails,
transactionReferenceNumber, relatedReferenceNumber,
userSetDisplayName, notes
)

View File

@ -14,7 +14,6 @@ import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import net.codinux.banking.client.model.AccountTransaction
import net.codinux.banking.ui.composables.StateHandler import net.codinux.banking.ui.composables.StateHandler
import net.codinux.banking.ui.composables.TransactionsList import net.codinux.banking.ui.composables.TransactionsList
import net.codinux.banking.ui.dialogs.AddAccountDialog import net.codinux.banking.ui.dialogs.AddAccountDialog

View File

@ -1,10 +1,12 @@
package net.codinux.banking.ui.config package net.codinux.banking.ui.config
import app.cash.sqldelight.db.SqlDriver
import net.codinux.banking.dataaccess.BankingRepository
import net.codinux.banking.dataaccess.InMemoryBankingRepository
import net.codinux.banking.dataaccess.SqliteBankingRepository
import net.codinux.banking.ui.Platform import net.codinux.banking.ui.Platform
import net.codinux.banking.ui.getPlatform import net.codinux.banking.ui.getPlatform
import net.codinux.banking.ui.service.BankFinder import net.codinux.banking.ui.service.*
import net.codinux.banking.ui.service.BankingService
import net.codinux.banking.ui.service.FormatUtil
import net.codinux.banking.ui.state.UiState import net.codinux.banking.ui.state.UiState
object DI { object DI {
@ -13,12 +15,24 @@ object DI {
val platform: Platform = getPlatform() val platform: Platform = getPlatform()
val bankFinder = BankFinder()
val bankingService = BankingService(uiState, bankFinder)
val formatUtil = FormatUtil() val formatUtil = FormatUtil()
val bankFinder = BankFinder()
var bankingRepository: BankingRepository = InMemoryBankingRepository(emptyList())
val bankingService by lazy {
BankingService(uiState, bankingRepository, bankFinder) }
fun setRepository(sqlDriver: SqlDriver) = setRepository(SqliteBankingRepository(sqlDriver))
fun setRepository(repository: BankingRepository) {
this.bankingRepository = repository
}
suspend fun init() { suspend fun init() {
bankingService.init() bankingService.init()

View File

@ -10,6 +10,7 @@ import net.codinux.banking.client.model.options.GetAccountDataOptions
import net.codinux.banking.client.model.options.RetrieveTransactions import net.codinux.banking.client.model.options.RetrieveTransactions
import net.codinux.banking.client.model.request.GetAccountDataRequest import net.codinux.banking.client.model.request.GetAccountDataRequest
import net.codinux.banking.client.model.response.* import net.codinux.banking.client.model.response.*
import net.codinux.banking.dataaccess.BankingRepository
import net.codinux.banking.fints.config.FinTsClientConfiguration import net.codinux.banking.fints.config.FinTsClientConfiguration
import net.codinux.banking.fints.config.FinTsClientOptions import net.codinux.banking.fints.config.FinTsClientOptions
import net.codinux.banking.ui.model.BankInfo import net.codinux.banking.ui.model.BankInfo
@ -25,6 +26,7 @@ import org.jetbrains.compose.resources.ExperimentalResourceApi
@OptIn(ExperimentalResourceApi::class) @OptIn(ExperimentalResourceApi::class)
class BankingService( class BankingService(
private val uiState: UiState, private val uiState: UiState,
private val bankingRepository: BankingRepository,
private val bankFinder: BankFinder private val bankFinder: BankFinder
) { ) {
@ -36,9 +38,11 @@ class BankingService(
suspend fun init() { suspend fun init() {
val transactions = readTransactionsFromCsv() try {
uiState.transactions.value = bankingRepository.getAllAccountTransactions()
uiState.transactions.value = transactions } catch (e: Throwable) {
log.error(e) { "Could not read all account transactions from repository" }
}
} }
suspend fun findBanks(query: String): List<BankInfo> = suspend fun findBanks(query: String): List<BankInfo> =
@ -64,13 +68,21 @@ class BankingService(
} }
} }
private fun handleSuccessfulGetAccountDataResponse(response: GetAccountDataResponse) { private suspend fun handleSuccessfulGetAccountDataResponse(response: GetAccountDataResponse) {
// TODO: save customer // TODO: save customer
val transactions = uiState.transactions.value.toMutableList() val transactions = uiState.transactions.value.toMutableList()
transactions.addAll(response.bookedTransactions) transactions.addAll(response.bookedTransactions)
uiState.transactions.value = transactions.sortedByDescending { it.valueDate } uiState.transactions.value = transactions.sortedByDescending { it.valueDate }
try {
bankingRepository.persistAccountTransactions(response.bookedTransactions)
log.info { "Saved ${response.bookedTransactions.size} transactions" }
} catch (e: Throwable) {
log.error(e) { "Could not save account transactions ${response.bookedTransactions}" }
}
} }
private fun handleUnsuccessfulBankingClientResponse(action: BankingClientAction, response: Response<*>) { private fun handleUnsuccessfulBankingClientResponse(action: BankingClientAction, response: Response<*>) {

View File

@ -0,0 +1,110 @@
CREATE TABLE IF NOT EXISTS AccountTransaction (
id INTEGER PRIMARY KEY AUTOINCREMENT,
amount TEXT NOT NULL,
currency TEXT NOT NULL,
reference TEXT NOT NULL,
bookingDate TEXT NOT NULL,
valueDate TEXT NOT NULL,
otherPartyName TEXT,
otherPartyBankCode TEXT,
otherPartyAccountId TEXT,
bookingText TEXT,
userSetDisplayName TEXT,
notes TEXT,
information TEXT,
statementNumber INTEGER,
sequenceNumber INTEGER,
openingBalance TEXT,
closingBalance TEXT,
endToEndReference TEXT,
customerReference TEXT,
mandateReference TEXT,
creditorIdentifier TEXT,
originatorsIdentificationCode TEXT,
compensationAmount TEXT,
originalAmount TEXT,
sepaReference TEXT,
deviantOriginator TEXT,
deviantRecipient TEXT,
referenceWithNoSpecialType TEXT,
primaNotaNumber TEXT,
textKeySupplement TEXT,
currencyType TEXT,
bookingKey TEXT,
referenceForTheAccountOwner TEXT,
referenceOfTheAccountServicingInstitution TEXT,
supplementaryDetails TEXT,
transactionReferenceNumber TEXT,
relatedReferenceNumber TEXT
);
insertTransaction:
INSERT INTO AccountTransaction(
amount, currency, reference,
bookingDate, valueDate,
otherPartyName, otherPartyBankCode, otherPartyAccountId,
bookingText,
userSetDisplayName, notes,
information,
statementNumber, sequenceNumber,
openingBalance, closingBalance,
endToEndReference, customerReference, mandateReference,
creditorIdentifier, originatorsIdentificationCode,
compensationAmount, originalAmount,
sepaReference,
deviantOriginator, deviantRecipient,
referenceWithNoSpecialType,
primaNotaNumber, textKeySupplement,
currencyType, bookingKey,
referenceForTheAccountOwner, referenceOfTheAccountServicingInstitution,
supplementaryDetails,
transactionReferenceNumber, relatedReferenceNumber
)
VALUES(
?, ?, ?,
?, ?,
?, ?, ?,
?,
?, ?,
?,
?, ?,
?, ?,
?, ?, ?,
?, ?,
?, ?,
?,
?, ?,
?,
?, ?,
?, ?,
?, ?,
?,
?, ?
);
selectAllTransactions:
SELECT AccountTransaction.*
FROM AccountTransaction;

View File

@ -4,9 +4,15 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.* import androidx.compose.ui.window.*
import app.cash.sqldelight.async.coroutines.synchronous
import app.cash.sqldelight.driver.jdbc.sqlite.JdbcSqliteDriver
import bankmeister.composeapp.generated.resources.AppIcon_svg import bankmeister.composeapp.generated.resources.AppIcon_svg
import bankmeister.composeapp.generated.resources.Res import bankmeister.composeapp.generated.resources.Res
import net.codinux.banking.dataaccess.BankmeisterDb
import net.codinux.banking.ui.config.DI
import net.codinux.log.Log
import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.resources.painterResource
import java.io.File
fun main() = application { fun main() = application {
Window( Window(
@ -15,6 +21,11 @@ fun main() = application {
icon = painterResource(Res.drawable.AppIcon_svg), icon = painterResource(Res.drawable.AppIcon_svg),
state = WindowState(position = WindowPosition(Alignment.Center), size = DpSize(900.dp, 800.dp)), state = WindowState(position = WindowPosition(Alignment.Center), size = DpSize(900.dp, 800.dp)),
) { ) {
File("data/db").mkdirs()
DI.setRepository(JdbcSqliteDriver("jdbc:sqlite:data/db/Bankmeister.db").apply {
val schema = BankmeisterDb.Schema.synchronous().create(this)
})
App() App()
} }
} }

View File

@ -0,0 +1,43 @@
package net.codinux.banking.dataaccess
import app.cash.sqldelight.async.coroutines.synchronous
import app.cash.sqldelight.driver.jdbc.sqlite.JdbcSqliteDriver
import kotlinx.coroutines.runBlocking
import kotlinx.datetime.LocalDate
import net.codinux.banking.client.model.AccountTransaction
import net.codinux.banking.client.model.Amount
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
class SqliteBankingRepositoryTest {
private val sqlDriver = JdbcSqliteDriver(JdbcSqliteDriver.IN_MEMORY).apply {
BankmeisterDb.Schema.synchronous().create(this)
}
private val underTest = SqliteBankingRepository(sqlDriver)
@Test
fun saveTransaction() = runBlocking {
val transaction = AccountTransaction(Amount("12.45"), "EUR", "Lohn", LocalDate(2024, 5, 7), LocalDate(2024, 6, 15), "Dein Boss")
underTest.persistAccountTransactions(listOf(transaction))
val result = underTest.getAllAccountTransactions()
assertEquals(1, result.size)
val persisted = result.first()
assertNotNull(persisted.id)
assertEquals(transaction.amount, persisted.amount)
assertEquals(transaction.currency, persisted.currency)
assertEquals(transaction.reference, persisted.reference)
assertEquals(transaction.bookingDate, persisted.bookingDate)
assertEquals(transaction.valueDate, persisted.valueDate)
assertEquals(transaction.otherPartyName, persisted.otherPartyName)
}
}

View File

@ -1,5 +1,13 @@
package net.codinux.banking.ui package net.codinux.banking.ui
import androidx.compose.ui.window.ComposeUIViewController import androidx.compose.ui.window.ComposeUIViewController
import app.cash.sqldelight.async.coroutines.synchronous
import app.cash.sqldelight.driver.native.NativeSqliteDriver
import net.codinux.banking.dataaccess.BankmeisterDb
import net.codinux.banking.ui.config.DI
fun MainViewController() = ComposeUIViewController { App() } fun MainViewController() = ComposeUIViewController {
DI.setRepository(NativeSqliteDriver(Database.Schema.synchronous(), "Bankmeister.db"))
App()
}

View File

@ -10,6 +10,8 @@ kotlinx-serializable = "1.7.1"
klf = "1.6.0" klf = "1.6.0"
logback = "1.5.7" logback = "1.5.7"
sqlDelight = "2.0.2"
agp = "8.2.2" agp = "8.2.2"
android-compileSdk = "34" android-compileSdk = "34"
android-minSdk = "24" android-minSdk = "24"
@ -36,6 +38,13 @@ kotlinx-serializable = { group = "org.jetbrains.kotlinx", name = "kotlinx-serial
klf = { group = "net.codinux.log", name = "klf", version.ref = "klf" } klf = { group = "net.codinux.log", name = "klf", version.ref = "klf" }
logback = { group = "ch.qos.logback", name = "logback-classic", version.ref = "logback" } logback = { group = "ch.qos.logback", name = "logback-classic", version.ref = "logback" }
sqldelight-runtime = { module = "app.cash.sqldelight:runtime", version.ref = "sqlDelight" }
sqldelight-coroutines-extensions = { module = "app.cash.sqldelight:coroutines-extensions", version.ref = "sqlDelight" }
sqldelight-paging-extensions = { module = "app.cash.sqldelight:androidx-paging3-extensions", version.ref = "sqlDelight" }
sqldelight-sqlite-driver = { module = "app.cash.sqldelight:sqlite-driver", version.ref = "sqlDelight" }
sqldelight-android-driver = { module = "app.cash.sqldelight:android-driver", version.ref = "sqlDelight" }
sqldelight-native-driver = { module = "app.cash.sqldelight:native-driver", version.ref = "sqlDelight" }
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "androidx-core-ktx" } androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "androidx-core-ktx" }
androidx-test-junit = { group = "androidx.test.ext", name = "junit", version.ref = "androidx-test-junit" } androidx-test-junit = { group = "androidx.test.ext", name = "junit", version.ref = "androidx-test-junit" }
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "androidx-espresso-core" } androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "androidx-espresso-core" }
@ -57,4 +66,6 @@ kotlinxSerialization = { id = "org.jetbrains.kotlin.plugin.serialization", versi
jetbrainsCompose = { id = "org.jetbrains.compose", version.ref = "compose-plugin" } jetbrainsCompose = { id = "org.jetbrains.compose", version.ref = "compose-plugin" }
compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
androidLibrary = { id = "com.android.library", version.ref = "agp" } androidLibrary = { id = "com.android.library", version.ref = "agp" }
androidApplication = { id = "com.android.application", version.ref = "agp" } androidApplication = { id = "com.android.application", version.ref = "agp" }
sqldelight = { id = "app.cash.sqldelight", version.ref = "sqlDelight" }