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..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 @@ -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.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 @@ -164,22 +165,26 @@ 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 = if (loanTotalAmount != 0.0) { + amountPaid / loanTotalAmount + } else { + 0.0 + } 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 +305,23 @@ 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 - - loanRecords.forEach { loanRecord -> - if (!loanRecord.interest) { - val convertedAmount = loanRecord.convertedAmount ?: loanRecord.amount - amount += convertedAmount - } + 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 + + loanRecord.loanRecordType.processByType( + decreaseAction = { currentAmountPaid + convertedAmount to currentLoanTotalAmount }, + increaseAction = { currentAmountPaid to currentLoanTotalAmount + 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 = "", 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..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 @@ -33,7 +33,9 @@ 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.base.model.processByType import com.ivy.data.model.LoanType import com.ivy.design.l0_system.UI import com.ivy.design.l0_system.style @@ -125,6 +127,7 @@ private fun BoxWithConstraintsScope.UI( Header( loan = state.loan, baseCurrency = state.baseCurrency, + loanTotalAmount = state.loanTotalAmount, amountPaid = state.amountPaid, loanAmountPaid = state.loanAmountPaid, itemColor = itemColor, @@ -172,6 +175,13 @@ private fun BoxWithConstraintsScope.UI( ) } ) + item { + InitialRecordItem( + loan = state.loan, + amount = state.loan.amount, + baseCurrency = state.baseCurrency, + ) + } } if (state.displayLoanRecords.isEmpty()) { @@ -234,14 +244,14 @@ private fun BoxWithConstraintsScope.UI( private fun Header( loan: Loan, 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) @@ -278,7 +288,7 @@ private fun Header( }, textColor = contrastColor, currency = baseCurrency, - balance = loan.amount, + balance = loanTotalAmount, ) Spacer(Modifier.height(20.dp)) @@ -288,6 +298,7 @@ private fun Header( baseCurrency = baseCurrency, amountPaid = amountPaid, loanAmountPaid = loanAmountPaid, + loanTotalAmount = loanTotalAmount, selectedLoanAccount = selectedLoanAccount, onAddRecord = onAddRecord ) @@ -356,10 +367,12 @@ private fun LoanItem( } } +@Suppress("LongMethod") @Composable private fun LoanInfoCard( loan: Loan, baseCurrency: String, + loanTotalAmount: Double, amountPaid: Double, loanAmountPaid: Double = 0.0, selectedLoanAccount: Account? = null, @@ -373,8 +386,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( @@ -437,7 +450,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 @@ -707,9 +720,23 @@ private fun LoanRecordItem( if (loanRecord.note.isNullOrEmpty()) { Spacer(Modifier.height(16.dp)) } + val transactionType = when (loan.type) { + LoanType.LEND -> { + loanRecord.loanRecordType.processByType( + increaseAction = { TransactionType.EXPENSE }, + decreaseAction = { TransactionType.INCOME } + ) + } + LoanType.BORROW -> { + loanRecord.loanRecordType.processByType( + increaseAction = { TransactionType.INCOME }, + decreaseAction = { TransactionType.EXPENSE } + ) + } + } TypeAmountCurrency( - transactionType = if (loan.type == LoanType.LEND) TransactionType.INCOME else TransactionType.EXPENSE, + transactionType = transactionType, dueDate = null, currency = baseCurrency, amount = loanRecord.amount @@ -730,6 +757,61 @@ 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( @@ -783,6 +865,7 @@ private fun Preview_Empty() { ), displayLoanRecords = persistentListOf(), amountPaid = 3821.00, + loanTotalAmount = 4023.54, loanAmountPaid = 100.0, accounts = persistentListOf(), selectedLoanAccount = null, @@ -816,14 +899,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,10 +916,12 @@ private fun Preview_Records() { amount = 1000.00, dateTime = timeNowUTC().minusMonths(1), note = "Revolut", - loanId = UUID.randomUUID() + loanId = UUID.randomUUID(), + loanRecordType = LoanRecordType.INCREASE ) ), ), + 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..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 @@ -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,22 @@ 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. + 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 + } + associatedTransaction = ioThread { transactionDao.findLoanTransaction(loanId = loan.value!!.id)?.toDomain() } 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..0289ecafde --- /dev/null +++ b/shared/base/src/main/java/com/ivy/base/model/LoanRecordType.kt @@ -0,0 +1,19 @@ +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 +} + +fun LoanRecordType.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/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 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..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 @@ -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,61 @@ 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 + getString(10) shouldBe "DECREASE" + } + newDb.close() + } + @Test fun migrateAll() { // given: 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..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 @@ -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,11 @@ class RoomTypeConverters { @TypeConverter fun parseInstant(value: Long): Instant = Instant.ofEpochMilli(value) + + @TypeConverter + fun saveLoanRecordType(value: LoanRecordType?): String? = value?.name + + @TypeConverter + fun parseLoanRecordType(value: String?): LoanRecordType? = + 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..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 @@ -5,11 +5,13 @@ 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 import java.util.* +@Suppress("DataClassDefaultValues") @Keep @Serializable @Entity(tableName = "loan_records") @@ -32,6 +34,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 = LoanRecordType.DECREASE, @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..a00e0db1d2 --- /dev/null +++ b/shared/data/src/main/java/com/ivy/data/db/migration/Migration126to127_LoanRecordType.kt @@ -0,0 +1,11 @@ +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'") + } +} \ 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 e41371e072..1cdd51fb84 100644 --- a/shared/resources/src/main/res/values/strings.xml +++ b/shared/resources/src/main/res/values/strings.xml @@ -443,4 +443,8 @@ Total (exclusive): %1$s %2$s Total Balance Total Balance (excluded) + 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..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 @@ -1,5 +1,6 @@ package com.ivy.wallet.ui.theme.modal +import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.clickable @@ -29,8 +30,10 @@ 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 +43,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 +68,7 @@ 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(), ) @Deprecated("Old design system. Use `:ivy-design` and Material3") @@ -109,6 +111,9 @@ fun BoxWithConstraintsScope.LoanRecordModal( var reCalculateVisible by remember(modal) { mutableStateOf(modal?.loanAccountCurrencyCode != null && modal.loanAccountCurrencyCode != modal.baseCurrency) } + var loanRecordType by remember(modal) { + mutableStateOf(modal?.loanRecord?.loanRecordType ?: LoanRecordType.DECREASE) + } var amountModalVisible by remember { mutableStateOf(false) } var deleteModalVisible by remember(modal) { mutableStateOf(false) } @@ -128,7 +133,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 +145,7 @@ fun BoxWithConstraintsScope.LoanRecordModal( selectedAccount = selectedAcc, createLoanRecordTransaction = createLoanRecordTrans, reCalculateAmount = reCalculate, + loanRecordType = loanRecordType, onCreate = onCreate, onEdit = onEdit, @@ -238,6 +244,24 @@ 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 = { + if (it == LoanRecordType.INCREASE) loanInterest = false + loanRecordType = it + }) + + Spacer(Modifier.height(16.dp)) + IvyCheckboxWithText( modifier = Modifier .padding(start = 16.dp) @@ -248,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) { @@ -339,6 +365,7 @@ fun BoxWithConstraintsScope.LoanRecordModal( selectedAccount = selectedAcc, createLoanRecordTransaction = createLoanRecordTrans, reCalculateAmount = reCalculate, + loanRecordType = loanRecordType, onCreate = onCreate, onEdit = onEdit, @@ -358,6 +385,7 @@ private fun save( createLoanRecordTransaction: Boolean = false, selectedAccount: Account? = null, reCalculateAmount: Boolean = false, + loanRecordType: LoanRecordType, onCreate: (CreateLoanRecordData) -> Unit, onEdit: (EditLoanRecordData) -> Unit, @@ -369,7 +397,8 @@ private fun save( amount = amount, dateTime = dateTime, interest = loanRecordInterest, - accountId = selectedAccount?.id + accountId = selectedAccount?.id, + loanRecordType = loanRecordType ) onEdit( EditLoanRecordData( @@ -387,7 +416,8 @@ private fun save( dateTime = dateTime, interest = loanRecordInterest, account = selectedAccount, - createLoanRecordTransaction = createLoanRecordTransaction + createLoanRecordTransaction = createLoanRecordTransaction, + loanRecordType = loanRecordType ) ) } @@ -395,6 +425,86 @@ 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.DECREASE, + selectedRecordType = selectedRecordType + ) { + onLoanRecordTypeChanged(it) + } + Spacer(modifier = Modifier.width(8.dp)) + LoanRecordType( + modifier = Modifier, + loanRecordType = LoanRecordType.INCREASE, + selectedRecordType = selectedRecordType + ) { + onLoanRecordTypeChanged(it) + } + } +} + +@Composable +private fun LoanRecordType( + loanRecordType: LoanRecordType, + selectedRecordType: LoanRecordType?, + modifier: Modifier = Modifier, + 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 + } + val selected = selectedRecordType == loanRecordType + val medium = UI.colors.medium + val rFull = UI.shapes.rFull + val selectedColor = UI.colors.green1 + 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)) + } +} + @Composable private fun AccountsRow( modifier: Modifier = Modifier,