From 61dcd1343f6717d1f2cb0da2a850422464c26af5 Mon Sep 17 00:00:00 2001 From: Michal Guspiel Date: Wed, 7 Feb 2024 21:32:36 +0200 Subject: [PATCH 01/12] Initial changes commit --- .../loans/loandetails/LoanDetailsScreen.kt | 73 ++++++++++- .../java/com/ivy/base/model/LoanRecordType.kt | 12 ++ .../com.ivy.data.db.IvyRoomDatabase/126.json | 12 +- .../java/com/ivy/data/db/IvyRoomDatabase.kt | 6 +- .../com/ivy/data/db/RoomTypeConverters.kt | 7 + .../ivy/data/db/entity/LoanRecordEntity.kt | 3 + .../Migration126to127_LoanRecordType.kt | 10 ++ .../resources/src/main/res/values/strings.xml | 4 + .../com/ivy/legacy/datamodel/LoanRecord.kt | 3 + .../legacy/datamodel/temp/LoanRecordExt.kt | 1 + .../deprecated/logic/LoanRecordCreator.kt | 3 +- .../logic/model/CreateLoanRecordData.kt | 4 +- .../legacy/ui/theme/modal/LoanRecordModal.kt | 121 +++++++++++++++++- 13 files changed, 243 insertions(+), 16 deletions(-) create mode 100644 shared/base/src/main/java/com/ivy/base/model/LoanRecordType.kt create mode 100644 shared/data/src/main/java/com/ivy/data/db/migration/Migration126to127_LoanRecordType.kt diff --git a/screen/loans/src/main/java/com/ivy/loans/loandetails/LoanDetailsScreen.kt b/screen/loans/src/main/java/com/ivy/loans/loandetails/LoanDetailsScreen.kt index 69cfa44f28..64dc1fb60d 100644 --- a/screen/loans/src/main/java/com/ivy/loans/loandetails/LoanDetailsScreen.kt +++ b/screen/loans/src/main/java/com/ivy/loans/loandetails/LoanDetailsScreen.kt @@ -33,6 +33,7 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel +import com.ivy.base.model.LoanRecordType import com.ivy.base.model.TransactionType import com.ivy.data.model.LoanType import com.ivy.design.l0_system.UI @@ -170,8 +171,14 @@ private fun BoxWithConstraintsScope.UI( displayLoanRecord ) ) - } - ) + }) + item { + InitialRecordItem( + loan = state.loan, + amount = state.loan.amount, + baseCurrency = state.baseCurrency, + ) + } } if (state.displayLoanRecords.isEmpty()) { @@ -730,6 +737,59 @@ private fun LoanRecordItem( } } +@Composable +private fun InitialRecordItem( + loan: Loan, + amount: Double, + baseCurrency: String, +) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + .clip(UI.shapes.r4) + .background(UI.colors.medium, UI.shapes.r4) + .testTag("loan_record_item") + ) { + + IvyButton( + modifier = Modifier.padding(16.dp), + backgroundGradient = Gradient.solid(UI.colors.pure), + text = stringResource(id = R.string.initial_loan_record), + iconTint = UI.colors.pureInverse, + iconStart = getCustomIconIdS( + iconName = loan.icon, + defaultIcon = R.drawable.ic_custom_loan_s + ), + textStyle = UI.typo.c.style( + color = UI.colors.pureInverse, fontWeight = FontWeight.ExtraBold + ), + padding = 8.dp, + ) {} + + + loan.dateTime?.formatNicelyWithTime(noWeekDay = false)?.let { nicelyFormattedDate -> + Text( + modifier = Modifier.padding(horizontal = 24.dp), + text = nicelyFormattedDate.uppercase(), + style = UI.typo.nC.style( + color = Gray, fontWeight = FontWeight.Bold + ) + ) + } + + Spacer(modifier = Modifier.height(16.dp)) + + TypeAmountCurrency( + transactionType = if (loan.type == LoanType.LEND) TransactionType.EXPENSE else TransactionType.INCOME, + dueDate = null, + currency = baseCurrency, + amount = amount + ) + Spacer(Modifier.height(16.dp)) + } +} + @Composable private fun NoLoanRecordsEmptyState() { Column( @@ -816,14 +876,16 @@ private fun Preview_Records() { amount = 123.45, dateTime = timeNowUTC().minusDays(1), note = "Cash", - loanId = UUID.randomUUID() + loanId = UUID.randomUUID(), + loanRecordType = LoanRecordType.INCREASE ) ), DisplayLoanRecord( LoanRecord( amount = 0.50, dateTime = timeNowUTC().minusYears(1), - loanId = UUID.randomUUID() + loanId = UUID.randomUUID(), + loanRecordType = LoanRecordType.DECREASE ) ), DisplayLoanRecord( @@ -831,7 +893,8 @@ private fun Preview_Records() { amount = 1000.00, dateTime = timeNowUTC().minusMonths(1), note = "Revolut", - loanId = UUID.randomUUID() + loanId = UUID.randomUUID(), + loanRecordType = LoanRecordType.INCREASE ) ), ), diff --git a/shared/base/src/main/java/com/ivy/base/model/LoanRecordType.kt b/shared/base/src/main/java/com/ivy/base/model/LoanRecordType.kt new file mode 100644 index 0000000000..e58a85f0e8 --- /dev/null +++ b/shared/base/src/main/java/com/ivy/base/model/LoanRecordType.kt @@ -0,0 +1,12 @@ +package com.ivy.base.model + +import androidx.annotation.Keep +import androidx.compose.runtime.Immutable +import kotlinx.serialization.Serializable + +@Immutable +@Keep +@Serializable +enum class LoanRecordType { + INCREASE, DECREASE +} diff --git a/shared/data/schemas/com.ivy.data.db.IvyRoomDatabase/126.json b/shared/data/schemas/com.ivy.data.db.IvyRoomDatabase/126.json index 38a879872c..d4af7b66c4 100644 --- a/shared/data/schemas/com.ivy.data.db.IvyRoomDatabase/126.json +++ b/shared/data/schemas/com.ivy.data.db.IvyRoomDatabase/126.json @@ -2,7 +2,7 @@ "formatVersion": 1, "database": { "version": 126, - "identityHash": "712907ba120b6227af4b79fca7ff41db", + "identityHash": "3e221e19eb3210bba162a6a86341c440", "entities": [ { "tableName": "accounts", @@ -637,7 +637,7 @@ }, { "tableName": "loan_records", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`loanId` TEXT NOT NULL, `amount` REAL NOT NULL, `note` TEXT, `dateTime` INTEGER NOT NULL, `interest` INTEGER NOT NULL, `accountId` TEXT, `convertedAmount` REAL, `isSynced` INTEGER NOT NULL, `isDeleted` INTEGER NOT NULL, `id` TEXT NOT NULL, PRIMARY KEY(`id`))", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`loanId` TEXT NOT NULL, `amount` REAL NOT NULL, `note` TEXT, `dateTime` INTEGER NOT NULL, `interest` INTEGER NOT NULL, `accountId` TEXT, `convertedAmount` REAL, `loanRecordType` TEXT, `isSynced` INTEGER NOT NULL, `isDeleted` INTEGER NOT NULL, `id` TEXT NOT NULL, PRIMARY KEY(`id`))", "fields": [ { "fieldPath": "loanId", @@ -681,6 +681,12 @@ "affinity": "REAL", "notNull": false }, + { + "fieldPath": "loanRecordType", + "columnName": "loanRecordType", + "affinity": "TEXT", + "notNull": false + }, { "fieldPath": "isSynced", "columnName": "isSynced", @@ -820,7 +826,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, '712907ba120b6227af4b79fca7ff41db')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '3e221e19eb3210bba162a6a86341c440')" ] } } \ No newline at end of file diff --git a/shared/data/src/main/java/com/ivy/data/db/IvyRoomDatabase.kt b/shared/data/src/main/java/com/ivy/data/db/IvyRoomDatabase.kt index 5d8f4fcd7d..a99eae6a90 100644 --- a/shared/data/src/main/java/com/ivy/data/db/IvyRoomDatabase.kt +++ b/shared/data/src/main/java/com/ivy/data/db/IvyRoomDatabase.kt @@ -40,6 +40,7 @@ import com.ivy.data.db.entity.TransactionEntity import com.ivy.data.db.entity.UserEntity import com.ivy.data.db.migration.Migration123to124_LoanIncludeDateTime import com.ivy.data.db.migration.Migration124to125_LoanEditDateTime +import com.ivy.data.db.migration.Migration126to127_LoanRecordType import com.ivy.domain.db.RoomTypeConverters import com.ivy.domain.db.migration.Migration105to106_TrnRecurringRules import com.ivy.domain.db.migration.Migration106to107_Wishlist @@ -74,7 +75,7 @@ import com.ivy.domain.db.migration.Migration125to126_Tags spec = IvyRoomDatabase.DeleteSEMigration::class ) ], - version = 126, + version = 127, exportSchema = true ) @TypeConverters(RoomTypeConverters::class) @@ -127,7 +128,8 @@ abstract class IvyRoomDatabase : RoomDatabase() { Migration122to123_ExchangeRates(), Migration123to124_LoanIncludeDateTime(), Migration124to125_LoanEditDateTime(), - Migration125to126_Tags() + Migration125to126_Tags(), + Migration126to127_LoanRecordType() ) @Suppress("SpreadOperator") diff --git a/shared/data/src/main/java/com/ivy/data/db/RoomTypeConverters.kt b/shared/data/src/main/java/com/ivy/data/db/RoomTypeConverters.kt index ed1528dfb9..9fb335b9a7 100644 --- a/shared/data/src/main/java/com/ivy/data/db/RoomTypeConverters.kt +++ b/shared/data/src/main/java/com/ivy/data/db/RoomTypeConverters.kt @@ -4,6 +4,7 @@ import androidx.room.TypeConverter import com.ivy.base.legacy.Theme import com.ivy.base.legacy.epochMilliToDateTime import com.ivy.base.legacy.toEpochMilli +import com.ivy.base.model.LoanRecordType import com.ivy.base.model.TransactionType import com.ivy.data.model.IntervalType import com.ivy.data.model.LoanType @@ -55,4 +56,10 @@ class RoomTypeConverters { @TypeConverter fun parseInstant(value: Long): Instant = Instant.ofEpochMilli(value) + + @TypeConverter + fun saveLoanRecordType(value: LoanRecordType?) = value?.name + + @TypeConverter + fun parseLoanRecordType(value: String?) = value?.let { LoanRecordType.valueOf(it) } } diff --git a/shared/data/src/main/java/com/ivy/data/db/entity/LoanRecordEntity.kt b/shared/data/src/main/java/com/ivy/data/db/entity/LoanRecordEntity.kt index 349bd16061..b01fd52c15 100644 --- a/shared/data/src/main/java/com/ivy/data/db/entity/LoanRecordEntity.kt +++ b/shared/data/src/main/java/com/ivy/data/db/entity/LoanRecordEntity.kt @@ -5,6 +5,7 @@ import androidx.room.Entity import androidx.room.PrimaryKey import com.ivy.base.kotlinxserilzation.KSerializerLocalDateTime import com.ivy.base.kotlinxserilzation.KSerializerUUID +import com.ivy.base.model.LoanRecordType import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import java.time.LocalDateTime @@ -32,6 +33,8 @@ data class LoanRecordEntity( // This is used store the converted amount for currencies which are different from the loan account currency @SerialName("convertedAmount") val convertedAmount: Double? = null, + @SerialName("loanRecordType") + val loanRecordType: LoanRecordType, @SerialName("isSynced") val isSynced: Boolean = false, diff --git a/shared/data/src/main/java/com/ivy/data/db/migration/Migration126to127_LoanRecordType.kt b/shared/data/src/main/java/com/ivy/data/db/migration/Migration126to127_LoanRecordType.kt new file mode 100644 index 0000000000..050e17f74d --- /dev/null +++ b/shared/data/src/main/java/com/ivy/data/db/migration/Migration126to127_LoanRecordType.kt @@ -0,0 +1,10 @@ +package com.ivy.data.db.migration + +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase + +class Migration126to127_LoanRecordType : Migration(126, 127) { + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL("ALTER TABLE loan_records ADD COLUMN loanRecordType TEXT NOT NULL DEFAULT 'DECREASE'") + } +} \ No newline at end of file diff --git a/shared/resources/src/main/res/values/strings.xml b/shared/resources/src/main/res/values/strings.xml index e6469a772b..a8d34e96fc 100644 --- a/shared/resources/src/main/res/values/strings.xml +++ b/shared/resources/src/main/res/values/strings.xml @@ -441,4 +441,8 @@ Experimental Settings Wallet balance Total (exclusive): %1$s %2$s + Starting record + Record type + Increase + Decrease diff --git a/temp/legacy-code/src/main/java/com/ivy/legacy/datamodel/LoanRecord.kt b/temp/legacy-code/src/main/java/com/ivy/legacy/datamodel/LoanRecord.kt index 2f0e2af158..f4661aa2eb 100644 --- a/temp/legacy-code/src/main/java/com/ivy/legacy/datamodel/LoanRecord.kt +++ b/temp/legacy-code/src/main/java/com/ivy/legacy/datamodel/LoanRecord.kt @@ -1,6 +1,7 @@ package com.ivy.legacy.datamodel import androidx.compose.runtime.Immutable +import com.ivy.base.model.LoanRecordType import com.ivy.data.db.entity.LoanRecordEntity import java.time.LocalDateTime import java.util.UUID @@ -16,6 +17,7 @@ data class LoanRecord( val accountId: UUID? = null, // This is used store the converted amount for currencies which are different from the loan account currency val convertedAmount: Double? = null, + val loanRecordType: LoanRecordType, val isSynced: Boolean = false, val isDeleted: Boolean = false, @@ -30,6 +32,7 @@ data class LoanRecord( interest = interest, accountId = accountId, convertedAmount = convertedAmount, + loanRecordType = loanRecordType, isSynced = isSynced, isDeleted = isDeleted, id = id diff --git a/temp/legacy-code/src/main/java/com/ivy/legacy/datamodel/temp/LoanRecordExt.kt b/temp/legacy-code/src/main/java/com/ivy/legacy/datamodel/temp/LoanRecordExt.kt index 3ca740c4a5..a4925e64b7 100644 --- a/temp/legacy-code/src/main/java/com/ivy/legacy/datamodel/temp/LoanRecordExt.kt +++ b/temp/legacy-code/src/main/java/com/ivy/legacy/datamodel/temp/LoanRecordExt.kt @@ -11,6 +11,7 @@ fun LoanRecordEntity.toDomain(): LoanRecord = LoanRecord( interest = interest, accountId = accountId, convertedAmount = convertedAmount, + loanRecordType = loanRecordType, isSynced = isSynced, isDeleted = isDeleted, id = id diff --git a/temp/legacy-code/src/main/java/com/ivy/legacy/domain/deprecated/logic/LoanRecordCreator.kt b/temp/legacy-code/src/main/java/com/ivy/legacy/domain/deprecated/logic/LoanRecordCreator.kt index 8221a97952..03483c535f 100644 --- a/temp/legacy-code/src/main/java/com/ivy/legacy/domain/deprecated/logic/LoanRecordCreator.kt +++ b/temp/legacy-code/src/main/java/com/ivy/legacy/domain/deprecated/logic/LoanRecordCreator.kt @@ -29,7 +29,8 @@ class LoanRecordCreator @Inject constructor( isSynced = false, interest = data.interest, accountId = data.account?.id, - convertedAmount = data.convertedAmount + convertedAmount = data.convertedAmount, + loanRecordType = data.loanRecordType ) loanRecordWriter.save(item.toEntity()) diff --git a/temp/legacy-code/src/main/java/com/ivy/legacy/domain/deprecated/logic/model/CreateLoanRecordData.kt b/temp/legacy-code/src/main/java/com/ivy/legacy/domain/deprecated/logic/model/CreateLoanRecordData.kt index 212609cded..8d7d9522c5 100644 --- a/temp/legacy-code/src/main/java/com/ivy/legacy/domain/deprecated/logic/model/CreateLoanRecordData.kt +++ b/temp/legacy-code/src/main/java/com/ivy/legacy/domain/deprecated/logic/model/CreateLoanRecordData.kt @@ -1,5 +1,6 @@ package com.ivy.wallet.domain.deprecated.logic.model +import com.ivy.base.model.LoanRecordType import com.ivy.legacy.datamodel.Account import java.time.LocalDateTime @@ -10,5 +11,6 @@ data class CreateLoanRecordData( val interest: Boolean = false, val account: Account? = null, val createLoanRecordTransaction: Boolean = false, - val convertedAmount: Double? = null + val convertedAmount: Double? = null, + val loanRecordType: LoanRecordType ) diff --git a/temp/legacy-code/src/main/java/com/ivy/legacy/legacy/ui/theme/modal/LoanRecordModal.kt b/temp/legacy-code/src/main/java/com/ivy/legacy/legacy/ui/theme/modal/LoanRecordModal.kt index 5e0f3900d2..2d5d3b1fe3 100644 --- a/temp/legacy-code/src/main/java/com/ivy/legacy/legacy/ui/theme/modal/LoanRecordModal.kt +++ b/temp/legacy-code/src/main/java/com/ivy/legacy/legacy/ui/theme/modal/LoanRecordModal.kt @@ -1,5 +1,6 @@ package com.ivy.wallet.ui.theme.modal +import android.annotation.SuppressLint import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.clickable @@ -23,14 +24,17 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import com.ivy.base.model.LoanRecordType import com.ivy.design.l0_system.UI import com.ivy.design.l0_system.style +import com.ivy.design.utils.thenIf import com.ivy.frp.test.TestingContext import com.ivy.legacy.IvyWalletPreview import com.ivy.legacy.datamodel.Account @@ -40,7 +44,6 @@ import com.ivy.legacy.legacy.ui.theme.modal.ModalNameInput import com.ivy.legacy.utils.getDefaultFIATCurrency import com.ivy.legacy.utils.onScreenStart import com.ivy.legacy.utils.selectEndTextFieldValue -import com.ivy.design.utils.thenIf import com.ivy.legacy.utils.timeNowUTC import com.ivy.resources.R import com.ivy.wallet.domain.deprecated.logic.model.CreateAccountData @@ -66,7 +69,8 @@ data class LoanRecordModalData( val selectedAccount: Account? = null, val createLoanRecordTransaction: Boolean = false, val isLoanInterest: Boolean = false, - val id: UUID = UUID.randomUUID() + val id: UUID = UUID.randomUUID(), + val loanRecordType: LoanRecordType? = null ) @Deprecated("Old design system. Use `:ivy-design` and Material3") @@ -109,6 +113,9 @@ fun BoxWithConstraintsScope.LoanRecordModal( var reCalculateVisible by remember(modal) { mutableStateOf(modal?.loanAccountCurrencyCode != null && modal.loanAccountCurrencyCode != modal.baseCurrency) } + var loanRecordType by remember(modal) { + mutableStateOf(modal?.loanRecordType ?: LoanRecordType.INCREASE) + } var amountModalVisible by remember { mutableStateOf(false) } var deleteModalVisible by remember(modal) { mutableStateOf(false) } @@ -128,7 +135,7 @@ fun BoxWithConstraintsScope.LoanRecordModal( ) { accountChangeConformationModal = initialRecord != null && modal.selectedAccount != null && - modal.baseCurrency != currencyCode && currencyCode != modal.loanAccountCurrencyCode + modal.baseCurrency != currencyCode && currencyCode != modal.loanAccountCurrencyCode if (!accountChangeConformationModal) { save( @@ -140,6 +147,7 @@ fun BoxWithConstraintsScope.LoanRecordModal( selectedAccount = selectedAcc, createLoanRecordTransaction = createLoanRecordTrans, reCalculateAmount = reCalculate, + loanRecordType = loanRecordType, onCreate = onCreate, onEdit = onEdit, @@ -238,6 +246,23 @@ fun BoxWithConstraintsScope.LoanRecordModal( ) Spacer(Modifier.height(16.dp)) + Text( + modifier = Modifier.padding(horizontal = 32.dp), + text = stringResource(R.string.loan_record_type), + style = UI.typo.b2.style( + color = UI.colors.pureInverse, + fontWeight = FontWeight.ExtraBold + ) + ) + + Spacer(Modifier.height(16.dp)) + + LoanRecordTypeRow(selectedRecordType = loanRecordType, onLoanRecordTypeChanged = { + loanRecordType = it + }) + + Spacer(Modifier.height(16.dp)) + IvyCheckboxWithText( modifier = Modifier .padding(start = 16.dp) @@ -339,6 +364,7 @@ fun BoxWithConstraintsScope.LoanRecordModal( selectedAccount = selectedAcc, createLoanRecordTransaction = createLoanRecordTrans, reCalculateAmount = reCalculate, + loanRecordType = loanRecordType, onCreate = onCreate, onEdit = onEdit, @@ -358,6 +384,7 @@ private fun save( createLoanRecordTransaction: Boolean = false, selectedAccount: Account? = null, reCalculateAmount: Boolean = false, + loanRecordType: LoanRecordType, onCreate: (CreateLoanRecordData) -> Unit, onEdit: (EditLoanRecordData) -> Unit, @@ -387,7 +414,8 @@ private fun save( dateTime = dateTime, interest = loanRecordInterest, account = selectedAccount, - createLoanRecordTransaction = createLoanRecordTransaction + createLoanRecordTransaction = createLoanRecordTransaction, + loanRecordType = loanRecordType ) ) } @@ -395,6 +423,91 @@ private fun save( dismiss() } +@Composable +private fun LoanRecordTypeRow( + selectedRecordType: LoanRecordType?, + modifier: Modifier = Modifier, + onLoanRecordTypeChanged: (LoanRecordType) -> Unit +) { + Row( + modifier = modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + ) { + Spacer(Modifier.width(24.dp)) + + LoanRecordType( + modifier = Modifier, + loanRecordType = LoanRecordType.INCREASE, + selectedRecordType = selectedRecordType + ) { + onLoanRecordTypeChanged(it) + } + + LoanRecordType( + modifier = Modifier, + loanRecordType = LoanRecordType.DECREASE, + selectedRecordType = selectedRecordType + ) { + onLoanRecordTypeChanged(it) + } + } + + Spacer(Modifier.width(24.dp)) +} + +@Composable +private fun LoanRecordType( + loanRecordType: LoanRecordType, + selectedRecordType: LoanRecordType?, + modifier: Modifier = Modifier, + onClick: (LoanRecordType) -> Unit +) { + val iconDrawable = + if (loanRecordType == LoanRecordType.INCREASE) R.drawable.ic_donate_plus + else R.drawable.ic_donate_minus + val text = + if (loanRecordType == LoanRecordType.INCREASE) stringResource(id = R.string.increase_loan) + else stringResource(id = R.string.decrease_loan) + val selected = selectedRecordType == loanRecordType + val medium = UI.colors.medium + val rFull = UI.shapes.rFull + val selectedColor = UI.colors.pureInverse + Row( + modifier = modifier + .clip(UI.shapes.rFull) + .thenIf(!selected) { + border(2.dp, medium, rFull) + } + .thenIf(selected) { + background(selectedColor, rFull) + } + .clickable(onClick = { onClick(loanRecordType) }), + verticalAlignment = Alignment.CenterVertically + ) { + Spacer(Modifier.width(12.dp)) + + ItemIconSDefaultIcon( + defaultIcon = iconDrawable, + iconName = null, + tint = UI.colors.pureInverse + ) + + Spacer(Modifier.width(4.dp)) + + Text( + modifier = Modifier.padding(vertical = 10.dp), + text = text, + style = UI.typo.b2.style( + color = UI.colors.pureInverse, + fontWeight = FontWeight.ExtraBold + ) + ) + + Spacer(Modifier.width(24.dp)) + } + Spacer(Modifier.width(8.dp)) +} + @Composable private fun AccountsRow( modifier: Modifier = Modifier, From 5d8200960d9720e6972e6d0f1ada784909812cdb Mon Sep 17 00:00:00 2001 From: Michal Guspiel Date: Wed, 7 Feb 2024 21:32:37 +0200 Subject: [PATCH 02/12] Feature Implementation - LoanDetailsScreen displays list of all records involving records that increase and decrease total loan amount - LoanDetailsScreenState contains calculated `loanTotalAmount` - LoanRecordModal provides a selection if loan record decrease or increase loan amount. Provided loan record increase total amount, `Mark as interest` checkbox disappears. --- .../loans/loandetails/LoanDetailsScreen.kt | 27 +++++++--- .../loandetails/LoanDetailsScreenState.kt | 1 + .../loans/loandetails/LoanDetailsViewModel.kt | 21 ++++++++ .../legacy/ui/theme/modal/LoanRecordModal.kt | 50 ++++++++----------- 4 files changed, 65 insertions(+), 34 deletions(-) diff --git a/screen/loans/src/main/java/com/ivy/loans/loandetails/LoanDetailsScreen.kt b/screen/loans/src/main/java/com/ivy/loans/loandetails/LoanDetailsScreen.kt index 64dc1fb60d..164a4f97ae 100644 --- a/screen/loans/src/main/java/com/ivy/loans/loandetails/LoanDetailsScreen.kt +++ b/screen/loans/src/main/java/com/ivy/loans/loandetails/LoanDetailsScreen.kt @@ -126,6 +126,7 @@ private fun BoxWithConstraintsScope.UI( Header( loan = state.loan, baseCurrency = state.baseCurrency, + loanTotalAmount = state.loanTotalAmount, amountPaid = state.amountPaid, loanAmountPaid = state.loanAmountPaid, itemColor = itemColor, @@ -241,6 +242,7 @@ private fun BoxWithConstraintsScope.UI( private fun Header( loan: Loan, baseCurrency: String, + loanTotalAmount: Double, amountPaid: Double, loanAmountPaid: Double = 0.0, itemColor: Color, @@ -285,7 +287,7 @@ private fun Header( }, textColor = contrastColor, currency = baseCurrency, - balance = loan.amount, + balance = loanTotalAmount, ) Spacer(Modifier.height(20.dp)) @@ -295,6 +297,7 @@ private fun Header( baseCurrency = baseCurrency, amountPaid = amountPaid, loanAmountPaid = loanAmountPaid, + loanTotalAmount = loanTotalAmount, selectedLoanAccount = selectedLoanAccount, onAddRecord = onAddRecord ) @@ -367,6 +370,7 @@ private fun LoanItem( private fun LoanInfoCard( loan: Loan, baseCurrency: String, + loanTotalAmount: Double, amountPaid: Double, loanAmountPaid: Double = 0.0, selectedLoanAccount: Account? = null, @@ -380,8 +384,8 @@ private fun LoanInfoCard( } val contrastColor = findContrastTextColor(backgroundColor) - val percentPaid = amountPaid / loan.amount - val loanPercentPaid = loanAmountPaid / loan.amount + val percentPaid = amountPaid / loanTotalAmount + val loanPercentPaid = loanAmountPaid / loanTotalAmount val nav = navigation() Column( @@ -444,7 +448,7 @@ private fun LoanInfoCard( modifier = Modifier .padding(horizontal = 24.dp) .testTag("amount_paid"), - text = "${amountPaid.format(baseCurrency)} / ${loan.amount.format(baseCurrency)}", + text = "${amountPaid.format(baseCurrency)} / ${loanTotalAmount.format(baseCurrency)}", style = UI.typo.nB1.style( color = contrastColor, fontWeight = FontWeight.ExtraBold @@ -714,9 +718,18 @@ private fun LoanRecordItem( if (loanRecord.note.isNullOrEmpty()) { Spacer(Modifier.height(16.dp)) } - + val transactionType = when (loan.type){ + LoanType.LEND -> { + if(loanRecord.loanRecordType == LoanRecordType.INCREASE) TransactionType.EXPENSE + else TransactionType.INCOME + } + LoanType.BORROW -> { + if(loanRecord.loanRecordType == LoanRecordType.INCREASE) TransactionType.INCOME + else TransactionType.EXPENSE + } + } TypeAmountCurrency( - transactionType = if (loan.type == LoanType.LEND) TransactionType.INCOME else TransactionType.EXPENSE, + transactionType = transactionType, dueDate = null, currency = baseCurrency, amount = loanRecord.amount @@ -843,6 +856,7 @@ private fun Preview_Empty() { ), displayLoanRecords = persistentListOf(), amountPaid = 3821.00, + loanTotalAmount = 4023.54, loanAmountPaid = 100.0, accounts = persistentListOf(), selectedLoanAccount = null, @@ -898,6 +912,7 @@ private fun Preview_Records() { ) ), ), + loanTotalAmount = 4023.54, amountPaid = 3821.00, loanAmountPaid = 100.0, accounts = persistentListOf(), diff --git a/screen/loans/src/main/java/com/ivy/loans/loandetails/LoanDetailsScreenState.kt b/screen/loans/src/main/java/com/ivy/loans/loandetails/LoanDetailsScreenState.kt index 5a4a66b632..d4d051b614 100644 --- a/screen/loans/src/main/java/com/ivy/loans/loandetails/LoanDetailsScreenState.kt +++ b/screen/loans/src/main/java/com/ivy/loans/loandetails/LoanDetailsScreenState.kt @@ -11,6 +11,7 @@ data class LoanDetailsScreenState( val baseCurrency: String, val loan: Loan?, val displayLoanRecords: ImmutableList, + val loanTotalAmount: Double, val amountPaid: Double, val loanAmountPaid: Double, val accounts: ImmutableList, diff --git a/screen/loans/src/main/java/com/ivy/loans/loandetails/LoanDetailsViewModel.kt b/screen/loans/src/main/java/com/ivy/loans/loandetails/LoanDetailsViewModel.kt index 52acfdb640..dbc0a76866 100644 --- a/screen/loans/src/main/java/com/ivy/loans/loandetails/LoanDetailsViewModel.kt +++ b/screen/loans/src/main/java/com/ivy/loans/loandetails/LoanDetailsViewModel.kt @@ -6,6 +6,7 @@ import androidx.compose.runtime.mutableDoubleStateOf import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.viewModelScope import com.ivy.base.legacy.Transaction +import com.ivy.base.model.LoanRecordType import com.ivy.data.db.dao.read.AccountDao import com.ivy.data.db.dao.read.LoanDao import com.ivy.data.db.dao.read.LoanRecordDao @@ -70,6 +71,7 @@ class LoanDetailsViewModel @Inject constructor( private val loan = mutableStateOf(null) private val displayLoanRecords = mutableStateOf>(persistentListOf()) + private val loanTotalAmount = mutableDoubleStateOf(0.0) private val amountPaid = mutableDoubleStateOf(0.0) private val accounts = mutableStateOf>(persistentListOf()) private val loanInterestAmountPaid = mutableDoubleStateOf(0.0) @@ -93,6 +95,7 @@ class LoanDetailsViewModel @Inject constructor( baseCurrency = baseCurrency.value, loan = loan.value, displayLoanRecords = displayLoanRecords.value, + loanTotalAmount = loanTotalAmount.doubleValue, amountPaid = amountPaid.doubleValue, loanAmountPaid = loanInterestAmountPaid.doubleValue, accounts = accounts.value, @@ -218,6 +221,7 @@ class LoanDetailsViewModel @Inject constructor( else -> {} } } + private fun start() { load(loanId = screen.loanId) } @@ -277,6 +281,10 @@ class LoanDetailsViewModel @Inject constructor( var amtPaid = 0.0 var loanInterestAmtPaid = 0.0 displayLoanRecords.value.forEach { + // We do not want to calculate records that increase loan. + if (it.loanRecord.loanRecordType == LoanRecordType.INCREASE) { + return@forEach + } val convertedAmount = it.loanRecord.convertedAmount ?: it.loanRecord.amount if (!it.loanRecord.interest) { amtPaid += convertedAmount @@ -289,6 +297,19 @@ class LoanDetailsViewModel @Inject constructor( loanInterestAmountPaid.doubleValue = loanInterestAmtPaid } + computationThread { + // Calculate total amount of loan borrowed or lent. + // That is initial amount + each record that increased the loan. + var totalAmount = loan.value?.amount ?: 0.0 + displayLoanRecords.value.forEach { + if (it.loanRecord.loanRecordType == LoanRecordType.INCREASE) { + val convertedAmount = it.loanRecord.convertedAmount ?: it.loanRecord.amount + totalAmount += convertedAmount + } + } + loanTotalAmount.doubleValue = totalAmount + } + associatedTransaction = ioThread { transactionDao.findLoanTransaction(loanId = loan.value!!.id)?.toDomain() } diff --git a/temp/legacy-code/src/main/java/com/ivy/legacy/legacy/ui/theme/modal/LoanRecordModal.kt b/temp/legacy-code/src/main/java/com/ivy/legacy/legacy/ui/theme/modal/LoanRecordModal.kt index 2d5d3b1fe3..6d790c4233 100644 --- a/temp/legacy-code/src/main/java/com/ivy/legacy/legacy/ui/theme/modal/LoanRecordModal.kt +++ b/temp/legacy-code/src/main/java/com/ivy/legacy/legacy/ui/theme/modal/LoanRecordModal.kt @@ -1,6 +1,6 @@ package com.ivy.wallet.ui.theme.modal -import android.annotation.SuppressLint +import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.clickable @@ -24,7 +24,6 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight @@ -70,7 +69,6 @@ data class LoanRecordModalData( val createLoanRecordTransaction: Boolean = false, val isLoanInterest: Boolean = false, val id: UUID = UUID.randomUUID(), - val loanRecordType: LoanRecordType? = null ) @Deprecated("Old design system. Use `:ivy-design` and Material3") @@ -114,7 +112,7 @@ fun BoxWithConstraintsScope.LoanRecordModal( mutableStateOf(modal?.loanAccountCurrencyCode != null && modal.loanAccountCurrencyCode != modal.baseCurrency) } var loanRecordType by remember(modal) { - mutableStateOf(modal?.loanRecordType ?: LoanRecordType.INCREASE) + mutableStateOf(modal?.loanRecord?.loanRecordType ?: LoanRecordType.DECREASE) } var amountModalVisible by remember { mutableStateOf(false) } @@ -258,6 +256,7 @@ fun BoxWithConstraintsScope.LoanRecordModal( Spacer(Modifier.height(16.dp)) LoanRecordTypeRow(selectedRecordType = loanRecordType, onLoanRecordTypeChanged = { + if(it == LoanRecordType.INCREASE) loanInterest = false loanRecordType = it }) @@ -273,14 +272,16 @@ fun BoxWithConstraintsScope.LoanRecordModal( createLoanRecordTrans = it } - IvyCheckboxWithText( - modifier = Modifier - .padding(start = 16.dp) - .align(Alignment.Start), - text = stringResource(R.string.mark_as_interest), - checked = loanInterest - ) { - loanInterest = it + AnimatedVisibility(visible = loanRecordType == LoanRecordType.DECREASE ) { + IvyCheckboxWithText( + modifier = Modifier + .padding(start = 16.dp) + .align(Alignment.Start), + text = stringResource(R.string.mark_as_interest), + checked = loanInterest + ) { + loanInterest = it + } } if (reCalculateVisible) { @@ -396,7 +397,8 @@ private fun save( amount = amount, dateTime = dateTime, interest = loanRecordInterest, - accountId = selectedAccount?.id + accountId = selectedAccount?.id, + loanRecordType = loanRecordType ) onEdit( EditLoanRecordData( @@ -434,25 +436,22 @@ private fun LoanRecordTypeRow( verticalAlignment = Alignment.CenterVertically, ) { Spacer(Modifier.width(24.dp)) - LoanRecordType( modifier = Modifier, - loanRecordType = LoanRecordType.INCREASE, + loanRecordType = LoanRecordType.DECREASE, selectedRecordType = selectedRecordType ) { onLoanRecordTypeChanged(it) } - + Spacer(modifier = Modifier.width(8.dp)) LoanRecordType( modifier = Modifier, - loanRecordType = LoanRecordType.DECREASE, + loanRecordType = LoanRecordType.INCREASE, selectedRecordType = selectedRecordType ) { onLoanRecordTypeChanged(it) } } - - Spacer(Modifier.width(24.dp)) } @Composable @@ -462,16 +461,13 @@ private fun LoanRecordType( modifier: Modifier = Modifier, onClick: (LoanRecordType) -> Unit ) { - val iconDrawable = - if (loanRecordType == LoanRecordType.INCREASE) R.drawable.ic_donate_plus - else R.drawable.ic_donate_minus - val text = - if (loanRecordType == LoanRecordType.INCREASE) stringResource(id = R.string.increase_loan) - else stringResource(id = R.string.decrease_loan) + val (text, iconDrawable) = + if (loanRecordType == LoanRecordType.INCREASE) stringResource(id = R.string.increase_loan) to R.drawable.ic_donate_plus + else stringResource(id = R.string.decrease_loan) to R.drawable.ic_donate_minus val selected = selectedRecordType == loanRecordType val medium = UI.colors.medium val rFull = UI.shapes.rFull - val selectedColor = UI.colors.pureInverse + val selectedColor = UI.colors.green1 Row( modifier = modifier .clip(UI.shapes.rFull) @@ -502,10 +498,8 @@ private fun LoanRecordType( fontWeight = FontWeight.ExtraBold ) ) - Spacer(Modifier.width(24.dp)) } - Spacer(Modifier.width(8.dp)) } @Composable From 823d96563f26ed5fae699e33a2b6136cf8ba7662 Mon Sep 17 00:00:00 2001 From: Michal Guspiel Date: Wed, 7 Feb 2024 21:32:37 +0200 Subject: [PATCH 03/12] Feature Implementation - DisplayLoan includes loanTotalAmount - LoanViewModel calculates total amount paid as well as loan total amount. - LoanScreen displays correct information about each loan. --- .../java/com/ivy/loans/loan/LoanViewModel.kt | 37 ++++++++++++------- .../java/com/ivy/loans/loan/LoansScreen.kt | 5 ++- .../com/ivy/loans/loan/data/DisplayLoan.kt | 1 + 3 files changed, 29 insertions(+), 14 deletions(-) diff --git a/screen/loans/src/main/java/com/ivy/loans/loan/LoanViewModel.kt b/screen/loans/src/main/java/com/ivy/loans/loan/LoanViewModel.kt index 6db6f12b7d..ba2b996fc2 100644 --- a/screen/loans/src/main/java/com/ivy/loans/loan/LoanViewModel.kt +++ b/screen/loans/src/main/java/com/ivy/loans/loan/LoanViewModel.kt @@ -5,6 +5,7 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.viewModelScope import com.ivy.base.legacy.SharedPrefs +import com.ivy.base.model.LoanRecordType import com.ivy.data.db.dao.read.LoanRecordDao import com.ivy.data.db.dao.read.SettingsDao import com.ivy.data.db.dao.write.WriteLoanDao @@ -164,22 +165,22 @@ class LoanViewModel @Inject constructor( allLoans = ioThread { loansAct(Unit) .map { loan -> - val amountPaid = calculateAmountPaid(loan) - val loanAmount = loan.amount - val percentPaid = amountPaid / loanAmount + val (amountPaid, loanTotalAmount) = calculateAmountPaidAndTotalAmount(loan) + val percentPaid = amountPaid / loanTotalAmount var currCode = findCurrencyCode(accounts.value, loan.accountId) when (loan.type) { - LoanType.BORROW -> totalOweAmount += (loanAmount - amountPaid) - LoanType.LEND -> totalOwedAmount += (loanAmount - amountPaid) + LoanType.BORROW -> totalOweAmount += (loanTotalAmount - amountPaid) + LoanType.LEND -> totalOwedAmount += (loanTotalAmount - amountPaid) } DisplayLoan( loan = loan, + loanTotalAmount = loanTotalAmount, amountPaid = amountPaid, currencyCode = currCode, formattedDisplayText = "${amountPaid.format(currCode)} $currCode / ${ - loanAmount.format( + loanTotalAmount.format( currCode ) } $currCode (${ @@ -300,18 +301,28 @@ class LoanViewModel @Inject constructor( } ?: defaultCurrencyCode } - private suspend fun calculateAmountPaid(loan: Loan): Double { + /** + * Calculates the total amount paid and the total loan amount including any changes made to the loan. + * @return A Pair containing the total amount paid and the total loan amount. + */ + private suspend fun calculateAmountPaidAndTotalAmount(loan: Loan): Pair { val loanRecords = ioThread { loanRecordDao.findAllByLoanId(loanId = loan.id) } - var amount = 0.0 + var amountPaid = 0.0 + var loanTotalAmount = loan.amount loanRecords.forEach { loanRecord -> - if (!loanRecord.interest) { - val convertedAmount = loanRecord.convertedAmount ?: loanRecord.amount - amount += convertedAmount + if(loanRecord.interest) return@forEach + val convertedAmount = loanRecord.convertedAmount ?: loanRecord.amount + when(loanRecord.loanRecordType){ + LoanRecordType.DECREASE -> { + amountPaid += convertedAmount + } + LoanRecordType.INCREASE -> { + loanTotalAmount += convertedAmount + } } } - - return amount + return amountPaid to loanTotalAmount } private fun updatePaidOffLoanVisibility() { diff --git a/screen/loans/src/main/java/com/ivy/loans/loan/LoansScreen.kt b/screen/loans/src/main/java/com/ivy/loans/loan/LoansScreen.kt index bf92a25cea..a043aefe68 100644 --- a/screen/loans/src/main/java/com/ivy/loans/loan/LoansScreen.kt +++ b/screen/loans/src/main/java/com/ivy/loans/loan/LoansScreen.kt @@ -310,7 +310,7 @@ private fun LoanHeader( Spacer(Modifier.height(4.dp)) - val leftToPay = loan.amount - displayLoan.amountPaid + val leftToPay = displayLoan.loanTotalAmount - displayLoan.amountPaid BalanceRow( modifier = Modifier .align(Alignment.CenterHorizontally), @@ -413,6 +413,7 @@ private fun Preview() { type = LoanType.BORROW, dateTime = LocalDateTime.now() ), + loanTotalAmount = 5500.0, amountPaid = 0.0, percentPaid = 0.4 ), @@ -425,6 +426,7 @@ private fun Preview() { type = LoanType.BORROW, dateTime = LocalDateTime.now() ), + loanTotalAmount = 252.36, amountPaid = 124.23, percentPaid = 0.2 ), @@ -437,6 +439,7 @@ private fun Preview() { type = LoanType.LEND, dateTime = LocalDateTime.now() ), + loanTotalAmount = 7000.0, amountPaid = 8000.0, percentPaid = 0.8 ), diff --git a/screen/loans/src/main/java/com/ivy/loans/loan/data/DisplayLoan.kt b/screen/loans/src/main/java/com/ivy/loans/loan/data/DisplayLoan.kt index 3ea2d7857e..66f16e7d2d 100644 --- a/screen/loans/src/main/java/com/ivy/loans/loan/data/DisplayLoan.kt +++ b/screen/loans/src/main/java/com/ivy/loans/loan/data/DisplayLoan.kt @@ -6,6 +6,7 @@ import com.ivy.wallet.domain.data.Reorderable data class DisplayLoan( val loan: Loan, + val loanTotalAmount: Double, val amountPaid: Double, val currencyCode: String? = getDefaultFIATCurrency().currencyCode, val formattedDisplayText: String = "", From de736505ae42934f50e6bc89b4d804a99aadbc36 Mon Sep 17 00:00:00 2001 From: michalguspiel Date: Thu, 8 Feb 2024 07:49:06 +0200 Subject: [PATCH 04/12] CI pipeline fix - Tests were failing due to the missing field in backup test. Now by default `loanRecordType` in `LoanRecordEntity` has DECRESE value. This fixes the issue with the backup, and is the simplest fix. This makes sense because before this pull request all loan records were implicitly of type DECREASE. --- .../src/main/java/com/ivy/data/db/entity/LoanRecordEntity.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/shared/data/src/main/java/com/ivy/data/db/entity/LoanRecordEntity.kt b/shared/data/src/main/java/com/ivy/data/db/entity/LoanRecordEntity.kt index b01fd52c15..6142f41756 100644 --- a/shared/data/src/main/java/com/ivy/data/db/entity/LoanRecordEntity.kt +++ b/shared/data/src/main/java/com/ivy/data/db/entity/LoanRecordEntity.kt @@ -33,8 +33,10 @@ data class LoanRecordEntity( // This is used store the converted amount for currencies which are different from the loan account currency @SerialName("convertedAmount") val convertedAmount: Double? = null, + // In order to keep backups valid, loanRecordType is by default DECREASE. + // This is because before issue 2740 all records were of this type implicitly. @SerialName("loanRecordType") - val loanRecordType: LoanRecordType, + val loanRecordType: LoanRecordType = LoanRecordType.DECREASE, @SerialName("isSynced") val isSynced: Boolean = false, From 6edcf3d13012cba610f9fcae78bdc66f1047d4fe Mon Sep 17 00:00:00 2001 From: michalguspiel Date: Thu, 8 Feb 2024 08:12:50 +0200 Subject: [PATCH 05/12] Fix detekt errors. --- .../java/com/ivy/loans/loan/LoanViewModel.kt | 6 ++-- .../loans/loandetails/LoanDetailsScreen.kt | 32 ++++++++++++------- .../com/ivy/data/db/RoomTypeConverters.kt | 5 +-- .../legacy/ui/theme/modal/LoanRecordModal.kt | 11 ++++--- 4 files changed, 34 insertions(+), 20 deletions(-) diff --git a/screen/loans/src/main/java/com/ivy/loans/loan/LoanViewModel.kt b/screen/loans/src/main/java/com/ivy/loans/loan/LoanViewModel.kt index ba2b996fc2..c63103f7d7 100644 --- a/screen/loans/src/main/java/com/ivy/loans/loan/LoanViewModel.kt +++ b/screen/loans/src/main/java/com/ivy/loans/loan/LoanViewModel.kt @@ -305,15 +305,15 @@ class LoanViewModel @Inject constructor( * Calculates the total amount paid and the total loan amount including any changes made to the loan. * @return A Pair containing the total amount paid and the total loan amount. */ - private suspend fun calculateAmountPaidAndTotalAmount(loan: Loan): Pair { + private suspend fun calculateAmountPaidAndTotalAmount(loan: Loan): Pair { val loanRecords = ioThread { loanRecordDao.findAllByLoanId(loanId = loan.id) } var amountPaid = 0.0 var loanTotalAmount = loan.amount loanRecords.forEach { loanRecord -> - if(loanRecord.interest) return@forEach + if (loanRecord.interest) return@forEach val convertedAmount = loanRecord.convertedAmount ?: loanRecord.amount - when(loanRecord.loanRecordType){ + when (loanRecord.loanRecordType) { LoanRecordType.DECREASE -> { amountPaid += convertedAmount } diff --git a/screen/loans/src/main/java/com/ivy/loans/loandetails/LoanDetailsScreen.kt b/screen/loans/src/main/java/com/ivy/loans/loandetails/LoanDetailsScreen.kt index 164a4f97ae..efc61afc71 100644 --- a/screen/loans/src/main/java/com/ivy/loans/loandetails/LoanDetailsScreen.kt +++ b/screen/loans/src/main/java/com/ivy/loans/loandetails/LoanDetailsScreen.kt @@ -172,7 +172,8 @@ private fun BoxWithConstraintsScope.UI( displayLoanRecord ) ) - }) + } + ) item { InitialRecordItem( loan = state.loan, @@ -718,14 +719,21 @@ private fun LoanRecordItem( if (loanRecord.note.isNullOrEmpty()) { Spacer(Modifier.height(16.dp)) } - val transactionType = when (loan.type){ + val transactionType = when (loan.type) { LoanType.LEND -> { - if(loanRecord.loanRecordType == LoanRecordType.INCREASE) TransactionType.EXPENSE - else TransactionType.INCOME + if (loanRecord.loanRecordType == LoanRecordType.INCREASE) { + TransactionType.EXPENSE + } else { + TransactionType.INCOME + } } + LoanType.BORROW -> { - if(loanRecord.loanRecordType == LoanRecordType.INCREASE) TransactionType.INCOME - else TransactionType.EXPENSE + if (loanRecord.loanRecordType == LoanRecordType.INCREASE) { + TransactionType.INCOME + } else { + TransactionType.EXPENSE + } } } TypeAmountCurrency( @@ -764,7 +772,6 @@ private fun InitialRecordItem( .background(UI.colors.medium, UI.shapes.r4) .testTag("loan_record_item") ) { - IvyButton( modifier = Modifier.padding(16.dp), backgroundGradient = Gradient.solid(UI.colors.pure), @@ -775,18 +782,21 @@ private fun InitialRecordItem( defaultIcon = R.drawable.ic_custom_loan_s ), textStyle = UI.typo.c.style( - color = UI.colors.pureInverse, fontWeight = FontWeight.ExtraBold + color = UI.colors.pureInverse, + fontWeight = FontWeight.ExtraBold ), padding = 8.dp, ) {} - - loan.dateTime?.formatNicelyWithTime(noWeekDay = false)?.let { nicelyFormattedDate -> + loan.dateTime?.formatNicelyWithTime( + noWeekDay = false + )?.let { nicelyFormattedDate -> Text( modifier = Modifier.padding(horizontal = 24.dp), text = nicelyFormattedDate.uppercase(), style = UI.typo.nC.style( - color = Gray, fontWeight = FontWeight.Bold + color = Gray, + fontWeight = FontWeight.Bold ) ) } diff --git a/shared/data/src/main/java/com/ivy/data/db/RoomTypeConverters.kt b/shared/data/src/main/java/com/ivy/data/db/RoomTypeConverters.kt index 9fb335b9a7..3433998deb 100644 --- a/shared/data/src/main/java/com/ivy/data/db/RoomTypeConverters.kt +++ b/shared/data/src/main/java/com/ivy/data/db/RoomTypeConverters.kt @@ -58,8 +58,9 @@ class RoomTypeConverters { fun parseInstant(value: Long): Instant = Instant.ofEpochMilli(value) @TypeConverter - fun saveLoanRecordType(value: LoanRecordType?) = value?.name + fun saveLoanRecordType(value: LoanRecordType?): String? = value?.name @TypeConverter - fun parseLoanRecordType(value: String?) = value?.let { LoanRecordType.valueOf(it) } + fun parseLoanRecordType(value: String?): LoanRecordType? = + value?.let { LoanRecordType.valueOf(it) } } diff --git a/temp/legacy-code/src/main/java/com/ivy/legacy/legacy/ui/theme/modal/LoanRecordModal.kt b/temp/legacy-code/src/main/java/com/ivy/legacy/legacy/ui/theme/modal/LoanRecordModal.kt index 6d790c4233..88e5470f90 100644 --- a/temp/legacy-code/src/main/java/com/ivy/legacy/legacy/ui/theme/modal/LoanRecordModal.kt +++ b/temp/legacy-code/src/main/java/com/ivy/legacy/legacy/ui/theme/modal/LoanRecordModal.kt @@ -256,7 +256,7 @@ fun BoxWithConstraintsScope.LoanRecordModal( Spacer(Modifier.height(16.dp)) LoanRecordTypeRow(selectedRecordType = loanRecordType, onLoanRecordTypeChanged = { - if(it == LoanRecordType.INCREASE) loanInterest = false + if (it == LoanRecordType.INCREASE) loanInterest = false loanRecordType = it }) @@ -272,7 +272,7 @@ fun BoxWithConstraintsScope.LoanRecordModal( createLoanRecordTrans = it } - AnimatedVisibility(visible = loanRecordType == LoanRecordType.DECREASE ) { + AnimatedVisibility(visible = loanRecordType == LoanRecordType.DECREASE) { IvyCheckboxWithText( modifier = Modifier .padding(start = 16.dp) @@ -462,8 +462,11 @@ private fun LoanRecordType( onClick: (LoanRecordType) -> Unit ) { val (text, iconDrawable) = - if (loanRecordType == LoanRecordType.INCREASE) stringResource(id = R.string.increase_loan) to R.drawable.ic_donate_plus - else stringResource(id = R.string.decrease_loan) to R.drawable.ic_donate_minus + if (loanRecordType == LoanRecordType.INCREASE) { + stringResource(id = R.string.increase_loan) to R.drawable.ic_donate_plus + } else { + stringResource(id = R.string.decrease_loan) to R.drawable.ic_donate_minus + } val selected = selectedRecordType == loanRecordType val medium = UI.colors.medium val rFull = UI.shapes.rFull From d437fe141acffe158065e67fe6573ca0c83c52bf Mon Sep 17 00:00:00 2001 From: michalguspiel Date: Thu, 8 Feb 2024 08:19:47 +0200 Subject: [PATCH 06/12] Suppress detekt. Suppressed detekt errors that were forcing this pull request to make changes unrelated to scope of this issue or keeping this pull request inconsistent with the rest of the codebase. - Suppressed LongMethod for LoanInfoCard - Suppressed DataClassDefaultValues for LoanRecordEntity, since there is a few default values already, and `LoanRecordEntity.loanRecordType` default value is the easiest fix for the backup problem. - Suppressed MagicNumber and ClassNaming for Migration class to keep it consistent with the rest of migration classes. --- .../src/main/java/com/ivy/loans/loandetails/LoanDetailsScreen.kt | 1 + .../src/main/java/com/ivy/data/db/entity/LoanRecordEntity.kt | 1 + .../ivy/data/db/migration/Migration126to127_LoanRecordType.kt | 1 + 3 files changed, 3 insertions(+) diff --git a/screen/loans/src/main/java/com/ivy/loans/loandetails/LoanDetailsScreen.kt b/screen/loans/src/main/java/com/ivy/loans/loandetails/LoanDetailsScreen.kt index efc61afc71..1bf4f4c7d0 100644 --- a/screen/loans/src/main/java/com/ivy/loans/loandetails/LoanDetailsScreen.kt +++ b/screen/loans/src/main/java/com/ivy/loans/loandetails/LoanDetailsScreen.kt @@ -367,6 +367,7 @@ private fun LoanItem( } } +@Suppress("LongMethod") @Composable private fun LoanInfoCard( loan: Loan, diff --git a/shared/data/src/main/java/com/ivy/data/db/entity/LoanRecordEntity.kt b/shared/data/src/main/java/com/ivy/data/db/entity/LoanRecordEntity.kt index 6142f41756..fce9b71d94 100644 --- a/shared/data/src/main/java/com/ivy/data/db/entity/LoanRecordEntity.kt +++ b/shared/data/src/main/java/com/ivy/data/db/entity/LoanRecordEntity.kt @@ -11,6 +11,7 @@ import kotlinx.serialization.Serializable import java.time.LocalDateTime import java.util.* +@Suppress("DataClassDefaultValues") @Keep @Serializable @Entity(tableName = "loan_records") diff --git a/shared/data/src/main/java/com/ivy/data/db/migration/Migration126to127_LoanRecordType.kt b/shared/data/src/main/java/com/ivy/data/db/migration/Migration126to127_LoanRecordType.kt index 050e17f74d..a00e0db1d2 100644 --- a/shared/data/src/main/java/com/ivy/data/db/migration/Migration126to127_LoanRecordType.kt +++ b/shared/data/src/main/java/com/ivy/data/db/migration/Migration126to127_LoanRecordType.kt @@ -3,6 +3,7 @@ package com.ivy.data.db.migration import androidx.room.migration.Migration import androidx.sqlite.db.SupportSQLiteDatabase +@Suppress("MagicNumber", "ClassNaming") class Migration126to127_LoanRecordType : Migration(126, 127) { override fun migrate(db: SupportSQLiteDatabase) { db.execSQL("ALTER TABLE loan_records ADD COLUMN loanRecordType TEXT NOT NULL DEFAULT 'DECREASE'") From d71652321489f2b459d53bd3a9c51d5d570c9298 Mon Sep 17 00:00:00 2001 From: michalguspiel Date: Thu, 8 Feb 2024 08:31:02 +0200 Subject: [PATCH 07/12] Fix Lint issue. --- .../main/java/com/ivy/loans/loandetails/LoanDetailsScreen.kt | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/screen/loans/src/main/java/com/ivy/loans/loandetails/LoanDetailsScreen.kt b/screen/loans/src/main/java/com/ivy/loans/loandetails/LoanDetailsScreen.kt index 1bf4f4c7d0..3f179d4127 100644 --- a/screen/loans/src/main/java/com/ivy/loans/loandetails/LoanDetailsScreen.kt +++ b/screen/loans/src/main/java/com/ivy/loans/loandetails/LoanDetailsScreen.kt @@ -245,13 +245,12 @@ private fun Header( baseCurrency: String, loanTotalAmount: Double, amountPaid: Double, - loanAmountPaid: Double = 0.0, itemColor: Color, - selectedLoanAccount: Account? = null, - onAmountClick: () -> Unit, onEditLoan: () -> Unit, onDeleteLoan: () -> Unit, + loanAmountPaid: Double = 0.0, + selectedLoanAccount: Account? = null, onAddRecord: () -> Unit ) { val contrastColor = findContrastTextColor(itemColor) From 5a6cb14e2beceb46b36b8e908775c7283c9b0b7c Mon Sep 17 00:00:00 2001 From: michalguspiel Date: Thu, 8 Feb 2024 18:00:48 +0200 Subject: [PATCH 08/12] Resolved part of the review requests. --- .../java/com/ivy/loans/loan/LoanViewModel.kt | 29 +- .../loans/loandetails/LoanDetailsScreen.kt | 19 +- .../loans/loandetails/LoanDetailsViewModel.kt | 15 +- .../java/com/ivy/base/model/LoanRecordType.kt | 7 + .../com.ivy.data.db.IvyRoomDatabase/126.json | 12 +- .../com.ivy.data.db.IvyRoomDatabase/127.json | 832 ++++++++++++++++++ 6 files changed, 874 insertions(+), 40 deletions(-) create mode 100644 shared/data/schemas/com.ivy.data.db.IvyRoomDatabase/127.json diff --git a/screen/loans/src/main/java/com/ivy/loans/loan/LoanViewModel.kt b/screen/loans/src/main/java/com/ivy/loans/loan/LoanViewModel.kt index c63103f7d7..f3753cacfe 100644 --- a/screen/loans/src/main/java/com/ivy/loans/loan/LoanViewModel.kt +++ b/screen/loans/src/main/java/com/ivy/loans/loan/LoanViewModel.kt @@ -5,7 +5,7 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.viewModelScope import com.ivy.base.legacy.SharedPrefs -import com.ivy.base.model.LoanRecordType +import com.ivy.base.model.processByType import com.ivy.data.db.dao.read.LoanRecordDao import com.ivy.data.db.dao.read.SettingsDao import com.ivy.data.db.dao.write.WriteLoanDao @@ -166,7 +166,11 @@ class LoanViewModel @Inject constructor( loansAct(Unit) .map { loan -> val (amountPaid, loanTotalAmount) = calculateAmountPaidAndTotalAmount(loan) - val percentPaid = amountPaid / loanTotalAmount + val percentPaid = if (loanTotalAmount != 0.0) { + amountPaid / loanTotalAmount + } else { + 0.0 + } var currCode = findCurrencyCode(accounts.value, loan.accountId) when (loan.type) { @@ -307,20 +311,15 @@ class LoanViewModel @Inject constructor( */ private suspend fun calculateAmountPaidAndTotalAmount(loan: Loan): Pair { val loanRecords = ioThread { loanRecordDao.findAllByLoanId(loanId = loan.id) } - var amountPaid = 0.0 - var loanTotalAmount = loan.amount - - loanRecords.forEach { loanRecord -> - if (loanRecord.interest) return@forEach + val (amountPaid, loanTotalAmount) = loanRecords.fold(0.0 to loan.amount) { value, loanRecord -> + val (currentAmountPaid, currentLoanTotalAmount) = value + if (loanRecord.interest) return@fold value val convertedAmount = loanRecord.convertedAmount ?: loanRecord.amount - when (loanRecord.loanRecordType) { - LoanRecordType.DECREASE -> { - amountPaid += convertedAmount - } - LoanRecordType.INCREASE -> { - loanTotalAmount += convertedAmount - } - } + + loanRecord.loanRecordType.processByType( + decreaseAction = { currentAmountPaid + convertedAmount to currentLoanTotalAmount }, + increaseAction = { currentAmountPaid to currentLoanTotalAmount + convertedAmount } + ) } return amountPaid to loanTotalAmount } diff --git a/screen/loans/src/main/java/com/ivy/loans/loandetails/LoanDetailsScreen.kt b/screen/loans/src/main/java/com/ivy/loans/loandetails/LoanDetailsScreen.kt index 3f179d4127..59d84c8fa1 100644 --- a/screen/loans/src/main/java/com/ivy/loans/loandetails/LoanDetailsScreen.kt +++ b/screen/loans/src/main/java/com/ivy/loans/loandetails/LoanDetailsScreen.kt @@ -35,6 +35,7 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import com.ivy.base.model.LoanRecordType import com.ivy.base.model.TransactionType +import com.ivy.base.model.processByType import com.ivy.data.model.LoanType import com.ivy.design.l0_system.UI import com.ivy.design.l0_system.style @@ -721,19 +722,17 @@ private fun LoanRecordItem( } val transactionType = when (loan.type) { LoanType.LEND -> { - if (loanRecord.loanRecordType == LoanRecordType.INCREASE) { - TransactionType.EXPENSE - } else { - TransactionType.INCOME - } + loanRecord.loanRecordType.processByType( + increaseAction = { TransactionType.EXPENSE }, + decreaseAction = { TransactionType.INCOME } + ) } LoanType.BORROW -> { - if (loanRecord.loanRecordType == LoanRecordType.INCREASE) { - TransactionType.INCOME - } else { - TransactionType.EXPENSE - } + loanRecord.loanRecordType.processByType( + increaseAction = { TransactionType.INCOME }, + decreaseAction = { TransactionType.EXPENSE } + ) } } TypeAmountCurrency( diff --git a/screen/loans/src/main/java/com/ivy/loans/loandetails/LoanDetailsViewModel.kt b/screen/loans/src/main/java/com/ivy/loans/loandetails/LoanDetailsViewModel.kt index dbc0a76866..57d7a5caf0 100644 --- a/screen/loans/src/main/java/com/ivy/loans/loandetails/LoanDetailsViewModel.kt +++ b/screen/loans/src/main/java/com/ivy/loans/loandetails/LoanDetailsViewModel.kt @@ -300,13 +300,16 @@ class LoanDetailsViewModel @Inject constructor( computationThread { // Calculate total amount of loan borrowed or lent. // That is initial amount + each record that increased the loan. - var totalAmount = loan.value?.amount ?: 0.0 - displayLoanRecords.value.forEach { - if (it.loanRecord.loanRecordType == LoanRecordType.INCREASE) { - val convertedAmount = it.loanRecord.convertedAmount ?: it.loanRecord.amount - totalAmount += convertedAmount + val totalAmount = + displayLoanRecords.value.fold(loan.value?.amount ?: 0.0) { value, record -> + if (record.loanRecord.loanRecordType == LoanRecordType.INCREASE) { + val convertedAmount = + record.loanRecord.convertedAmount ?: record.loanRecord.amount + value + convertedAmount + } else { + value + } } - } loanTotalAmount.doubleValue = totalAmount } diff --git a/shared/base/src/main/java/com/ivy/base/model/LoanRecordType.kt b/shared/base/src/main/java/com/ivy/base/model/LoanRecordType.kt index e58a85f0e8..009a5497e1 100644 --- a/shared/base/src/main/java/com/ivy/base/model/LoanRecordType.kt +++ b/shared/base/src/main/java/com/ivy/base/model/LoanRecordType.kt @@ -10,3 +10,10 @@ import kotlinx.serialization.Serializable enum class LoanRecordType { INCREASE, DECREASE } + +funLoanRecordType.processByType(decreaseAction : () -> T, increaseAction : () -> T) : T{ + return when(this){ + LoanRecordType.DECREASE -> decreaseAction() + LoanRecordType.INCREASE -> increaseAction() + } +} \ No newline at end of file diff --git a/shared/data/schemas/com.ivy.data.db.IvyRoomDatabase/126.json b/shared/data/schemas/com.ivy.data.db.IvyRoomDatabase/126.json index d4af7b66c4..38a879872c 100644 --- a/shared/data/schemas/com.ivy.data.db.IvyRoomDatabase/126.json +++ b/shared/data/schemas/com.ivy.data.db.IvyRoomDatabase/126.json @@ -2,7 +2,7 @@ "formatVersion": 1, "database": { "version": 126, - "identityHash": "3e221e19eb3210bba162a6a86341c440", + "identityHash": "712907ba120b6227af4b79fca7ff41db", "entities": [ { "tableName": "accounts", @@ -637,7 +637,7 @@ }, { "tableName": "loan_records", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`loanId` TEXT NOT NULL, `amount` REAL NOT NULL, `note` TEXT, `dateTime` INTEGER NOT NULL, `interest` INTEGER NOT NULL, `accountId` TEXT, `convertedAmount` REAL, `loanRecordType` TEXT, `isSynced` INTEGER NOT NULL, `isDeleted` INTEGER NOT NULL, `id` TEXT NOT NULL, PRIMARY KEY(`id`))", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`loanId` TEXT NOT NULL, `amount` REAL NOT NULL, `note` TEXT, `dateTime` INTEGER NOT NULL, `interest` INTEGER NOT NULL, `accountId` TEXT, `convertedAmount` REAL, `isSynced` INTEGER NOT NULL, `isDeleted` INTEGER NOT NULL, `id` TEXT NOT NULL, PRIMARY KEY(`id`))", "fields": [ { "fieldPath": "loanId", @@ -681,12 +681,6 @@ "affinity": "REAL", "notNull": false }, - { - "fieldPath": "loanRecordType", - "columnName": "loanRecordType", - "affinity": "TEXT", - "notNull": false - }, { "fieldPath": "isSynced", "columnName": "isSynced", @@ -826,7 +820,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, '3e221e19eb3210bba162a6a86341c440')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '712907ba120b6227af4b79fca7ff41db')" ] } } \ No newline at end of file diff --git a/shared/data/schemas/com.ivy.data.db.IvyRoomDatabase/127.json b/shared/data/schemas/com.ivy.data.db.IvyRoomDatabase/127.json new file mode 100644 index 0000000000..6f38aa92cf --- /dev/null +++ b/shared/data/schemas/com.ivy.data.db.IvyRoomDatabase/127.json @@ -0,0 +1,832 @@ +{ + "formatVersion": 1, + "database": { + "version": 127, + "identityHash": "72a6dbd3363d8c5f8c99572d8573c0cd", + "entities": [ + { + "tableName": "accounts", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `currency` TEXT, `color` INTEGER NOT NULL, `icon` TEXT, `orderNum` REAL NOT NULL, `includeInBalance` INTEGER NOT NULL, `isSynced` INTEGER NOT NULL, `isDeleted` INTEGER NOT NULL, `id` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "currency", + "columnName": "currency", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "color", + "columnName": "color", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "icon", + "columnName": "icon", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "orderNum", + "columnName": "orderNum", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "includeInBalance", + "columnName": "includeInBalance", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isSynced", + "columnName": "isSynced", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isDeleted", + "columnName": "isDeleted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "transactions", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` TEXT NOT NULL, `type` TEXT NOT NULL, `amount` REAL NOT NULL, `toAccountId` TEXT, `toAmount` REAL, `title` TEXT, `description` TEXT, `dateTime` INTEGER, `categoryId` TEXT, `dueDate` INTEGER, `recurringRuleId` TEXT, `attachmentUrl` TEXT, `loanId` TEXT, `loanRecordId` TEXT, `isSynced` INTEGER NOT NULL, `isDeleted` INTEGER NOT NULL, `id` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "amount", + "columnName": "amount", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "toAccountId", + "columnName": "toAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "toAmount", + "columnName": "toAmount", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "dateTime", + "columnName": "dateTime", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "categoryId", + "columnName": "categoryId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "dueDate", + "columnName": "dueDate", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "recurringRuleId", + "columnName": "recurringRuleId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "attachmentUrl", + "columnName": "attachmentUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loanId", + "columnName": "loanId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loanRecordId", + "columnName": "loanRecordId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isSynced", + "columnName": "isSynced", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isDeleted", + "columnName": "isDeleted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "categories", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `color` INTEGER NOT NULL, `icon` TEXT, `orderNum` REAL NOT NULL, `isSynced` INTEGER NOT NULL, `isDeleted` INTEGER NOT NULL, `id` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "color", + "columnName": "color", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "icon", + "columnName": "icon", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "orderNum", + "columnName": "orderNum", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "isSynced", + "columnName": "isSynced", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isDeleted", + "columnName": "isDeleted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "settings", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`theme` TEXT NOT NULL, `currency` TEXT NOT NULL, `bufferAmount` REAL NOT NULL, `name` TEXT NOT NULL, `isSynced` INTEGER NOT NULL, `isDeleted` INTEGER NOT NULL, `id` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "theme", + "columnName": "theme", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "currency", + "columnName": "currency", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bufferAmount", + "columnName": "bufferAmount", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isSynced", + "columnName": "isSynced", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isDeleted", + "columnName": "isDeleted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "planned_payment_rules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`startDate` INTEGER, `intervalN` INTEGER, `intervalType` TEXT, `oneTime` INTEGER NOT NULL, `type` TEXT NOT NULL, `accountId` TEXT NOT NULL, `amount` REAL NOT NULL, `categoryId` TEXT, `title` TEXT, `description` TEXT, `isSynced` INTEGER NOT NULL, `isDeleted` INTEGER NOT NULL, `id` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "startDate", + "columnName": "startDate", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "intervalN", + "columnName": "intervalN", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "intervalType", + "columnName": "intervalType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "oneTime", + "columnName": "oneTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "amount", + "columnName": "amount", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "categoryId", + "columnName": "categoryId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isSynced", + "columnName": "isSynced", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isDeleted", + "columnName": "isDeleted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "users", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`email` TEXT NOT NULL, `authProviderType` TEXT NOT NULL, `firstName` TEXT NOT NULL, `lastName` TEXT, `profilePicture` TEXT, `color` INTEGER NOT NULL, `testUser` INTEGER NOT NULL, `id` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "authProviderType", + "columnName": "authProviderType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "firstName", + "columnName": "firstName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastName", + "columnName": "lastName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "profilePicture", + "columnName": "profilePicture", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "color", + "columnName": "color", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "testUser", + "columnName": "testUser", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "exchange_rates", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`baseCurrency` TEXT NOT NULL, `currency` TEXT NOT NULL, `rate` REAL NOT NULL, `manualOverride` INTEGER NOT NULL, PRIMARY KEY(`baseCurrency`, `currency`))", + "fields": [ + { + "fieldPath": "baseCurrency", + "columnName": "baseCurrency", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "currency", + "columnName": "currency", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "rate", + "columnName": "rate", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "manualOverride", + "columnName": "manualOverride", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "baseCurrency", + "currency" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "budgets", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `amount` REAL NOT NULL, `categoryIdsSerialized` TEXT, `accountIdsSerialized` TEXT, `isSynced` INTEGER NOT NULL, `isDeleted` INTEGER NOT NULL, `orderId` REAL NOT NULL, `id` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "amount", + "columnName": "amount", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "categoryIdsSerialized", + "columnName": "categoryIdsSerialized", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "accountIdsSerialized", + "columnName": "accountIdsSerialized", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isSynced", + "columnName": "isSynced", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isDeleted", + "columnName": "isDeleted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "orderId", + "columnName": "orderId", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "loans", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `amount` REAL NOT NULL, `type` TEXT NOT NULL, `color` INTEGER NOT NULL, `icon` TEXT, `orderNum` REAL NOT NULL, `accountId` TEXT, `isSynced` INTEGER NOT NULL, `isDeleted` INTEGER NOT NULL, `dateTime` INTEGER, `id` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "amount", + "columnName": "amount", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "color", + "columnName": "color", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "icon", + "columnName": "icon", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "orderNum", + "columnName": "orderNum", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isSynced", + "columnName": "isSynced", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isDeleted", + "columnName": "isDeleted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "dateTime", + "columnName": "dateTime", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "loan_records", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`loanId` TEXT NOT NULL, `amount` REAL NOT NULL, `note` TEXT, `dateTime` INTEGER NOT NULL, `interest` INTEGER NOT NULL, `accountId` TEXT, `convertedAmount` REAL, `loanRecordType` TEXT NOT NULL, `isSynced` INTEGER NOT NULL, `isDeleted` INTEGER NOT NULL, `id` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "loanId", + "columnName": "loanId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "amount", + "columnName": "amount", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "note", + "columnName": "note", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "dateTime", + "columnName": "dateTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "interest", + "columnName": "interest", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "convertedAmount", + "columnName": "convertedAmount", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "loanRecordType", + "columnName": "loanRecordType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isSynced", + "columnName": "isSynced", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isDeleted", + "columnName": "isDeleted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "tags", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `description` TEXT, `color` INTEGER NOT NULL, `icon` TEXT, `orderNum` REAL NOT NULL, `isDeleted` INTEGER NOT NULL, `dateTime` INTEGER NOT NULL, `lastSyncedTime` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "color", + "columnName": "color", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "icon", + "columnName": "icon", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "orderNum", + "columnName": "orderNum", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "isDeleted", + "columnName": "isDeleted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "dateTime", + "columnName": "dateTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastSyncedTime", + "columnName": "lastSyncedTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "tags_association", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`tagId` TEXT NOT NULL, `associatedId` TEXT NOT NULL, `lastSyncedTime` INTEGER NOT NULL, `isDeleted` INTEGER NOT NULL, PRIMARY KEY(`tagId`, `associatedId`))", + "fields": [ + { + "fieldPath": "tagId", + "columnName": "tagId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "associatedId", + "columnName": "associatedId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastSyncedTime", + "columnName": "lastSyncedTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isDeleted", + "columnName": "isDeleted", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "tagId", + "associatedId" + ] + }, + "indices": [], + "foreignKeys": [] + } + ], + "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, '72a6dbd3363d8c5f8c99572d8573c0cd')" + ] + } +} \ No newline at end of file From 868890c1994a3a677ec4f18d0890e8ad7510212a Mon Sep 17 00:00:00 2001 From: michalguspiel Date: Thu, 8 Feb 2024 18:06:53 +0200 Subject: [PATCH 09/12] Fixed detekt. --- .../loans/src/main/java/com/ivy/loans/loan/LoanViewModel.kt | 2 +- .../base/src/main/java/com/ivy/base/model/LoanRecordType.kt | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/screen/loans/src/main/java/com/ivy/loans/loan/LoanViewModel.kt b/screen/loans/src/main/java/com/ivy/loans/loan/LoanViewModel.kt index f3753cacfe..ce35787af0 100644 --- a/screen/loans/src/main/java/com/ivy/loans/loan/LoanViewModel.kt +++ b/screen/loans/src/main/java/com/ivy/loans/loan/LoanViewModel.kt @@ -317,7 +317,7 @@ class LoanViewModel @Inject constructor( val convertedAmount = loanRecord.convertedAmount ?: loanRecord.amount loanRecord.loanRecordType.processByType( - decreaseAction = { currentAmountPaid + convertedAmount to currentLoanTotalAmount }, + decreaseAction = { currentAmountPaid + convertedAmount to currentLoanTotalAmount }, increaseAction = { currentAmountPaid to currentLoanTotalAmount + convertedAmount } ) } diff --git a/shared/base/src/main/java/com/ivy/base/model/LoanRecordType.kt b/shared/base/src/main/java/com/ivy/base/model/LoanRecordType.kt index 009a5497e1..0289ecafde 100644 --- a/shared/base/src/main/java/com/ivy/base/model/LoanRecordType.kt +++ b/shared/base/src/main/java/com/ivy/base/model/LoanRecordType.kt @@ -11,8 +11,8 @@ enum class LoanRecordType { INCREASE, DECREASE } -funLoanRecordType.processByType(decreaseAction : () -> T, increaseAction : () -> T) : T{ - return when(this){ +fun LoanRecordType.processByType(decreaseAction: () -> T, increaseAction: () -> T): T { + return when (this) { LoanRecordType.DECREASE -> decreaseAction() LoanRecordType.INCREASE -> increaseAction() } From 4b38b8f773cc1a9b545f3f16a9a6ad2493ebdb44 Mon Sep 17 00:00:00 2001 From: michalguspiel Date: Thu, 8 Feb 2024 18:39:15 +0200 Subject: [PATCH 10/12] DB migration test --- .../data/db/IvyRoomDatabaseMigrationTest.kt | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/shared/data/src/androidTest/java/com/ivy/data/db/IvyRoomDatabaseMigrationTest.kt b/shared/data/src/androidTest/java/com/ivy/data/db/IvyRoomDatabaseMigrationTest.kt index fb8ee97d2e..f4bf407311 100644 --- a/shared/data/src/androidTest/java/com/ivy/data/db/IvyRoomDatabaseMigrationTest.kt +++ b/shared/data/src/androidTest/java/com/ivy/data/db/IvyRoomDatabaseMigrationTest.kt @@ -7,6 +7,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry import com.ivy.data.db.migration.Migration123to124_LoanIncludeDateTime import com.ivy.data.db.migration.Migration124to125_LoanEditDateTime +import com.ivy.data.db.migration.Migration126to127_LoanRecordType import com.ivy.data.model.LoanType import io.kotest.matchers.shouldBe import org.junit.Rule @@ -78,6 +79,60 @@ class IvyRoomDatabaseMigrationTest { newDb.close() } + @Test + fun migrate126to127_LoanRecordType() { + // given + val loanId = java.util.UUID.randomUUID().toString() + val noteString = "here is your note" + helper.createDatabase(TestDb, 126).apply { + // Database has schema version 1. Insert some data using SQL queries. + // You can't use DAO classes because they expect the latest schema. + val insertSql = """ + INSERT INTO loan_records (loanId, amount, note, dateTime, interest, accountId, convertedAmount, isSynced, isDeleted, id) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?); + """.trimIndent() + // Assuming you have an instance of LoanRecordEntity named loanRecordEntity + val preparedStatement = compileStatement(insertSql).apply { + // Bind the values from your LoanRecordEntity instance to the prepared statement + bindString(1, loanId) + bindDouble(2, 123.50) + bindString(3, noteString) + bindString(4, "this will fail, LocalDateTimeNeeded") + bindLong(5, 0) // interest + bindString(6, UUID.randomUUID().toString()) + bindDouble(7, 3.14) // convertedAmount + bindLong(8, 1) + bindLong(9, 0) + bindString(10, UUID.randomUUID().toString()) + } + preparedStatement.executeInsert() + close() + } + + // when + helper.runMigrationsAndValidate( + TestDb, + 126, + true, + Migration126to127_LoanRecordType() + ) + val newDb = helper.runMigrationsAndValidate( + TestDb, + 127, + true, + Migration126to127_LoanRecordType() + ) + + // then + newDb.query("SELECT * FROM loan_records").apply { + moveToFirst() shouldBe true + getString(0) shouldBe loanId + getDouble(1) shouldBe 123.50 + getString(2) shouldBe noteString + } + newDb.close() + } + @Test fun migrateAll() { // given: From 2455aa1c6f80966cf6df183f3a8d016be15399df Mon Sep 17 00:00:00 2001 From: michalguspiel Date: Thu, 8 Feb 2024 18:59:45 +0200 Subject: [PATCH 11/12] Add check for LoanRecordType --- .../java/com/ivy/data/db/IvyRoomDatabaseMigrationTest.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/shared/data/src/androidTest/java/com/ivy/data/db/IvyRoomDatabaseMigrationTest.kt b/shared/data/src/androidTest/java/com/ivy/data/db/IvyRoomDatabaseMigrationTest.kt index f4bf407311..cfe9b24bb6 100644 --- a/shared/data/src/androidTest/java/com/ivy/data/db/IvyRoomDatabaseMigrationTest.kt +++ b/shared/data/src/androidTest/java/com/ivy/data/db/IvyRoomDatabaseMigrationTest.kt @@ -129,6 +129,7 @@ class IvyRoomDatabaseMigrationTest { getString(0) shouldBe loanId getDouble(1) shouldBe 123.50 getString(2) shouldBe noteString + getString(7) shouldBe "DECREASE" } newDb.close() } From cc38e2d19abccba6e246045aebbcdc263ff5028c Mon Sep 17 00:00:00 2001 From: michalguspiel Date: Thu, 8 Feb 2024 19:06:37 +0200 Subject: [PATCH 12/12] Fix broken test. --- .../java/com/ivy/data/db/IvyRoomDatabaseMigrationTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shared/data/src/androidTest/java/com/ivy/data/db/IvyRoomDatabaseMigrationTest.kt b/shared/data/src/androidTest/java/com/ivy/data/db/IvyRoomDatabaseMigrationTest.kt index cfe9b24bb6..e9b0d7888e 100644 --- a/shared/data/src/androidTest/java/com/ivy/data/db/IvyRoomDatabaseMigrationTest.kt +++ b/shared/data/src/androidTest/java/com/ivy/data/db/IvyRoomDatabaseMigrationTest.kt @@ -129,7 +129,7 @@ class IvyRoomDatabaseMigrationTest { getString(0) shouldBe loanId getDouble(1) shouldBe 123.50 getString(2) shouldBe noteString - getString(7) shouldBe "DECREASE" + getString(10) shouldBe "DECREASE" } newDb.close() }